# JVM 调优实战

用生活化的比喻,让你从"听说过 GC 调优"到"能排查线上 OOM 和卡顿"

前置知识:第01章 JVM 深度原理(GC 算法、JIT 编译)、第02章 Java 并发编程深度


阅读指南(初学者必看)

为什么你需要学习 JVM 调优?

游戏服务器线上最怕两件事:

  1. OOM(OutOfMemory):服务直接挂掉,所有玩家断线
  2. STW(Stop-The-World):GC 暂停导致所有玩家卡顿

学完本章,你能回答:

  • 线上 OOM 了,怎么保留现场、定位原因、快速恢复?
  • G1 和 ZGC 的调优参数分别怎么设?游戏服务器推荐哪个?
  • 怎么看 GC 日志判断 GC 是否健康?
  • 怎么用 JFR(Java Flight Recorder)做性能分析?
  • Arthas 怎么用?

本文结构

第一部分:OOM 排查流程(最紧急的线上问题)
第二部分:GC 日志分析(看懂 GC 的"体检报告")
第三部分:G1 调优实战(游戏服务器默认选择)
第四部分:ZGC 调优实战(超低延迟选择)
第五部分:JFR 性能分析(找到性能瓶颈)
第六部分:Arthas 工具使用

一、OOM 排查流程

生活类比:OOM 就像"办公室满了,新员工进不来"。你需要找出谁占了最多空间,然后决定是扩容还是清理。

OOM 排查六步法

1. 保留现场
   - -XX:+HeapDumpOnOutOfMemoryError  ← 启动时加这个参数
   - -XX:HeapDumpPath=/data/dumps/     ← 指定 dump 文件路径
   - 发生 OOM 时自动生成 heap dump

2. 获取 dump
   - 如果没加参数:jmap -dump:format=b,file=heap.hprof <pid>
   - 或者:jcmd <pid> GC.heap_dump /data/dumps/heap.hprof

3. 分析 dump
   - 用 MAT(Memory Analyzer Tool)打开 heap dump
   - 查看 Dominator Tree(谁占内存最多)
   - 查看 Leak Suspects Report(可疑泄漏点)

4. 定位泄漏
   - 从最大对象开始,查看引用链
   - 找到"GC Root → ... → 泄漏对象"的路径
   - 确定:谁持有引用不释放?

5. 修复代码
   - 关闭未关闭的资源(连接、流)
   - 移除缓存中不再使用的对象
   - 取消不再需要的监听器/回调
   - 缩小对象的作用域

6. 验证修复
   - 重新压测
   - 监控内存趋势
   - 确认不再泄漏

常见 OOM 场景与修复

OOM 类型 原因 修复方法
Java heap space 堆内存不足 增大堆/修复泄漏
Metaspace 类加载过多 增大 Metaspace/排查动态代理
GC overhead limit GC 回收太少 同 Java heap space
Direct buffer NIO 堆外内存泄漏 检查 ByteBuf 是否释放
unable to create thread 线程数太多 检查线程池配置

游戏服务器常见内存泄漏模式

// 泄漏模式1:事件监听器未移除
public class Room {
  private List<Listener> listeners = new ArrayList<>();
  
  public void addListener(Listener l) { listeners.add(l); }
  // ❌ 没有 removeListener!玩家退出后 Listener 还在
  // ✅ 玩家退出时调用 removeListener
}

// 泄漏模式2:缓存无限增长
public class PlayerCache {
  private Map<Long, Player> cache = new HashMap<>();
  
  public Player get(long id) { return cache.get(id); }
  // ❌ 只有 put 没有 evict!
  // ✅ 使用 Caffeine/Guava Cache 设置最大容量和过期时间
}

// 泄漏模式3:ThreadLocal 未清理
public class GameContext {
  private static ThreadLocal<GameSession> session = new ThreadLocal<>();
  // ❌ 线程池中线程复用,ThreadLocal 不清理会累积
  // ✅ 请求处理完后调用 session.remove()
}

二、GC 日志分析

生活类比:GC 日志就像"医院的体检报告"。你不需要理解每一项指标,但需要知道哪些异常值需要关注。

开启 GC 日志

# JDK 8
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/data/logs/gc.log

# JDK 11+
-Xlog:gc*:file=/data/logs/gc.log:time,uptime,level,tags

GC 日志关键指标

[2026-04-23T16:30:00.123+0800] GC pause (G1 Evacuation Pause) (young)
  [Eden: 256.0M(256.0M)->0.0B(230.0M)      ← Eden 从 256M 清空
   Survivors: 0.0B->26.0M                    ← Survivor 从 0 涨到 26M
   Heap: 1.5G(2.0G)->1.3G(2.0G)]            ← 堆从 1.5G 降到 1.3G
 [Times: user=0.23 sys=0.01, real=0.02 secs]  ← real=20ms ✅ 很好

需要关注的指标:
1. real time(实际暂停时间)── 游戏服务器要求 < 50ms
2. GC 频率 ── Young GC 间隔应 > 1秒
3. Full GC ── 应该几乎不发生(< 1次/天)
4. 堆使用率 ── Full GC 后应 < 70%

GC 健康判断标准

指标 健康 警告 危险
Young GC 暂停 < 50ms 50~200ms > 200ms
Full GC 暂停 < 500ms 500ms~2s > 2s
Young GC 频率 每 2~5 秒 每 1 秒 < 1 秒
Full GC 频率 < 1 次/天 每小时 每分钟
Full GC 后堆使用率 < 60% 60~80% > 80%

三、G1 调优实战

生活类比:G1 调优就像"调节空调温度"——设置一个目标(MaxGCPauseMillis),G1 会自动朝这个目标努力。

G1 核心参数

# 基础配置
-XX:+UseG1GC                           # 使用 G1
-Xms4g -Xmx4g                          # 堆大小(固定,避免扩缩容)
-XX:MaxGCPauseMillis=200               # 目标暂停时间 200ms

# 进阶配置
-XX:G1HeapRegionSize=8m                # Region 大小(1/2/4/8/16/32M)
-XX:InitiatingHeapOccupancyPercent=45  # 触发并发标记的堆占用率
-XX:ConcGCThreads=4                    # 并发标记线程数
-XX:ParallelGCThreads=8                # STW 阶段并行线程数

# 游戏服务器推荐配置(4核8G)
-XX:+UseG1GC
-Xms4g -Xmx4g
-XX:MaxGCPauseMillis=100               # 游戏要求更低延迟
-XX:G1HeapRegionSize=4m
-XX:InitiatingHeapOccupancyPercent=40  # 更早触发标记,避免 Full GC
-XX:ParallelGCThreads=4
-XX:ConcGCThreads=2

G1 调优常见误区

误区 正确做法
MaxGCPauseMillis 设太小(如 10ms) G1 只是尽量接近,设太小会导致 GC 更频繁
手动设置新生代大小 G1 会自动调整,手动设置反而限制 G1 的灵活性
Region 大小设太大 Region 越大,GC 粒度越粗,一般 4~8M 足够
只看暂停时间不看吞吐量 暂停短但 GC 太频繁也不好,需要平衡

四、ZGC 调优实战

生活类比:ZGC 就像"不停车收费站"——车辆(线程)不需要停下来等收费(GC),几乎零延迟通过。

ZGC 核心参数

# JDK 17+ ZGC 配置
-XX:+UseZGC                            # 使用 ZGC
-Xms8g -Xmx8g                          # ZGC 适合大堆

# 进阶配置
-XX:ZCollectionInterval=0              # 自动 GC
-XX:ZAllocationSpikeTolerance=2        # 分配尖峰容忍度
-XX:ZFragmentationLimit=5              # 碎片率上限(%)
-XX:SoftMaxHeapSize=6g                 # 软最大堆(尽量不超这个值)

# 游戏服务器推荐配置(8核16G)
-XX:+UseZGC
-Xms8g -Xmx8g
-XX:SoftMaxHeapSize=6g
-XX:ZAllocationSpikeTolerance=3
-XX:ConcGCThreads=2

G1 vs ZGC 游戏服务器选型

G1 ZGC
暂停时间 10~200ms < 1ms
适合堆大小 2~8GB 8GB+
吞吐量 略低(5~10%)
JDK 版本 JDK 8+ JDK 15+(生产可用 JDK 17+)
游戏推荐 通用选择 ✅ 对延迟极度敏感的服务 ✅

建议:战斗服务器用 ZGC(延迟最敏感),其他服务用 G1(吞吐量优先)。


五、JFR 性能分析

生活类比:JFR 就像"飞机的黑匣子"——一直在后台记录,出问题时回放分析。

JFR 使用

# 启动时开启 JFR
java -XX:StartFlightRecording=duration=60s,filename=game.jfr ...

# 运行中开启
jcmd <pid> JFR.start duration=60s filename=game.jfr

# 用 JDK Mission Control 打开 game.jfr 分析

JFR 能分析什么

分析维度 JFR 事件 游戏场景
CPU 热点 Method Profiling 找到最耗 CPU 的方法
内存分配 Object Allocation 找到分配最多的对象
GC 活动 GC Events 分析 GC 频率和耗时
线程阻塞 Java Monitor Blocked 找到锁竞争热点
IO 操作 File/Socket Read/Write 找到 IO 瓶颈
异常 Exception Throw 统计异常频率

六、Arthas 工具使用

Arthas 是什么?

Arthas是阿里开源的Java诊断工具,功能强大!

# 一键安装
curl -L https://alibaba.github.io/arthas/install.sh | sh

# 启动
java -jar arthas-boot.jar

# 或者 attach到已运行的进程
java -jar arthas-boot.jar <pid>

常用命令

1. dashboard - 查看全局

$ dashboard

显示:线程信息、内存信息、GC信息

2. thread - 查看线程

# 查看CPU占用最高的线程
$ thread -n 5

# 查看阻塞的线程
$ thread -b

3. trace - 性能追踪

# 追踪方法调用链路和耗时
$ trace com.game.server.GameServer processMessage

# 只显示超过10ms的调用
$ trace com.game.server.GameServer processMessage '#cost > 10'

4. heapdump - 堆快照

# 生成堆快照
$ heapdump

# 只生成活动对象
$ heapdump --live

自问自答

Q1:OOM 后服务要不要立即重启?

先保留现场(生成 dump),然后重启恢复服务。同时分析 dump 找根因,避免复发。如果加了 -XX:+HeapDumpOnOutOfMemoryError,OOM 时会自动 dump,重启后分析即可。

Q2:G1 和 ZGC 怎么选?

堆 < 8GB 用 G1,堆 > 8GB 用 ZGC。如果对延迟极度敏感(如战斗服务),即使堆不大也推荐 ZGC。

Q3:GC 调优第一步做什么?

先加 GC 日志参数,跑一天后看日志。不要凭感觉调参——先看数据再决定。

Q4:为什么游戏服务器要固定 -Xms 和 -Xmx?

堆扩缩容会触发 Full GC。游戏服务器启动时就分配好内存,避免运行时因堆大小变化导致 STW。

Q5:JFR 和 JProfiler 有什么区别?

JFR 是 JDK 内置的,开销极低(< 2%),可以在线上持续运行。JProfiler 功能更强但开销大,适合开发/测试环境。


实践任务

  1. 模拟 OOM 并排查:写一个内存泄漏程序,用 MAT 分析 heap dump
  2. GC 日志分析:用 GCEasy.io 分析一段 GC 日志,找出问题
  3. G1 vs ZGC 对比:同一程序分别用 G1 和 ZGC 跑,对比暂停时间和吞吐量
  4. JFR 分析实战:用 JFR 找到一个游戏服务器的 CPU 热点方法
  5. 制定 GC 调优方案:为一个 4 核 8G 的游戏服务器制定 JVM 参数方案

与其他章节的关联

本节内容 关联章节 关联点
GC 原理 第01章 JVM 深度原理 GC 算法是调优的理论基础
内存泄漏 2_3 第04章 内存泄漏排查体系 浏览器和 JVM 的泄漏模式相似
线程问题 第02章 Java 并发编程深度 锁竞争导致 STW 时间变长
容器化 第15章 云原生与容器化 Docker 中 JVM 需要容器感知

⬅️ 上一章:RPC 框架与微服务通信 | ➡️ 下一章:高并发与分布式一致性