Skip to content

GC 日志与问题定位 ​

学习目标 ​

  • 用统一 GC 日志识别 Young GC、Full GC、长停顿和 safepoint 问题。
  • 区分分配过快、老年代晋升、缓存泄漏、大对象和堆过小。
  • 建立“现象—证据—改动—结果”的可回滚调优闭环。

核心知识点 ​

专业术语 ​

  • 分配速率(allocation rate):单位时间内新对象分配的字节量。
  • 存活集(live set):一次回收后仍由根可达的对象集合。
  • 晋升压力(promotion pressure):对象存活时间变长,进入老年代或长期区域的压力。
  • Safepoint:所有 Java 线程到达可安全检查/暂停的位置。
  • GC 日志:记录触发原因、区域变化、停顿、并发阶段和收集器决策的证据。

白话解释与边界 ​

日志先回答“多久发生一次、停多久、回收前后剩多少、为什么触发”。频繁 Young GC 可能是分配速率高,Full GC 可能是老年代逼近上限、显式 GC、元空间压力或收集器策略;单看次数不能定性。

定位时保持基线:应用版本、JDK、收集器、Xms/Xmx、CPU/容器限制、请求量和代码版本都要记录。一次只改一个参数,并在同样负载下比较 p95/p99 停顿、吞吐、RSS、分配和存活量。

常用用法 ​

统一 GC 日志:记录时间、标签和滚动文件 ​

gc* 记录 GC 相关标签,safepoint 帮助区分“GC 停顿”和“到达安全点耗时”。

java
// 说明:统一 GC 日志:记录时间、标签和滚动文件。
public class UnifiedGcLogDemo {
    public static void main(String[] args) {
        System.out.println("-Xlog:gc*,safepoint:file=gc.log:time,uptime,level,tags:filecount=5,filesize=20M");
// 输出:-Xlog:gc*,safepoint:file=gc.log:time,uptime,level,tags:filecount=5,filesize=20M
    }
}

滚动文件避免无限增长,日志目录要有容量和权限控制。

-Xlog:gc+heap=debug:观察区域变化 ​

增加 heap 标签可以看到回收前后区域占用和对象晋升线索;日志等级越详细,文件和分析成本越高,应在短时间诊断窗口开启。

java
// 说明:-Xlog:gc+heap=debug:观察区域变化。
public class GcHeapLogDemo {
    public static void main(String[] args) {
        System.out.println("-Xlog:gc+heap=debug");
// 输出:-Xlog:gc+heap=debug
    }
}

Young GC:看分配速率和释放量 ​

如果 Young GC 很频繁但每次释放很多短命对象,优先找分配热点;如果释放后仍接近上限,检查长生命周期缓存、队列积压和晋升。

java
// 说明:Young GC:看分配速率和释放量。
public class YoungGcReasonDemo {
    public static void main(String[] args) {
        System.out.println("young GC: compare before/after occupancy and allocation rate");
// 输出:young GC: compare before/after occupancy and allocation rate
    }
}

Full GC:看触发原因与回收后存活量 ​

检查 Full GC 用于判断回收后存活集是否持续上升,以及频繁触发是否来自堆过小或显式 GC。

java
// 说明:Full GC:看触发原因与回收后存活量。
public class FullGcReasonDemo {
    public static void main(String[] args) {
        System.out.println("full GC: inspect cause, old occupancy, and post-GC live set");
// 输出:full GC: inspect cause, old occupancy, and post-GC live set
    }
}

必须结合日志的 cause 字段和业务负载。

jstat -gcutil:快速确认趋势 ​

它能快速看 Eden、Survivor、老年代和 GC 次数趋势;采样间隔、收集器和 JDK 版本会影响列含义,详细结论以目标 JVM 文档和 GC 日志为准。

java
// 说明:jstat -gcutil:快速确认趋势。
public class GcUtilCommandDemo {
    public static void main(String[] args) {
        System.out.println("jstat -gcutil <pid> 1000 10");
// 输出:jstat -gcutil <pid> 1000 10
    }
}

jcmd GC.heap_info:确认收集器和堆概要 ​

先确认实际收集器、堆使用和区域配置,再解释日志。

java
// 说明:jcmd GC.heap_info:确认收集器和堆概要。
public class GcHeapInfoCommandDemo {
    public static void main(String[] args) {
        System.out.println("jcmd <pid> GC.heap_info");
// 输出:jcmd <pid> GC.heap_info
    }
}

不要把启动参数中的预期收集器当成运行时事实。

jcmd GC.class_histogram:找数量异常的类型 ​

直方图适合初筛对象数量变化;连续两份在同负载下对比比单次排序更有意义,必要时再生成 hprof 看引用链。

java
// 说明:jcmd GC.class_histogram:找数量异常的类型。
public class GcClassHistogramDemo {
    public static void main(String[] args) {
        System.out.println("jcmd <pid> GC.class_histogram");
// 输出:jcmd <pid> GC.class_histogram
    }
}

OOM 堆转储:保留事后证据 ​

OOM 后保留堆转储用于定位“谁持有这些对象”,但文件可能很大且包含敏感数据。

java
// 说明:OOM 堆转储:保留事后证据。
public class OomDumpOptionDemo {
    public static void main(String[] args) {
        System.out.println("-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./dumps");
// 输出:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./dumps
    }
}

确保磁盘、权限、加密和保留周期,不要在事故中临时寻找存储位置。

关联运行时事件:在一条时间线上检查分配、GC、线程和 CPU ​

JFR 能把 GC 停顿与 CPU、锁竞争、对象分配和线程状态对齐;记录时长和事件阈值要受控,不要把一份 JFR 当成无需基线的自动答案。

java
// 说明:关联运行时事件:在一条时间线上检查分配、GC、线程和 CPU。
public class GcJfrCommandDemo {
    public static void main(String[] args) {
        System.out.println("jcmd <pid> JFR.start name=gc settings=profile duration=60s filename=gc.jfr");
// 输出:jcmd <pid> JFR.start name=gc settings=profile duration=60s filename=gc.jfr
    }
}

不常用但需要知道 ​

Humongous allocation:大对象路径 ​

G1 下大数组或大字符串可能占用 Humongous Region,回收和碎片行为与普通小对象不同。优先检查批量序列化、图片、缓存和一次性缓冲区的大小与生命周期。

java
// 说明:这行提示要在 G1 日志中关联 Humongous region 数量与大数组分配事件,而不是仅凭一次 GC 停顿判断。
public class HumongousAllocationHintDemo {
    public static void main(String[] args) {
        System.out.println("inspect humongous regions and large arrays in G1 logs");
// 输出:inspect humongous regions and large arrays in G1 logs
    }
}

Safepoint 日志:区分 GC 以外的停顿 ​

应用停顿不一定来自 GC,类重定义、偏向锁处理(具体 JDK 行为可能变化)或其他 VM 操作也可能需要安全点。用 safepoint 日志和 JFR 对齐停顿时间,避免把所有长暂停都归因于 GC。

java
// 说明:把 -Xlog:safepoint=debug 加到目标 JVM 启动参数,日志会记录进入安全点的原因与耗时,便于对齐非 GC 停顿。
public class SafepointLogHintDemo {
    public static void main(String[] args) {
        System.out.println("-Xlog:safepoint=debug");
// 输出:-Xlog:safepoint=debug
    }
}

-XX:+PrintNMTStatistics:本地内存收尾统计 ​

NMT 关注的是 JVM 本地内存,不等于 GC 堆日志;开启会增加开销,且需要在启动时配置。容器 RSS 高但堆不高时,线程栈、元空间、直接内存和本地库都应纳入排查。

java
public class NmtStatisticsHintDemo {
    public static void main(String[] args) {
        System.out.println("-XX:NativeMemoryTracking=summary -XX:+PrintNMTStatistics");
// 输出:-XX:NativeMemoryTracking=summary -XX:+PrintNMTStatistics
    }
}

GC cause:不要只按收集器名称判断 ​

同一收集器在不同触发原因下路径可能不同;只有把 cause、时间、堆变化和业务 QPS 放在一起,才能判断参数、代码或下游压力哪一层需要修复。

java
// 说明:诊断时对每个 GC 事件同时记录 cause、回收前后占用量、pause 和 concurrent phase,才能区分显式 GC 与分配压力。
public class GcCauseHintDemo {
    public static void main(String[] args) {
        System.out.println("record cause, before/after occupancy, pause, and concurrent phase");
// 输出:record cause, before/after occupancy, pause, and concurrent phase
    }
}

System.gc:确认是否存在显式 GC 请求 ​

禁用显式 GC 可能掩盖第三方库的生命周期问题;先确认调用来源、停顿和业务需要,再决定是否使用 -XX:+DisableExplicitGC 等参数,并用同负载回归。

java
// 作用:通过 System.gc 确认是否存在显式 GC 请求。
public class ExplicitGcHintDemo {
    public static void main(String[] args) {
        System.out.println("search code and libraries for System.gc before adding DisableExplicitGC");
// 输出:search code and libraries for System.gc before adding DisableExplicitGC
    }
}

简单案例 ​

java
import java.util.ArrayList;
import java.util.List;

public class GcInvestigationChecklistDemo {
    public static void main(String[] args) {
        List<String> evidence = new ArrayList<>();
// 初始状态:evidence = new ArrayList<>()。
// 作用:List<String> evidence = new ArrayList<>();;确认是否存在显式 GC 请求。
        evidence.add("symptom");
// 作用:evidence.add("symptom");;确认是否存在显式 GC 请求。
        evidence.add("gc log");
// 作用:evidence.add("gc log");;确认是否存在显式 GC 请求。
        evidence.add("heap histogram");
// 作用:evidence.add("heap histogram");;确认是否存在显式 GC 请求。
        evidence.add("same-load result");
// 作用:evidence.add("same-load result");;确认是否存在显式 GC 请求。
        System.out.println(evidence);
// 输出:[symptom, gc log, heap histogram, same-load result]
    }
}

任何 GC 调优结论都应能回到“现象—证据—一个改动—同负载结果”;否则只是参数猜测。

易混点 ​

  • Young GC 频繁不一定是泄漏,Full GC 多也不一定是泄漏;看回收后存活集和引用链。
  • GC 停顿和 safepoint 停顿可能不是同一件事,要分别采集。
  • 堆占用低但 RSS 高时,排查线程栈、元空间、直接内存和 native 库。
  • G1 大对象、容器缓存和序列化缓冲可能产生 Humongous 压力,单纯调大堆不一定有效。
  • 调参必须同版本、同负载、同指标比较,不能只凭一条日志或一次压测结论。

课后小问 ​

  1. Full GC 后老年代占用仍持续上升说明什么? 答案:更像长生命周期引用增长、缓存泄漏或队列积压,但仍需引用链确认。 解析:如果回收后存活量不高,优先检查堆大小、触发原因、显式 GC 和分配速率。

  2. 为什么要把 GC 日志和业务 QPS 对齐? 答案:GC 频率、分配速率和停顿往往随负载变化,孤立日志无法判断是否由流量突增导致。 解析:按相同时间窗对齐请求量、错误率、延迟、分配和停顿,才能比较改动前后。

  3. 什么时候需要堆转储而不是继续看日志? 答案:当需要知道哪些对象被谁引用、缓存为何不能回收时。 解析:日志能说明时间和容量趋势,堆转储和分析工具才能给出对象引用链;采集前要评估停顿和敏感数据。

本节小结 ​

  • GC 日志首先看触发原因、频率、停顿和回收后存活量。
  • 分配过快、晋升压力、缓存泄漏、大对象和非堆内存会呈现不同证据。
  • jstat/jcmd 适合快速采样,GC 日志/JFR/堆转储用于更深分析。
  • 以基线和同负载回归为调优闭环,一次只改一个可回滚变量。

快速回顾 ​

  • 会用 -Xlog:gc* 和 safepoint 日志建立证据。
  • 能区分 Young GC、Full GC、泄漏、分配过快和大对象压力。
  • 会按需要选择 jstat、jcmd、直方图、堆转储或 JFR。
  • 能写出“现象—证据—改动—结果”的问题定位记录。