# JVM 调优实战
用生活化的比喻,让你从"听说过 GC 调优"到"能排查线上 OOM 和卡顿"
前置知识:第01章 JVM 深度原理(GC 算法、JIT 编译)、第02章 Java 并发编程深度
阅读指南(初学者必看)
为什么你需要学习 JVM 调优?
游戏服务器线上最怕两件事:
- OOM(OutOfMemory):服务直接挂掉,所有玩家断线
- 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 功能更强但开销大,适合开发/测试环境。
实践任务
- 模拟 OOM 并排查:写一个内存泄漏程序,用 MAT 分析 heap dump
- GC 日志分析:用 GCEasy.io 分析一段 GC 日志,找出问题
- G1 vs ZGC 对比:同一程序分别用 G1 和 ZGC 跑,对比暂停时间和吞吐量
- JFR 分析实战:用 JFR 找到一个游戏服务器的 CPU 热点方法
- 制定 GC 调优方案:为一个 4 核 8G 的游戏服务器制定 JVM 参数方案
与其他章节的关联
| 本节内容 | 关联章节 | 关联点 |
|---|---|---|
| GC 原理 | 第01章 JVM 深度原理 | GC 算法是调优的理论基础 |
| 内存泄漏 | 2_3 第04章 内存泄漏排查体系 | 浏览器和 JVM 的泄漏模式相似 |
| 线程问题 | 第02章 Java 并发编程深度 | 锁竞争导致 STW 时间变长 |
| 容器化 | 第15章 云原生与容器化 | Docker 中 JVM 需要容器感知 |
⬅️ 上一章:RPC 框架与微服务通信 | ➡️ 下一章:高并发与分布式一致性