简体中文
GCViewer 使用教程:离线 GC 日志分析与性能调优
GCViewer 是 JVM 圈子里最老牌的 GC 日志可视化工具:一个 jar、纯 Java 桌面应用、LGPL 开源,双击就能看堆的锯齿图和几十项统计数据,日志全程不出本机。
它和 EasyGC 读的是同一份日志,区别在于形态:EasyGC 在浏览器里跑(免安装、带泄漏检测与调优建议引擎),GCViewer 更适合日志不能外传的内网环境、需要批量/CI 出结构化数据,或者想逐条翻原始 GC 事件的场合。两者结论可以互相验证。
一、安装与启动
当前版本 1.37(2026-05-25 发布):
| 获取方式 | 地址 |
|---|---|
| 可执行 jar | https://sourceforge.net/projects/gcviewer/files/gcviewer-1.37.jar/download |
| macOS 应用包 | gcviewer-1.37-dist-mac.zip(同一个 SourceForge 目录) |
| Maven Central | com.github.chewiebug:gcviewer:1.37 |
第一个坑
GitHub Release 页面里没有附件,只有 SourceForge 的下载链接。找不到 jar 别以为项目没发版。
启动 GUI 需要一个 Java 8 及以上的虚拟机:
bash
java -jar gcviewer-1.37.jar # 打开 GUI
java -jar gcviewer-1.37.jar gc.log # 打开 GUI 并直接加载这份日志从源码构建(需要 Maven 3,产物在 target/):
bash
git clone https://github.com/chewiebug/GCViewer.git
cd GCViewer && mvn clean install要在自己的工具里复用它的解析器,直接引 Maven Central 坐标即可:
xml
<dependency>
<groupId>com.github.chewiebug</groupId>
<artifactId>gcviewer</artifactId>
<version>1.37</version>
</dependency>二、先准备一份能解析的日志
GCViewer 的解析能力取决于日志开关给了多少信息。按 JDK 版本对号入座:
| JDK | 推荐参数 | 说明 |
|---|---|---|
| Java 9+(统一日志) | -Xlog:gc=info:file="gc.log":tags,uptime,level | 官方给出的最小可用配置 |
| Java 9+(推荐) | -Xlog:gc*:file=gc.log:tags,time,uptime,level | 信息最全,含各阶段与并发周期 |
| Java 9+(最大) | -Xlog:gc*=trace:file="gc.log":tags,time,uptime,level | 多余 tag 会被忽略 |
| Oracle JDK 6 / 7 / 8 | -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps | 官方点名的最佳实践组合 |
| Sun JDK 1.4 / 1.5 | -Xloggc:gc.log -XX:+PrintGCDetails | |
| Sun JDK 1.2.2 / 1.3.1 | -verbose:gc | 无时间戳,算不出 Total Time / Throughput |
| IBM JDK 1.2.2 – 1.4.2 | -verbose:gc | 需保证事件没有换行 |
| HP-UX JDK 1.2 – 1.4 | -Xverbosegc | |
| BEA JRockit 1.4.2 – 1.6 | -verbose:memory -Xverbose:gcpause,gcreport |
两个额外的开关,会直接决定你能看到多少指标:
-XX:+PrintGCDetails—— 没有它,年轻代/老年代区域图、Promotion(晋升量)、InitiatingOccFraction 全部缺失。Java 9+ 的-Xlog:gc*已等价包含。-XX:+PrintGCApplicationStoppedTime—— 有了它才能看到 VM operations overhead,即安全点里不属于 GC的那部分停顿(偏向锁撤销、线程转储、类重定义等)。
版本差异
1.36 及更早版本里,统一日志(-Xlog)多余的 decoration 会导致解析直接失败;1.37 起已支持全部 decoration。如果你用的老版本报解析错误,先把 decoration 收敛到 tags,time,uptime,level 再试。
三、GUI 怎么看
打开日志
File → Open File 打开单文件;开了 -XX:+UseGCLogFileRotation 的轮转日志用 File → Open Series,把 gc.log.0…gc.log.N 当一个连续日志读;File → Open URL 甚至可以直接喂一个 URL。
压测时两个很实用的开关:
File → Watch—— 文件一变自动重载,边压边看;View → show datestamps—— 在"日志开始后的秒数"与"绝对时间戳"之间切换。
图表区(Chart)
| 图例 | 含义 | 能读出什么 |
|---|---|---|
| Total Heap(红线) | 已分配的堆总量 | 堆是否被撑大、有没有缩容 |
| Used Heap(蓝线) | 实际占用 | 锯齿 = 健康;GC 后谷底逐级抬升 = 内存泄漏 |
| Young / Tenured Generation(橙 / 洋红区域) | 分代占用(需 PrintGCDetails) | 老年代水位离上限还有多远 |
| Used young / used tenured heap | 分代实际占用 | 存活对象主要集中在哪一代 |
| Full GC Lines(黑竖线) | 每次 Full GC | 密度 = Full GC 频率 |
| Inc GC Lines(青竖线) | 每次增量 GC | |
| GC Times Line(绿线) | 停顿时间趋势 | 停顿是在变长还是稳定 |
| GC Times Rectangles(矩形) | 单次停顿时长,颜色区分 Full GC / initial-mark / remark / VM operation / 普通 GC / 增量 GC | 一眼看出长停顿来自哪一类事件 |
| Initial mark level(黄线) | 并发周期启动时的水位(CMS / G1) | 触发时机是否太晚 |
| Concurrent collections(青竖线开始 / 粉竖线结束) | 并发回收周期 | 并发周期是否跑不完就又被触发 |
View 菜单可以逐个开关上面这些图层;1.37 起支持滚轮缩放;在时间标尺上右键可以手工指定日志的绝对开始时间(日志带了 PrintGCDateStamps 时会自动带出)。
事件明细(Event details)
按事件类型分页:
- Gc pauses —— 非 Full GC 的 stop-the-world 停顿;
- Full gc pauses —— 跨代回收或名字里带 full gc 的停顿;
- VM operations overhead (safepoint pauses) —— 安全点相对 GC 的额外开销(需
PrintGCApplicationStoppedTime); - Concurrent GCs —— 并发周期信息(应用线程仍在跑的那部分)。
先看 Parser 标签页
旁边还有个 Parser 标签,列出解析过程中的警告。有报错就说明有行没吃进去,后面的统计是不完整的——先处理它再看指标。
数据面板(Data Panel,View → Data Panel)
三个标签页,下面这些是排查时真正会用到的:
Summary
| 指标 | 含义 | 关注点 |
|---|---|---|
| Footprint | 窗口内堆的最大占用 | 这台机器至少得给这么多内存 |
| Total Time | 日志覆盖时长 | 太短(几分钟)结论不可信 |
| Throughput | 业务线程时间占比 = 1 − 累计停顿 / 总时长 | > 95% 健康,< 90% 要调 |
| Accumulated pauses / GC / full GC | 各类停顿累计 | Full GC 那一项应该接近 0 |
| Freed Memory / Freed by GC / Freed by full GC | 回收掉的字节数 | |
| Freed Mem/Min | 每分钟回收量 | 粗略的分配速率 |
| GC Performance / Full GC Performance | 单位时间回收量 | 回收效率高不高 |
| Slope GC / Slope full GC | GC 后堆占用的增长斜率 | 持续为正 = 泄漏信号 |
| Avg rel inc after GC / after FGC | 每次 GC 后堆的相对增幅 | 同上,另一个角度 |
Memory
| 指标 | 含义 |
|---|---|
| Total / Tenured / Young / Perm heap (usage / alloc. max) | 各代使用量与分配上限 |
| Avg / Max heap after conc GC | 并发周期后的堆占用(决定 Footprint 的下限) |
| Avg / Max heap after full GC | Full GC 后的堆占用 —— 逼近 Footprint 就是容量不足 |
| Avg / Max tenured after conc GC | 老年代水位 |
| Avg / Total promotion | 晋升量(需 PrintGCDetails)—— 每次 Young GC 有多少对象进了老年代 |
| InitiatingOccFraction (avg / max) | 并发周期触发时的老年代占用比例(CMS 调 -XX:CMSInitiatingOccupancyFraction 的依据) |
Pause
| 指标 | 含义 |
|---|---|
| Number of pauses / gc pauses / full gc pauses | 停顿次数 |
| Accumulated / Avg / Min / Max Pause | 累计 / 平均 / 最小 / 最大停顿 |
| Avg pause interval / Min / max pause interval | 停顿间隔 —— 比次数更能反映频率与业务节奏 |
| Full gc pauses 同名一组 | Full GC 单独的统计(只有它没有 avg interval 之外的额外含义,看次数即可) |
数字怎么读
Max Pause 直接对 SLO(比如"P99 < 200 ms");Avg pause interval 异常小说明 GC 密集;Total pause 占 Total Time 的比例就是吞吐量的反面。
四、命令行:批量与 CI
不带参数(或只带日志)时启动 GUI,参数 ≥ 2 个就进入命令行模式:
bash
# 只导出报告(默认 -t SUMMARY)
java -jar gcviewer-1.37.jar gc.log summary.csv
# 报告 + 堆图 PNG
java -jar gcviewer-1.37.jar gc.log summary.csv chart.png
# 指定导出类型
java -jar gcviewer-1.37.jar gc.log summary.csv chart.png -t CSV_TS
# 轮转日志:用分号串起来当一个连续日志
java -jar gcviewer-1.37.jar "gc.log.0;gc.log.1;gc.log.2;gc.log.current" summary.csv参数含义:
| 位置 | 参数 | 必填 | 说明 |
|---|---|---|---|
| 1 | <gc-log-file|url> | 是 | 单个日志,或分号分隔的轮转日志序列,也可以是一个 URL |
| 2 | <export.csv> | 是 | 报告输出路径 |
| 3 | [chart.png] | 否 | 同时渲染一张堆图 |
| — | -t <类型> | 否 | 导出类型,默认 SUMMARY |
导出类型怎么选:
| 类型 | 输出 | 适合干什么 |
|---|---|---|
SUMMARY | 完整摘要(= GUI 数据面板的全部字段) | 人工归档、发邮件 |
CSV | 逗号分隔,聚合后的数据 | 丢进 Excel |
CSV_TS | 每个 GC 事件一行,带 unix 时间戳 | 入库、画趋势、前后两次调优做 diff |
PLAIN | 纯文本(Sun/Oracle 日志兼容 HPjmeter) | 喂给 HPjmeter |
SIMPLE | <事件名> <日志开始后秒数> <停顿秒数> | 喂给 gchisto |
命令行的退出码(来自源码常量),CI 里可以直接判断:
| 退出码 | 含义 |
|---|---|
0 | 成功 |
-1 | 导出失败 |
-2 | 参数解析失败(会打印 usage) |
-3 | 位置参数超过 3 个(会打印 usage) |
批量跑一整夜的日志:
bash
for f in /var/log/app/gc.log.*; do
java -jar gcviewer-1.37.jar "$f" "${f}.csv" "${f}.png" -t CSV_TS || echo "FAILED: $f"
doneshell 转义
分号是日志序列的分隔符,在 bash 里必须加引号,否则 shell 会把它当成命令结束。
五、性能优化实战:四类问题的定位路径
拿到一份日志,按下面四条路径走,比"把所有数字看一遍"高效得多。
1. 吞吐量低(Throughput < 95%)
看 Summary 的 Throughput 与 Accumulated pauses → 翻 Pause 页,看 Number of gc pauses 和 Avg pause interval。
- 次数多、间隔小、单次不长 → 年轻代太小,对象刚分配就被回收:调大
-Xmn或降低-XX:NewRatio; - 单次就很长 → 属于第 3 类问题。
复测目标:Throughput 上升、Avg pause interval 变大(GC 变稀)。
2. Full GC 频繁 / 老年代撑满
看图表里 Full GC Lines(黑竖线) 的密度 → Memory 页对比 Max heap after full GC 与 Footprint:
- 两者接近 → 容量不足:调大
-Xmx,并审视缓存与数据加载量; - 两者差很远却频繁 Full GC → 多半是
System.gc()或元空间触发,去Event details看具体原因; Used Heap蓝线谷底逐级抬升、Summary 里 Slope GC 为正 → 内存泄漏:GCViewer 到此为止,转jmap+ MAT 找对象(见JVM 排查工具箱)。
3. 单次停顿超过 SLO
看 Pause 页的 Max Pause → 回图表看 GC Times Rectangles 的颜色,定位长停顿的类型:
- 黑色(Full GC)→ 回到第 2 类;
- remark / initial-mark(CMS)→ 并发周期启动太晚,调低
-XX:CMSInitiatingOccupancyFraction,或换 G1; - G1 且停顿分布长尾 → 设
-XX:MaxGCPauseMillis,检查是否有巨型对象(调大-XX:G1HeapRegionSize); - 对延迟极敏感 → 上 ZGC / Shenandoah(1.37 对这两者的解析已增强)。
4. 晋升压力:老年代悄悄变胖
看 Memory 页的 Avg / Total promotion 和 Max tenured after conc GC:
- 晋升量高 + 老年代持续上涨 → 年轻代太小导致对象"早晋升",或 Survivor 装不下:调大年轻代、调
-XX:SurvivorRatio、提高-XX:MaxTenuringThreshold; - 晋升量大但老年代稳 → 正常,观察即可。
5. 一个容易误判的情况
Event details 里 VM operations overhead 明显高于 GC pauses 时,问题根本不在 GC——是安全点上的其他操作(偏向锁撤销、线程转储、类重定义)。这时候调堆参数是白费力气。
六、GCViewer 与 EasyGC 怎么选
| 维度 | GCViewer | EasyGC |
|---|---|---|
| 形态 | 桌面 jar,需 Java 8+ | 网页,打开即用 |
| 日志去向 | 本地 | 本地(浏览器内解析,不上传) |
| 图表 | 堆/分代/停顿矩形,可滚轮缩放、图层开关 | 使用率锯齿、停顿分布、触发原因环形图 |
| 事件明细 | 逐条 GC 事件 + Parser 警告 | 完整事件明细表 |
| 泄漏检测 | 看 Slope / 曲线谷底(人工判断) | 线性回归自动判定并给出结论 |
| 调优建议 | 无 | 15 条规则、四级严重度 |
| 批量 / CI | 命令行,可脚本化 | 手动上传 |
| 结构化导出 | CSV / CSV_TS / PLAIN / SIMPLE / PNG | PDF / Word / JSON |
| 部署到内网 | 拷 jar 即可 | 静态站点,也可内网部署 |
推荐组合:先用 EasyGC 快速看结论和建议 → 需要内网、批量或原始事件时用 GCViewer 复核。两者基于同一份日志,结论一致时你对判断的信心会高很多。
七、常见坑
- 找不到下载包:jar 在 SourceForge,不在 GitHub Release 附件里。
- 老版本解析统一日志失败:多余的 decoration 在 1.36 及更早会报错,升到 1.37 或精简 decoration。
- CMS / G1 并发事件与 STW 混在一起输出:官方 README 明确说解析器有时无法恢复并报错——这是已知限制,换一份日志或改用
-Xlog:gc*的完整输出。 - 大日志 OOM:GCViewer 本身也是个 Java 程序,几百 MB 的日志记得
java -Xmx2g -jar gcviewer.jar。 - 轮转日志:命令行用分号连(记得加引号),GUI 用
File → Open Series。 - 指标缺一块:没有
PrintGCDetails就没有晋升量和分代图;没有PrintGCApplicationStoppedTime就没有安全点开销。 - 别忽略 Parser 警告:有未解析行时,所有统计都是"部分数据"的结论。
相关文章
- JVM 性能优化指南 — 改哪个参数,以及验证闭环
- 如何解读分析报告 — 用 EasyGC 交叉验证 GCViewer 的结论
- 开启 GC 日志 — 一切分析的基础
- JVM 排查工具箱 — GC 之外的问题去哪儿查