JDK 11+ 必须用 -Xlog:gc*:file=gc.log:time,uptime,pid,tags 开启 GC 日志,旧参数已废弃;需关注标签组合、滚动策略及 GCEasy 等工具辅助分析。

Java GC日志现在必须用 -Xlog,旧参数如 -XX:+PrintGCDetails 在 JDK 11+ 已废弃,硬配会静默失效或报错。
怎么开 GC 日志(JDK 11 及以上)
新版 JVM 统一用 -Xlog,语法是 -Xlog:<tags>:<output>:<decorators>,不是拼参数,而是“选标签 + 定输出 + 加修饰”。
-
-Xlog:gc*:file=gc.log:time,uptime,pid,tags是最常用组合:开启所有 GC 相关标签,写入文件,带时间戳、JVM 运行时长、进程号和操作类型(如gc,age或gc,heap) - 别再用
-Xloggc:gc.log—— 它在 JDK 10 就被标记为 deprecated,JDK 17 起直接不识别 - 如果只想要轻量日志,用
-Xlog:gc=info;要分析晋升年龄,必须加gc,age标签,否则MaxTenuringThreshold相关行为根本看不到 - 生产环境务必加滚动策略:
:filecount=5,filesize=20M,否则单个日志跑满磁盘很常见
怎么看 GC 日志里关键字段(以 G1 为例)
GC 日志不是日志,是结构化快照。比如一行 [2.456s][info][gc,heap,exit] Heap after GC: 248M(512M) 中,2.456s 是 uptime(非系统时间),gc,heap,exit 是标签组合,248M(512M) 表示当前已用 / 总堆大小。
-
[GC pause (G1 Evacuation Pause) (young)]表示这次是 Young GC;(mixed)才说明 G1 开始清理老年代部分 Region - 关注
[Eden: 120M(120M)->0B(120M)]这类括号对:前者是 GC 前占用 / 总容量,后者是 GC 后占用 / 总容量。如果->0B后仍是(120M),说明 Eden 空间没被压缩,只是对象挪走了 - 出现
[Full GC (Ergonomics)]要立刻警觉:这不是正常路径,通常是元空间爆了、堆碎片严重、或 G1 无法在目标停顿内完成回收 -
[Times: user=0.04s, sys=0.00s, real=0.03s]中real才是 STW 时间,user是 CPU 时间总和,多核下可能比real大
为什么用 GCEasy 而不是自己 parse 日志
GC 日志文本格式看似简单,但不同 GC 算法(ZGC、Shenandoah、G1、Parallel)输出字段差异极大,同一算法在不同 JDK 小版本中也会微调字段名或单位(比如有的用 K,有的用 k,有的省略单位)。
立即学习“Java免费学习笔记(深入)”;
- GCEasy 会自动识别 JDK 版本和 GC 类型,把原始日志转成统一的 JSON 结构,再生成吞吐量、暂停分布、内存趋势三类图表
- 它能标出异常点:比如某次 Young GC 耗时突然从 20ms 跳到 180ms,会打上
⚠️ Latency spike并关联前后 3 次 GC 的晋升速率 - 别信“自己写脚本 grep”,
grep "Full GC"会漏掉 ZGC 的Pause Final Mark和 Shenandoah 的Concurrent evacuation—— 它们不算 Full GC,但停顿一样致命 - 本地离线分析可用
jstat -gc <pid> 1s实时看,但它不记录历史,也没办法回溯“第 17 次 GC 时老年代用了多少”
容易被忽略的三个配置陷阱
很多团队开了日志却看不出问题,不是日志没用,是参数没对上。
- 忘了加
gc,metaspace标签:元空间 OOM 往往先于堆 OOM 发生,但默认gc*不包含 metaspace,结果看到频繁 Full GC 却查不到Metaspace used增长曲线 - 用
-Xlog:gc=debug代替=info:debug 级别每秒输出几百行,IO 压力大,且会掩盖关键路径(比如混在几十行 region 移动日志里,根本找不到 STW 时间) - 在容器环境没设
-XX:+UseContainerSupport:Kubernetes 里-Xmx若没对齐 cgroup memory limit,JVM 会按宿主机内存算堆,默认新生代比例爆炸,导致大量短命对象直接进老年代,GC 日志里全是 mixed GC 却查不出代码问题
GC 日志本身不解决问题,它只告诉你“哪里疼”;真正难的是把 [GC pause (mixed)] 和你代码里那个没关的 InputStream、那个没设 maxIdleTime 的连接池、或者那个被反复 new String(byte[]) 的解析逻辑连起来 —— 那些才是日志背后的真实敌人。










