性能优化与 TPS
服务器卡,玩家就跑。这一篇讲怎么发现卡顿、定位元凶、优化性能。
TPS 是什么
TPS = Ticks Per Second,每秒游戏刻数。理想是 20(1 tick = 50ms)。
| TPS | 状态 |
|---|---|
| 19~20 | 健康 |
| 15~18 | 轻微卡顿,怪物/红石有延迟 |
| 10~14 | 明显卡,玩家能感觉到 |
| <10 | 严重,几乎没法玩 |
用 /tps 查(Paper 原生支持,Spigot 装插件)。也可用 /spark 看详细。
MSPT
Paper 还有 MSPT(毫秒/刻)指标,比 TPS 更精细——直接看每刻花了多少 ms。50ms 内是健康,超了就开始掉 TPS。
卡顿的两大元凶
1. 主线程被阻塞
主线程负责所有游戏逻辑(方块、实体、事件)。任何耗时操作在主线程跑都会拖 TPS:
- 数据库查询(最常见杀手)
- 大文件读写
- HTTP 请求
- 大量循环计算
2. 高频事件里做重活
某些事件触发极频繁,里面做重计算就崩:
| 事件 | 频率 | 注意 |
|---|---|---|
PlayerMoveEvent | 每次移动+转头 | 高频,逻辑要极轻 |
BlockPhysicsEvent | 物理变化 | 高频,慎监听 |
EntityDamageEvent | 每次受伤 | 中频 |
ProjectileHitEvent | 投射物命中 | 中频 |
定位卡顿
用 Spark
Spark 是性能分析神器。装上后:
/spark profiler start # 开始采样
# 玩一会儿让卡顿发生
/spark profiler stop # 停止,生成报告链接报告会显示每个方法占用主线程时间的百分比,一眼看出元凶是哪个插件、哪段代码。
看 /timings
Spigot/Paper 自带 /timings on,玩一会儿 /timings paste 生成报告。比 Spark 粗,但能看各插件耗时占比。
先测量再优化
别盲猜哪里慢。先用 Spark/timings 量化,找到真正的瓶颈再动手。90% 的卡顿来自你没想到的地方。
纯配置优化(不改插件)
代码一行不改也能大幅提性能。改配置是最低成本的优化,先试这些再动代码。
server.properties
| 配置项 | 推荐 | 说明 |
|---|---|---|
view-distance | 6~8 | 视距(区块)。性能第一杀手,降它最有效 |
simulation-distance | 4~6 | 模拟距离(实体/红石活跃范围),可比视距小 |
network-compression-threshold | 256 | 网络压缩阈值,调大减 CPU |
视距是性能第一杀手
视距从 10 降到 6,加载区块数几乎减半(10 = 441 区块,6 = 169 区块)。玩家肉眼难察觉,TPS 立竿见影。
bukkit.yml
生怪上限直接降主线程负担:
spawn-limits:
monsters: 70 # 默认 70,人多可降
animals: 15
water-animals: 5
ambient: 15
ticks-per:
animal-spawns: 400 # 生怪周期(tick)
monster-spawns: 20spigot.yml
实体 AI 和合并是重点:
entity-activation-range:
animals: 16 # 激活范围,越小越省 AI
monsters: 24
raiders: 32
misc: 8
merge-radius:
item: 2.5 # 掉落物合并半径,减少实体数
exp: 3.0
max-entity-collisions: 2 # 限制碰撞检测,默认无限
mob-spawn-range: 3实体激活范围
只有玩家附近的怪才跑 AI。激活范围越小,远处怪物"休眠"不耗算力。降到合理值玩家基本无感。
paper.yml(Paper 专属)
Paper 提供更细的优化:
optimize-explosions: true # 爆炸计算优化
mob-spawner-tick-rate: 2 # 刷怪笼周期
container-update-tick-rate: 1
max-auto-save-chunks-per-tick: 8JVM 参数:Aikar's Flags
JVM 参数对性能影响巨大。用社区验证的 Aikar's Flags:
java -Xms8G -Xmx8G -XX:+UseG1GC -XX:+ParallelRefProcEnabled \
-XX:MaxGCPauseMillis=200 -jar paper.jar nogui-Xms 等于 -Xmx
最小堆和最大堆设成一样(如都 8G),避免运行时堆扩缩的开销。给服务器内存的 70% 左右,别全占。
配置改完要重启
大部分配置改完需重启生效(不是 reload)。逐项改、每次改完测 TPS 对比,找出最有效的那几项。一次改太多看不出哪个起作用。
常见优化手段
1. 高频事件瘦身
PlayerMoveEvent 里只做必要判断,重逻辑延迟处理:
@EventHandler
public void onMove(PlayerMoveEvent e) {
// 只在真正位移时处理(忽略转头)
if (e.getFrom().getX() == e.getTo().getX()
&& e.getFrom().getZ() == e.getTo().getZ()) return;
Location from = e.getFrom();
// 轻量判断:是否进入某区域
// 别在这里查数据库、遍历大集合
}2. 缓存
重复计算的值缓存起来。区域检测别每次 getBlocks(),缓存区块状态。
// 坏:每次移动都遍历所有区域
for (Region r : regions) {
if (r.contains(loc)) { ... }
}
// 好:按区块索引,只查当前区块的区域
List<Region> nearby = regionIndex.get(chunkKey);3. 降低频率
不必每 tick 都跑。用 runTaskTimer 控制频率:
- 全服广播:每 5 分钟,不每 tick
- 排行榜重算:每 3 分钟
- 数据保存:每 5 分钟
4. 批处理
大量方块操作别一个个 setType,用批量:
// 慢:逐个设置(每次都触发更新)
for (int x = 0; x < 16; x++)
for (int z = 0; z < 16; z++)
world.getBlockAt(x, 0, z).setType(Material.STONE);
// 快:用 BlockData 批量或设区块避免物理更新
setType 默认触发物理更新(流体、红石)。大批量改方块时若不需要物理,用 setType(material, false) 跳过更新。
5. 异步化
重计算(排序、过滤大列表)丢异步线程,结果回主线程。
6. 数据结构选择
- 频繁按 UUID 查 →
HashMap(别用List线性查找) - 区域检测 → 空间索引(网格分桶),别全量遍历
- 去重 →
Set,别用List.contains
监控
长期运营要持续监控,不是上线测一次就完:
- Spark 持续采样:定期看报告
- TPS 告警:TPS 低于阈值自动通知(接 Discord/邮件)
- 日志:慢查询、慢操作记日志
上线后回归
每次发版后测一次性能。新功能可能引入性能回归——上线前 spark 一次,上线后对比。
常见坑
Bukkit.getOnlinePlayers()在循环里反复调:缓存结果,别每次循环都调player.getLocation()频繁 new 对象:会产生大量 Location 对象,注意 GC- 遍历在线玩家做重活:玩家多了 O(n) 操作变慢,按需索引
- 同步保存所有玩家:一次保存几百玩家卡爆主线程,分批异步存
何时该优化
别过早优化
TPS 19+ 别瞎优化,反而引入 bug。先写对,再测,发现慢了再优化。"过早优化是万恶之源。"
只有 TPS 实际下降、或 Spark 显示某段确实耗时,才动手。健康的服务器不需要为优化而优化。
玩法开发 9 篇到此结束。回到插件开发总览或Java 网络游戏首页。
