主题
界面
主题
「调优」不是背参数,而是:看懂日志 → 定位病 → 再动手。
★ 本节跑出三份真实 GC 日志(稳态 / 泄漏 OOM / 大对象突发),再用自写解析器读它们:
steady(128M 堆):Young 13 次 0 次 Full,存活量稳定在 9M ✓✓
leak (64M 堆) :存活量 61M→63M 顶住上限,Full GC 5 次(G1 Compaction Pause)后 OOM ✗✗
spike(128M 堆):Full GC 里 3 次原因写着 (System.gc()) —— 是代码自己调的 ✗★★ 一个反直觉的实测细节:steady 组 13 次 Young GC 的原因全是 (G1 Humongous Allocation) ——小堆 + 1MB 数组 = 每个数组都够 Humongous 门槛(呼应 《JVM 内存结构与对象创建》)✓✓
-Xlog:gc* 并逐字段读一条 Pause 行 ★★★;★ 线上「接口偶尔卡一下」的头号嫌疑人就是 GC ✗✓(呼应 T2-014 的 P99 毛刺)—— 而排查的起点不是调参,是日志 ✓✓✓:
| 只会这一句 | 实际要看 |
|---|---|
| 「加内存就好了」✗ | 存活量是否持续上涨(泄漏加内存也白搭) |
| 「换 ZGC 就好了」✗ | Full GC 的原因是什么(是代码在调 System.gc()) |
| 「GC 次数太多」⚠️ | 次数 × 单次停顿的总账,以及是什么在触发 |
java -Xmx64m "-Xlog:gc*:file=gc.log" -cp <classes> <主类>★ 三个关键参数 ✓✓: gc*(全部 gc 相关标签)/ file=(写文件,避免拖慢 stdout)/ 生产加 time,uptime,level,tags 等装饰 ✓。
GC(3) Pause Young (Normal) (G1 Evacuation Pause) 33M->1M(66M) 1.346ms
↑ ↑类型 ↑原因分类 ↑GC 名称 ↑回收前→回收后(堆容量) ↑停顿★★ 三元组读法:「活多少 → 还剩多少」比「回收了多少」更有信息量 ✓✓✓ —— 回收前后都是 30M+ = 对象回收不掉(泄漏信号) ✗✗。
| steady(稳态) | leak(泄漏) | spike(大对象) | |
|---|---|---|---|
| 参数 | -Xmx128m | -Xmx64m | -Xmx128m |
| Young 次数 | 13 | 6 | 4 |
| Full 次数 | 0 ✓✓ | 5(Compaction)✗✗ | 5(3 次 System.gc())✗ |
| 存活量趋势 | 9M 平坦 ✓ | 61M→63M 顶上限 ✗✗✗ | 18M→83M→66M→…→1M |
| 结局 | 正常结束 | OutOfMemoryError: Java heap space ✗✗✗ | 128MB 分配失败 ✗ |
★ leak 的关键证据链 ✓✓✓:存活量顶到堆上限(63M/64M)→ Full GC 反复 Compaction → 最终 OOM —— 而 G1 Compaction Pause 的含义就是「整理也腾不出空间了」✗✗。
★ spike 组的 Full GC 原因分布(实测):
Full|(System.gc()) x 3 ← 实验代码里每轮显式调用 System.gc() ✓ 是我们自己引起的
Young|(Concurrent Start) (G1 Humongous Allocation) x 2 ← 大对象触发
Full|(G1 Compaction Pause) x 2 ← 真正「装不下」的整理★★ 同一个数字,原因不同,含义完全不同 ✓✓✓: 生产代码里出现 System.gc() 是禁忌(一次 Full GC 可能 = 数百毫秒停顿)✗✗;而 G1 Compaction Pause 才是内存压力信号 ✓。
① 定目标:低延迟(P99 < 100ms)还是高吞吐(批处理跑得快)?
② 看日志:次数 × 单次停顿;存活量趋势;Full GC 有无与原因
③ 定手段:
存活量上涨 → 查代码(泄漏/无界缓存),调参救不了 ✗✗
存活量平坦 → 堆/收集器/Region 才有意义 ✓
大对象 → 分块处理 / 调大 Region / 抬高 Humongous 门槛 ✓
④ 验证:同一压测流量,前后对比「次数 × 停顿」与业务 SLO ✓✓★ 参数速查 ✓:
| 参数 | 作用 | 备忘 |
|---|---|---|
-Xms / -Xmx | 初始 / 最大堆 | 生产建议相等,避免扩容抖动 ✓✓ |
-XX:+UseG1GC 等 | 收集器 | 按堆大小与延迟目标选(《垃圾回收算法与收集器》)✓ |
-XX:G1HeapRegionSize | Region 大小 | 大对象多时调大,减少 Humongous ✓ |
-XX:+HeapDumpOnOutOfMemoryError | OOM 自动导堆 | 上生产前必配 ✓✓✓ |
# 泄漏演练:64M 堆,几十秒看到 OOM
java -Xmx64m "-Xlog:gc*:file=gc-leak.log" -cp <classes> cn.kb.learn.deep.jvm.GcTuningLesson leakjava -cp <classes> cn.kb.learn.deep.jvm.GcLogParser gc-leak.logleak 场景把 -Xmx 从 64M 加到 128M → OOM 推迟,但照样 OOM ✗✗
根因是「静态集合只进不出」,加内存只是延后崩盘 ✓✓代码:labs/java-deep/src/main/java/cn/kb/learn/deep/jvm/GcTuningLesson.java(三种负载)+ GcLogParser.java(日志分析)
实测一(steady,-Xmx128m):
[steady] 每轮 8MB 短命对象 x 40 轮
第 10 轮结束,堆使用约 18MB
第 20 轮结束,堆使用约 34MB
第 30 轮结束,堆使用约 50MB
第 40 轮结束,堆使用约 17MB
-- 次数与停顿 --
Young GC:13 次,平均 1.8 ms,最长 3.4 ms
全部 GC:13 次,总停顿 22.8 ms
-- 原因分布 --
Young|(Concurrent Start) (G1 Humongous Allocation) x 13
-- 堆趋势(最后 8 次)--
9M → 9M → 9M → 9M → 9M → 9M → 9M → 9M
✓ 全程 0 次 Full GC —— 说明没有「整堆都被卡住」的回收
✓ 停顿平缓(最长 3.4 ms / 平均 1.8 ms)—— 对在线服务友好实测二(leak,-Xmx64m) —— 本节最有信息量的一份:
已累积 10MB,堆使用约 22MB
已累积 20MB,堆使用约 41MB
已累积 30MB,堆使用约 61MB
第 32 MB 分配失败 → OutOfMemoryError: Java heap space
(静态集合已持有 31MB,已强制清空自救)
-- 次数与停顿 --
Young GC:6 次,平均 0.9 ms;Full GC:5 次,平均 2.8 ms
-- 原因分布 --
Full|(G1 Compaction Pause) x 5
Young|(Concurrent Start) (G1 Humongous Allocation) x 3
...
-- 堆趋势(最后 8 次)--
61M → 61M → 63M → 63M → 63M → 63M → 63M → 1M
✗✗ 存活量顶到堆上限附近(63M / 上限 64M):OOM 的前兆或已经发生
✗✗ 出现 Full GC 5 次 —— 先看上面的原因★★ 最后那条「1M」是故意的教学点 ✓✓✓:它来自 catch 到 OOM 后清空集合自救 —— 日志的最后一帧会「骗人」,读懂趋势必须结合代码行为 ✓✓。
实测三(spike,-Xmx128m):
已分配 16MB 单块(length=16777216),堆使用约 19MB
已分配 64MB 单块(length=67108864),堆使用约 83MB
分配 128MB 失败 → OutOfMemoryError: Java heap space
-- 原因分布 --
Full|(System.gc()) x 3
Young|(Concurrent Start) (G1 Humongous Allocation) x 2
Full|(G1 Compaction Pause) x 2
-- 堆趋势(最后 8 次)--
18M → 83M → 66M → 66M → 66M → 1M → 1M → 1M-Xmx 调成 128m:观察 OOM 只是「推迟」而不是消失 ✗✗✓;System.gc():观察 Full GC 从 5 次降到 2 次 ✓✓(原因分布直接给出答案);-XX:+HeapDumpOnOutOfMemoryError 重跑 leak:产出一个堆快照,为 《Arthas 与线上问题诊断》 的诊断埋伏笔 ✓✓✓。| # | 错误 | 说明 |
|---|---|---|
| 1 | 看到 Full GC 就加内存 ✗✗ | 先看原因:泄漏加多少都白搭 ✓✓ |
| 2 | 把 System.gc() 引起的 Full 当故障 ✗ | 原因分布里写得清清楚楚 ✓✓ |
| 3 | 只看 GC 次数不看单次停顿 ✗ | 次数 × 停顿才是总账 ✓ |
| 4 | 不开日志靠猜 ✗✗ | 「配置对了」和「观察到对了」是两回事 ✓✓ |
| 5 | 忽略 Humongous ✗ | 小堆下 1MB 数组就是 Humongous ✓(本节 steady 实测) |
| 6 | 没配 HeapDumpOnOutOfMemoryError 就上生产 ✗✗✗ | OOM 现场只出现一次 ✓ |
GcLogParser 输出「每次 Young GC 的间隔」:判断分配速率 ✓✓;ConcurrentHashMap 缓存)重复本节流程 ✓✓✓;-Xms64m 与 -Xms128m 下 leak 的 GC 次数差异 ✓。验收方式:拿到任何一份 -Xlog:gc* 日志,10 分钟内说出:有没有 Full、原因是什么、存活量是否健康、下一步该查什么。
扩展任务:给项目 A 配置「OOM 自动导堆 + GC 日志落盘」,并写一份《GC 健康基线》文档(正常频率、单次停顿上限、告警阈值)✓✓
33M->1M(66M) 三个数字的含义?(System.gc()) 与 (G1 Compaction Pause) 都算 Full GC,含义差在哪?-Xms=-Xmx(避免抖动) 与 -XX:+HeapDumpOnOutOfMemoryError(保留现场) ✓✓★《Arthas 与线上问题诊断》 Arthas 与线上问题诊断 —— 把「看日志」升级为「在线取证」。 监控侧的呼应:服务端监控与瓶颈分析见 T2-016。