Java JVM 垃圾回收器(一):经典垃圾回收器

17 阅读13分钟

经典垃圾回收器和现代垃圾回收器

CMS 及以后的垃圾回收器被称为“现代垃圾回收器”,CMS 之前的通常称为“传统垃圾回收器”或“经典垃圾回收器”。


为什么这样区分?

1. 并发与低停顿
  • 传统垃圾回收器(如 Serial GC、Parallel GC)在回收时会长时间 Stop-The-World(STW),所有应用线程暂停,影响响应速度。
  • 现代垃圾回收器(从 CMS 开始,如 CMS、G1、ZGC、Shenandoah)引入了并发标记、并发清理等机制,GC 线程和应用线程可以同时工作,大大降低了停顿时间,提升了应用的响应性。
2. 适应多核和大堆
  • 现代 GC 更好地支持多核 CPU 和大内存堆,能在大规模服务端场景下保持良好性能。
3. 更智能的算法
  • 现代 GC 引入了三色标记法、SATB、分区回收、预测停顿等更智能的算法和机制,能更好地平衡吞吐量与延迟。

总结

  • CMS 之前:Serial GC、Parallel GC,称为“传统”或“经典”垃圾回收器,特点是全停顿、简单高效但不适合低延迟场景。
  • CMS 及以后:CMS、G1、ZGC、Shenandoah,称为“现代垃圾回收器”,特点是并发、低停顿、适合大堆和多核环境。

本质区别
现代垃圾回收器的目标是降低应用停顿时间,提高响应性和可扩展性,而传统垃圾回收器更关注实现简单和吞吐量。

Serial GC(串行回收器)

Serial GC(串行回收器)是 Java 虚拟机中最简单的一种垃圾回收器。它的特点是所有垃圾回收操作都由单线程完成,在回收期间会暂停所有应用线程(Stop-The-World),直到回收结束。


一、适用场景

  • 适合单核 CPU、小内存、对延迟要求不高的应用(如嵌入式、客户端、测试环境)。
  • 在服务器端或多核环境下不推荐使用。

二、运行机制和原理

1. 新生代回收(Minor GC)

  • 采用复制算法(Copying)

    • 新生代(Young Generation)分为 Eden 区和两个 Survivor 区(S0、S1)。
    • 大部分对象在 Eden 区分配。
    • 当 Eden 区满时,触发 Minor GC。
    • 存活对象从 Eden 和 From Survivor 区复制到 To Survivor 区,年龄增加,达到阈值晋升到老年代。
    • 整个过程只有一个 GC 线程,所有应用线程暂停。

2. 老年代回收(Major/Full GC)

  • 采用标记-整理算法(Mark-Compact)

    • 首先标记所有存活对象。
    • 然后将存活对象向一端移动,清理无效空间,避免内存碎片。
    • 依然是单线程,所有应用线程暂停。

3. Stop-The-World

  • 无论 Minor GC 还是 Major/Full GC,Serial GC 都会暂停所有应用线程,直到回收完成。

三、优缺点

优点

  • 实现简单,稳定可靠。
  • 在单核、小堆环境下,GC 开销低,性能反而不错。

缺点

  • 单线程,无法利用多核 CPU。
  • GC 停顿时间长,应用不可用,影响用户体验。
  • 不适合高并发、大内存、对响应时间敏感的场景。

四、如何启用

  • 显式指定参数:-XX:+UseSerialGC
  • 在单核或小内存环境下,JVM 可能自动选择 Serial GC。

五、总结

  • Serial GC 是最基础的垃圾回收器,所有回收工作由单线程完成,回收期间应用完全暂停。
  • 适合资源有限、对延迟不敏感的场景。
  • 现代服务器端应用一般不推荐使用。

六、日志分析

下面是一个典型的 Serial GC(串行回收器)GC 日志样例(加上 -XX:+PrintGCDetails -XX:+PrintGCDateStamps 参数):

2024-06-10T15:30:12.123+0800: 0.123: [GC (Allocation Failure) 
    [DefNew: 6144K->512K(6144K), 0.0056789 secs] 
    6144K->1536K(19840K), 0.0057890 secs] 
[Times: user=0.01 sys=0.00, real=0.01 secs]

2024-06-10T15:30:13.456+0800: 1.456: [Full GC (Ergonomics) 
    [Tenured: 1024K->512K(13696K), 0.0101234 secs] 
    2560K->512K(19840K), [Metaspace: 3500K->3500K(1056768K)], 0.0102345 secs] 
[Times: user=0.01 sys=0.00, real=0.01 secs]

日志说明

  • [GC (Allocation Failure) ...]:Minor GC(新生代回收),原因是分配失败。
  • [DefNew: 6144K->512K(6144K), ...]:新生代(DefNew)从 6144K 回收到 512K,总容量 6144K。
  • 6144K->1536K(19840K):整个堆从 6144K 回收到 1536K,总容量 19840K。
  • [Full GC (Ergonomics) ...]:Full GC(包括老年代),原因是 JVM Ergonomics(自适应调整)。
  • [Tenured: 1024K->512K(13696K), ...]:老年代(Tenured)从 1024K 回收到 512K,总容量 13696K。
  • [Metaspace: 3500K->3500K(1056768K)]:元空间使用情况。
  • user/sys/real:GC 期间的用户态、内核态和实际耗时。

下面对 Serial GC 日志样例的每一部分做详细解释:


日志样例

2024-06-10T15:30:12.123+0800: 0.123: [GC (Allocation Failure) 
    [DefNew: 6144K->512K(6144K), 0.0056789 secs] 
    6144K->1536K(19840K), 0.0057890 secs] 
[Times: user=0.01 sys=0.00, real=0.01 secs]

1. 2024-06-10T15:30:12.123+0800: 0.123:
  • 2024-06-10T15:30:12.123+0800:GC 发生的日期和时间。
  • 0.123: :JVM 启动后的时间(秒),即本次 GC 发生在 JVM 启动后 0.123 秒。
2. [GC (Allocation Failure)
  • [GC:表示一次 Minor GC(新生代回收)。
  • (Allocation Failure) :GC 触发原因,这里是因为新生代空间不足,无法分配新对象。
3. [DefNew: 6144K->512K(6144K), 0.0056789 secs]
  • DefNew:新生代(Default New Generation)。
  • 6144K->512K(6144K) :GC 前新生代使用 6144K,GC 后剩 512K,总容量 6144K。
  • 0.0056789 secs:新生代回收耗时约 5.7 毫秒。
4. 6144K->1536K(19840K), 0.0057890 secs]
  • 6144K->1536K(19840K) :GC 前整个堆使用 6144K,GC 后剩 1536K,总容量 19840K。
  • 0.0057890 secs:整个 GC 过程耗时约 5.8 毫秒。
5. [Times: user=0.01 sys=0.00, real=0.01 secs]
  • user=0.01:用户态 CPU 时间(0.01 秒)。
  • sys=0.00:内核态 CPU 时间(0.00 秒)。
  • real=0.01 secs:GC 实际耗时(0.01 秒)。

Full GC 日志示例

2024-06-10T15:30:13.456+0800: 1.456: [Full GC (Ergonomics) 
    [Tenured: 1024K->512K(13696K), 0.0101234 secs] 
    2560K->512K(19840K), [Metaspace: 3500K->3500K(1056768K)], 0.0102345 secs] 
[Times: user=0.01 sys=0.00, real=0.01 secs]
1. [Full GC (Ergonomics)
  • Full GC:表示一次 Full GC(包括老年代和新生代)。
  • (Ergonomics) :GC 触发原因,这里是 JVM 自适应调整。
2. [Tenured: 1024K->512K(13696K), 0.0101234 secs]
  • Tenured:老年代。
  • 1024K->512K(13696K) :GC 前老年代使用 1024K,GC 后剩 512K,总容量 13696K。
  • 0.0101234 secs:老年代回收耗时约 10 毫秒。
3. 2560K->512K(19840K)
  • 2560K->512K(19840K) :GC 前整个堆使用 2560K,GC 后剩 512K,总容量 19840K。
4. [Metaspace: 3500K->3500K(1056768K)]
  • Metaspace:元空间(存放类元数据)。
  • 3500K->3500K(1056768K) :GC 前后元空间使用 3500K,总容量 1056768K(未回收)。
5. 0.0102345 secs
  • 整个 Full GC 过程耗时约 10 毫秒。

Parallel GC(并行回收器/吞吐量优先 GC)

一、什么是 Parallel GC?

Parallel GC(也叫吞吐量优先 GC、Throughput Collector)是一种多线程并行执行垃圾回收任务的回收器。
它的目标是最大化应用的吞吐量,即让应用程序花更多时间做“正事”,而不是做 GC。

  • JDK8 默认的垃圾回收器就是 Parallel GC。
  • 主要参数:-XX:+UseParallelGC(新生代),-XX:+UseParallelOldGC(老年代)。

二、运行机制和原理

1. 新生代回收(Minor GC)

  • 采用复制算法(Copying) ,与 Serial GC 类似,但回收过程由多个 GC 线程并行完成
  • 新生代分为 Eden 区和两个 Survivor 区(S0、S1)。
  • 当 Eden 区满时,触发 Minor GC。
  • 存活对象从 Eden 和 From Survivor 区复制到 To Survivor 区,年龄增加,达到阈值晋升到老年代。
  • 多个 GC 线程同时工作,大大加快回收速度。

2. 老年代回收(Major/Full GC)

  • 采用多线程标记-整理算法(Parallel Mark-Compact)
  • 标记所有存活对象,然后将其整理到一端,清理无效空间,避免内存碎片。
  • 老年代的回收同样是多线程并行完成(需开启 -XX:+UseParallelOldGC,JDK8 默认开启)。

3. Stop-The-World

  • 无论 Minor GC 还是 Major/Full GC,Parallel GC 都会暂停所有应用线程,直到回收完成。
  • 但由于回收过程是多线程并行,GC 停顿时间比 Serial GC 短很多,尤其在多核 CPU 上优势明显。

三、优缺点

优点

  • 高吞吐量:GC 线程并行,回收速度快,应用可用时间比例高。
  • 适合多核服务器:能充分利用多核 CPU 的计算能力。
  • 实现稳定,适合大多数通用场景

缺点

  • GC 停顿依然存在:所有回收过程都 Stop-The-World,不适合对延迟极敏感的应用。
  • 不支持并发回收:应用线程和 GC 线程不能同时运行。

四、常用参数

  • -XX:+UseParallelGC:启用并行新生代回收(默认 JDK8)。
  • -XX:+UseParallelOldGC:启用并行老年代回收(JDK8 默认)。
  • -XX:ParallelGCThreads=N:设置 GC 线程数(默认与 CPU 核心数相关)。
  • -XX:MaxGCPauseMillis=xxx:期望最大 GC 停顿时间(JVM 尽量满足,但不保证)。

五、适用场景

  • 多核服务器、对吞吐量要求高、对单次停顿时间要求不高的应用。
  • 批处理、数据分析、后台服务等。

六、总结

  • Parallel GC 是多线程并行执行的 Stop-The-World 回收器,追求高吞吐量。
  • 新生代和老年代都能并行回收,适合多核、高吞吐场景。
  • 不适合对延迟极敏感的应用(如金融、实时系统),这些场景建议用 G1、ZGC、Shenandoah 等低停顿 GC。

七、日志分析

下面是一个典型的 Parallel GC(吞吐量优先 GC)日志样例(加上 -XX:+PrintGCDetails -XX:+PrintGCDateStamps 参数):

2024-06-10T16:00:12.123+0800: 0.123: [GC (Allocation Failure) 
    [PSYoungGen: 6144K->512K(9216K)] 
    8192K->2048K(19456K), 0.0045678 secs] 
[Times: user=0.01 sys=0.00, real=0.01 secs]

2024-06-10T16:01:15.456+0800: 63.456: [Full GC (Ergonomics) 
    [PSYoungGen: 1024K->0K(9216K)] 
    [ParOldGen: 8192K->1024K(10240K)] 
    9216K->1024K(19456K), [Metaspace: 3500K->3500K(1056768K)], 0.0123456 secs] 
[Times: user=0.03 sys=0.00, real=0.01 secs]

日志说明

  • [GC (Allocation Failure) ...]:Minor GC(新生代回收),因分配失败触发。
  • [PSYoungGen: 6144K->512K(9216K)]:新生代(Parallel Scavenge Young Generation)GC 前后使用情况。
  • [ParOldGen: 8192K->1024K(10240K)]:老年代(Parallel Old Generation)GC 前后使用情况。
  • [Full GC (Ergonomics) ...]:Full GC(包括新生代和老年代),因 JVM 自适应调整触发。
  • [Metaspace: 3500K->3500K(1056768K)]:元空间使用情况。
  • user/sys/real:GC 期间的用户态、内核态和实际耗时。

下面详细解读上面 Parallel GC 日志样例的每一部分含义:


日志样例

2024-06-10T16:00:12.123+0800: 0.123: [GC (Allocation Failure) 
    [PSYoungGen: 6144K->512K(9216K)] 
    8192K->2048K(19456K), 0.0045678 secs] 
[Times: user=0.01 sys=0.00, real=0.01 secs]

2024-06-10T16:01:15.456+0800: 63.456: [Full GC (Ergonomics) 
    [PSYoungGen: 1024K->0K(9216K)] 
    [ParOldGen: 8192K->1024K(10240K)] 
    9216K->1024K(19456K), [Metaspace: 3500K->3500K(1056768K)], 0.0123456 secs] 
[Times: user=0.03 sys=0.00, real=0.01 secs]
第一条日志(Minor GC)
2024-06-10T16:00:12.123+0800: 0.123: [GC (Allocation Failure) 
    [PSYoungGen: 6144K->512K(9216K)] 
    8192K->2048K(19456K), 0.0045678 secs] 
[Times: user=0.01 sys=0.00, real=0.01 secs]
  • 2024-06-10T16:00:12.123+0800:GC 发生的日期和时间。

  • 0.123: :JVM 启动后 0.123 秒发生 GC。

  • [GC (Allocation Failure) :Minor GC,因新生代空间分配失败触发。

  • [PSYoungGen: 6144K->512K(9216K)]

    • 新生代(Parallel Scavenge Young Generation)GC 前使用 6144K,GC 后剩 512K,总容量 9216K。
  • 8192K->2048K(19456K)

    • 整个堆(新生代+老年代)GC 前使用 8192K,GC 后剩 2048K,总容量 19456K。
  • 0.0045678 secs:本次 GC 总耗时约 4.6 毫秒。

  • [Times: user=0.01 sys=0.00, real=0.01 secs]

    • user=0.01:用户态 CPU 时间 0.01 秒。
    • sys=0.00:内核态 CPU 时间 0.00 秒。
    • real=0.01 secs:GC 实际耗时 0.01 秒。

第二条日志(Full GC)
2024-06-10T16:01:15.456+0800: 63.456: [Full GC (Ergonomics) 
    [PSYoungGen: 1024K->0K(9216K)] 
    [ParOldGen: 8192K->1024K(10240K)] 
    9216K->1024K(19456K), [Metaspace: 3500K->3500K(1056768K)], 0.0123456 secs] 
[Times: user=0.03 sys=0.00, real=0.01 secs]
  • 2024-06-10T16:01:15.456+0800:GC 发生的日期和时间。

  • 63.456: :JVM 启动后 63.456 秒发生 GC。

  • [Full GC (Ergonomics) :Full GC,因 JVM 自适应调整触发。

  • [PSYoungGen: 1024K->0K(9216K)]

    • 新生代 GC 前使用 1024K,GC 后为 0K,总容量 9216K。
  • [ParOldGen: 8192K->1024K(10240K)]

    • 老年代(Parallel Old Generation)GC 前使用 8192K,GC 后为 1024K,总容量 10240K。
  • 9216K->1024K(19456K)

    • 整个堆 GC 前使用 9216K,GC 后为 1024K,总容量 19456K。
  • [Metaspace: 3500K->3500K(1056768K)]

    • 元空间 GC 前后都为 3500K,总容量 1056768K(未回收)。
  • 0.0123456 secs:本次 Full GC 总耗时约 12.3 毫秒。

  • [Times: user=0.03 sys=0.00, real=0.01 secs]

    • user=0.03:用户态 CPU 时间 0.03 秒。
    • sys=0.00:内核态 CPU 时间 0.00 秒。
    • real=0.01 secs:GC 实际耗时 0.01 秒。

总结

  • PSYoungGen:新生代(Parallel Scavenge)。
  • ParOldGen:老年代(Parallel Old)。
  • Metaspace:元空间(类元数据)。
  • user/sys/real:分别表示 GC 期间的用户态、内核态和实际耗时。
  • GC/Full GC:分别表示 Minor GC 和 Full GC。