Skip to content

性能优化与 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-distance6~8视距(区块)。性能第一杀手,降它最有效
simulation-distance4~6模拟距离(实体/红石活跃范围),可比视距小
network-compression-threshold256网络压缩阈值,调大减 CPU

视距是性能第一杀手

视距从 10 降到 6,加载区块数几乎减半(10 = 441 区块,6 = 169 区块)。玩家肉眼难察觉,TPS 立竿见影。

bukkit.yml ​

生怪上限直接降主线程负担:

yaml
spawn-limits:
  monsters: 70      # 默认 70,人多可降
  animals: 15
  water-animals: 5
  ambient: 15
ticks-per:
  animal-spawns: 400   # 生怪周期(tick)
  monster-spawns: 20

spigot.yml ​

实体 AI 和合并是重点:

yaml
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 提供更细的优化:

yaml
optimize-explosions: true            # 爆炸计算优化
mob-spawner-tick-rate: 2             # 刷怪笼周期
container-update-tick-rate: 1
max-auto-save-chunks-per-tick: 8

JVM 参数:Aikar's Flags ​

JVM 参数对性能影响巨大。用社区验证的 Aikar's Flags:

bash
java -Xms8G -Xmx8G -XX:+UseG1GC -XX:+ParallelRefProcEnabled \
     -XX:MaxGCPauseMillis=200 -jar paper.jar nogui

-Xms 等于 -Xmx

最小堆和最大堆设成一样(如都 8G),避免运行时堆扩缩的开销。给服务器内存的 70% 左右,别全占。

配置改完要重启

大部分配置改完需重启生效(不是 reload)。逐项改、每次改完测 TPS 对比,找出最有效的那几项。一次改太多看不出哪个起作用。

常见优化手段 ​

1. 高频事件瘦身 ​

PlayerMoveEvent 里只做必要判断,重逻辑延迟处理:

java
@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(),缓存区块状态。

java
// 坏:每次移动都遍历所有区域
for (Region r : regions) {
    if (r.contains(loc)) { ... }
}

// 好:按区块索引,只查当前区块的区域
List<Region> nearby = regionIndex.get(chunkKey);

3. 降低频率 ​

不必每 tick 都跑。用 runTaskTimer 控制频率:

  • 全服广播:每 5 分钟,不每 tick
  • 排行榜重算:每 3 分钟
  • 数据保存:每 5 分钟

4. 批处理 ​

大量方块操作别一个个 setType,用批量:

java
// 慢:逐个设置(每次都触发更新)
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 网络游戏首页。