JVM-垃圾收集器


前言

Java 是一门不需要开发人员关注对象销毁、内存回收的开发语言。这主要得益于 JVM 中的垃圾收集器,它们会在必要的时候,自动对可不用的对象进行垃圾回收,释放内存,保证程序的持续运行。Java 发布至今,有许多各式各样的垃圾收集器,它们各有优劣,满足于不同应用场景。

测试程序

​ 提供的测试程序,通过设置不同的虚拟机参数,以观察回收日志输出情况。

新生代

1
2
3
4
5
6
7
8
9
10
11
12
public class MinorGC {
    
    public byte[] _1MB = new byte[1024 * 1024];

    public static void main(String[] args) throws InterruptedException {
        while(true) {
            new MinorGC();
            System.out.println("产生 1MB 垃圾...");
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

老年代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MajorGC {
    
    public byte[] _1MB = new byte[1024 * 1024];
    
    public static void main(String[] args) throws InterruptedException {
        MajorGC[] majorGCs = new MajorGC[6];
        int index = 0;
        while(true) {
            majorGCs[index++ % majorGCs.length] = new MajorGC();
            System.out.println("消耗 1MB 资源");
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

新生代

Serial

Serial 收集器是所有垃圾收集器中最古老的的一种。作用于新生代,采用复制算法、单线程、独占的方式进行回收。回收过程中,会暂停所有应用线程(Stop The World)。在 CPU 环境受限(单 CPU)的环境下,有不错的性能,甚至能超过并行/并发收集器。至今仍是虚拟机 Client 模式下的默认垃圾收集器。

​ 启用参数及程序启动 Serial 的工作输出:

  • -XX:+UseSerialGC:使用 SerialSerial Old 分别回收新生代、老年代
1
2
3
4
5
6
7
8
9
# 使用虚拟机参数运行: -XX:+PrintGCDetails -XX:+UseSerialGC -Xmx10m
[GC (Allocation Failure) [DefNew: 2033K->320K(3072K), 0.0012017 secs] 2033K->722K(9920K), 0.0012257 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
产生 1MB 垃圾...
[GC (Allocation Failure) [DefNew: 2308K->320K(3072K), 0.0018155 secs] 2711K->1056K(9920K), 0.0018468 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
产生 1MB 垃圾...
产生 1MB 垃圾...
[GC (Allocation Failure) [DefNew: 2410K->0K(3072K), 0.0008429 secs] 3147K->1056K(9920K), 0.0008700 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
产生 1MB 垃圾...
产生 1MB 垃圾...

ParNew

ParNew 相当于是 Serial 的并行版本,除了使用多线程方式回收垃圾之外,其它都跟 Serial 相同。作用于新生代,采用复制算法,垃圾回收时会暂停应用线程。

​ 启用参数及程序启动 ParNew 的工作输出:

  • -XX:+UseParNewGC:使用 ParNewSerial Old 分别回收新生代、老年代
  • -XX:+UseConcMarkSweepGC:使用 ParNewCMS 分别回收新生代、老年代
1
2
3
4
5
6
7
8
9
# 使用虚拟机参数运行: -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC -Xmx10m
[GC (Allocation Failure) [ParNew: 2055K->320K(3072K), 0.0017130 secs] 2055K->750K(9920K), 0.0017508 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
产生 1MB 垃圾...
[GC (Allocation Failure) [ParNew: 2308K->320K(3072K), 0.0017421 secs] 2738K->1119K(9920K), 0.0017836 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
产生 1MB 垃圾...
产生 1MB 垃圾...
[GC (Allocation Failure) [ParNew: 2410K->28K(3072K), 0.0013704 secs] 3209K->1113K(9920K), 0.0013973 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
产生 1MB 垃圾...
产生 1MB 垃圾...

Parallel Scavenge

Parallel Scavenge 是新生代收集器,采用复制算法,并行方式执行垃圾回收。被称为“吞吐量优先垃圾收集器”,可以控制吞吐量大小(吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 运行垃圾收集时间)[1])。

Parallel Scavenge 收集器有两个可以控制吞吐量的参数和一个自动调节分代大小策略的参数:

  • -XX:MaxGCPauseMills

    用户线程暂停(Stop The World)时间阈值,如果指定了该参数,虚拟机将尽量在设定的时间范围内完成垃圾回收。

  • -XX:GCTimeRatio

    值域(0, 100),垃圾回收时间跟 JVM 运行总时间占比,也可以理解为 GC 执行频率,公式 1/(1+N)。默认值 99,即有 1/(1+99) 的时间用于垃圾回收。

  • -XX:+UseAdaptiveSizePolicy

    开启自动调节策略后,-Xmn(新生代大小),EdenSurvivor 的比例(-XX:SurvivorRatio),晋升老年代对象大小(-XX:PretenureSizeThreshold)等参数不需要设置,将由虚拟机动态调节以获得最小延迟或最大吞吐。

​ 启用参数及程序启动 Parallel Scavenge 的工作输出:

  • -XX:+UseParallelGC:使用 Parallel ScavengeParallel Old 分别回收新生代、老年代
  • -XX:+UseParallelOldGC:使用 Parallel ScavengeParallel Old 分别回收新生代、老年代
  • -XX:-UseParallelOldGC:使用 Parallel ScavengeSerial Old 分别回收新生代、老年代
1
2
3
4
5
6
7
8
9
10
11
# 使用虚拟机参数运行: -XX:+PrintGCDetails -XX:+UseParallelGC -Xmx10m
产生 1MB 垃圾...
[GC (Allocation Failure) [PSYoungGen: 2048K->480K(2560K)] 3072K->1824K(9728K), 0.0011552 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
产生 1MB 垃圾...
产生 1MB 垃圾...
产生 1MB 垃圾...
产生 1MB 垃圾...
产生 1MB 垃圾...
产生 1MB 垃圾...
[GC (Allocation Failure) [PSYoungGen: 2435K->496K(2560K)] 8899K->7284K(9728K), 0.0014250 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 496K->0K(2560K)] [ParOldGen: 6788K->1055K(5632K)] 7284K->1055K(8192K), [Metaspace: 3957K->3957K(1056768K)], 0.0062091 secs] [Times: user=0.16 sys=0.00, real=0.01 secs]

老年代

Serial Old

Serial Old 以串行的方式,采用标记-整理算法,对老年代进行垃圾回收。老年代垃圾回收通常比新生代花费更多时间。因此,在堆内存较大的应用中,Serial Old 启动会导致程序停顿更长时间。

​ 启用参数及程序启动 Serial Old 的工作输出:

  • -XX:+UseSerialGC:使用 SerialSerial Old 分别回收新生代、老年代
  • -XX:+UseParNewGC:使用 ParNewSerial Old 分别回收新生代、老年代
  • -XX:+UseParallelGC:使用 Parallel ScavengeSerial Old 分别回收新生代、老年代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 使用虚拟机参数运行: -XX:+PrintGCDetails -XX:+UseSerialGC -Xmx10m
[GC (Allocation Failure) [DefNew: 2033K->320K(3072K), 0.0013412 secs] 2033K->697K(9920K), 0.0013746 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
消耗 1MB 资源
[GC (Allocation Failure) [DefNew: 2355K->320K(3072K), 0.0047058 secs] 2732K->2081K(9920K), 0.0047585 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
消耗 1MB 资源
消耗 1MB 资源
[GC (Allocation Failure) [DefNew: 2410K->0K(3072K), 0.0021078 secs] 4172K->4129K(9920K), 0.0021364 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
消耗 1MB 资源
消耗 1MB 资源
[GC (Allocation Failure) [DefNew: 2085K->0K(3072K), 0.0013578 secs] 6215K->6177K(9920K), 0.0013868 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
消耗 1MB 资源
消耗 1MB 资源
# 这次的新生代回收,导致老年代中有7个对象,占了 7MB
[GC (Allocation Failure) [DefNew: 2091K->2091K(3072K), 0.0000388 secs][Tenured: 6177K->6177K(6848K), 0.0101472 secs] 8269K->7201K(9920K), [Metaspace: 3958K->3958K(1056768K)], 0.0103054 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
消耗 1MB 资源
# 新生代再次回收,由于老年代内存不足,触发 Full GC
[Full GC (Allocation Failure) [Tenured: 6177K->6176K(6848K), 0.0044253 secs] 8225K->7200K(9920K), [Metaspace: 3958K->3958K(1056768K)], 0.0044737 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

Parallel Old

Parallel Old 使用标记-整理算法,是一个多线程并发的收集器。是 Parallel Scavenge 的老年代版本,同样关注吞吐。-XX:ParallelGCThreads 参数可以控制垃圾回收时的线程数。

启用参数列表及程序启动 Parallel Old 的工作输出:

  • -XX:+UseParallelOldGC:使用 Parallel ScavengeParallel Old 分别回收新生代、老年代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 使用虚拟机参数运行: -XX:+PrintGCDetails -XX:+UseParallelOldGC -Xmx10m
消耗 1MB 资源
[GC (Allocation Failure) [PSYoungGen: 2048K->480K(2560K)] 3072K->1800K(9728K), 0.0010063 secs] [Times: user=0.16 sys=0.00, real=0.00 secs] 
消耗 1MB 资源
消耗 1MB 资源
消耗 1MB 资源
消耗 1MB 资源
消耗 1MB 资源
消耗 1MB 资源
[GC (Allocation Failure) --[PSYoungGen: 2435K->2435K(2560K)] 8875K->9203K(9728K), 0.0019634 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2435K->0K(2560K)] [ParOldGen: 6768K->6693K(7168K)] 9203K->6693K(9728K), [Metaspace: 3954K->3954K(1056768K)], 0.0114621 secs] [Times: user=0.16 sys=0.00, real=0.01 secs] 
消耗 1MB 资源
[Full GC (Ergonomics) [PSYoungGen: 1024K->0K(2560K)] [ParOldGen: 6693K->7157K(7168K)] 7717K->7157K(9728K), [Metaspace: 3954K->3954K(1056768K)], 0.0213025 secs] [Times: user=0.16 sys=0.00, real=0.02 secs] 
消耗 1MB 资源

CMS

CMS(Concurrent Mark Sweep)是一个尽可能低停顿时间的老年代收集器。垃圾回收分为 4 个主要阶段:

  • 初始标记(Initial Mark)

    标记 GC Roots 直接关联的对象,暂停应用线程,但速度快。

  • 并发标记(Concurrent Mark)

    从标记 GC Roots 直接关联的对象开始标记不可用对象,花费较长时间,但可并发执行。

  • 重新标记(Remark)

    由于并发标记时,应用线程执行产生标记变动,在这个阶段中可以修正。暂停应用线程,比初始标记花费时间长,但相较并发标记,所需的时间短很多。

  • 并发清除(Concurrent Sweep)

    清除标记的对象,可并发执行。

CMS 的主要设置参数:

  • -XX:ConcGCThreads / -XX:ParallelCMSThreads

    CMS 默认启动的回收线程数 (处理器核心数 + 3)/4 ,对于处理器核心数少于 4 个的环境,垃圾回收所占用的处理资源占比较大,容易导致应用程序变慢,降低吞吐。-XX:ConcGCThreads / -XX:ParallelCMSThreads 可以控制 GC 线程数。

  • -XX:CMSInitiatingOccupancyFraction

    CMS 无法处理浮动垃圾,可能出现 Concurrent Mode Failure 进而触发一次 Full GC。在并发清除阶段,应用线程可能会再产生垃圾对象,而这一部分对象暂未标记,CMS 暂时无法清理,只能等待下一次垃圾回收。也正是由于应用线程仍然进行中,所以必须预留一部分内存供并发收集时的程序使用。-XX:CMSInitiatingOccupancyFractionJDK 5 默认 68, JDK 6 默认 92)指当老年代空间使用率达到阈值时,会执行一次 CMS 回收。这个值设定的太低,就会高频率的 CMS 回收;设置过高,则容易导致内存不足,出现 Concurrent Mode Failure 进而临时启用 Serial Old 收集器重新回收老年代。

  • -XX:UseCMSCompactAtFullCollection

    CMS 采用标记-清除方法,垃圾回收才产生空间碎片。分配大对象时,如果在空闲记录表无法找到连续内存足够大的空间,则会提前触发 Full GC-XX:UseCMSCompactAtFullCollection 表示在一次 CMS 回收后,进行碎片整理,碎片清理不是并发执行的。

  • -XX:CMSFullGCsBeforeCompaction

    由于 -XX:UseCMSCompactAtFullCollection 的碎片清理会暂停应用线程 (Stop The World),导致延迟变大,影响用户交互。可指定参数 -XX:CMSFullGCsBeforeCompaction,参数表示进行多少次 CMS 回收后,才进行一次碎片整理,可以减少碎片整理次数。

​ 启用参数列表及程序启动 Parallel Old 的工作输出:

  • -XX:+UseConcMarkSweepGC:使用 ParNewCMS 分别回收新生代、老年代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  # 使用虚拟机参数运行: -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC -Xmx10m
[GC (Allocation Failure) [ParNew: 2055K->320K(3072K), 0.0019080 secs] 2055K->751K(9920K), 0.0019539 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
消耗 1MB 资源
[GC (Allocation Failure) [ParNew: 2308K->320K(3072K), 0.0013299 secs] 2739K->2143K(9920K), 0.0013496 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
消耗 1MB 资源
消耗 1MB 资源
[GC (Allocation Failure) [ParNew: 2410K->56K(3072K), 0.0016667 secs] 4233K->4213K(9920K), 0.0016913 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
消耗 1MB 资源[GC (CMS Initial Mark) [1 CMS-initial-mark: 4156K(6848K)] 5238K(9920K), 0.0003791 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (CMS Final Remark) [YG occupancy: 1081 K (3072 K)][Rescan (parallel) , 0.0005615 secs][weak refs processing, 0.0000136 secs][class unloading, 0.0002156 secs][scrub symbol table, 0.0004013 secs][scrub string table, 0.0001137 secs][1 CMS-remark: 4156K(6848K)] 5238K(9920K), 0.0013543 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

G1

G1(Garbage First) 是一款主要面向服务端应用的垃圾收集器,开创了收集器面向局部收集的设计思路和基于 Region 的内存布局形式。

JDK 6 Update 14,处于”实验状态” (Experimental)。

JDK 7 Update 4,移除 Experimental 标志。

JDK 8 Update 40,提供并发的类卸载的支持,此后的 G1Oracle 官方称为 “全功能的垃圾收集器”。

JDK 9 发布之日,G1 宣布取代 Parallel ScavengeParallel Old 的组合,称为服务端模式下默认的垃圾收集器,而 CMS 被声明为不推荐使用 Deprecate

Region 是由连续的堆内存划分出来的多个大小相等的独立区域。根据需要,每个 Region 都可以扮演新生代的 Eden 空间、Survivor 空间、或老年代空间。收集器根据具体的角色,采用不同的策略去处理。Region 还有一类专门用来存储大对象(超过一个 Region 容量一般的对象)的特殊区域 Humongous

​ 而 G1 基于 Region 组成回收集(Collection Set,一般简称 CSet)进行垃圾回收,衡量标准不再是它属于哪个分代,而是哪块内存的垃圾数量最多,回收收益最大。G1 后台会维护一个优先级列表,记录每块 Region 的回收价值(回收所获得的空间以及回收所需时间的经验值) ,根据用户设定允许的收集停顿时间,优先处理回收价值收益最大的那些 Region。这就是 G1Mixed GC 模式。

G1 大致运作过程分为四个步骤:

  • 初始标记(Initial Marking)

    标记 GC Roots 直接关联的对象,修改 TAMS 指针的值,暂停应用线程,但速度快。

  • 并发标记(Concurrent Marking)

    从标记 GC Roots 直接关联的对象开始标记不可用对象,花费较长时间,但可并发执行。扫描完之后,还要重新处理 SATB 记录下,在并发时有引用变动的对象。

  • 最终标记(Final Marking)

    处理并发阶段结束后仍遗留下来的最后那少量的 SATB 记录,短暂暂停用户线程。

  • 帅选回收(Live Data Counting and Evacuation)

    更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户期望的停顿时间来制定回收计划,可以自由选择任意多个 Region 构成回收集,然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。这里的操作设计存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

G1 的主要设置参数:

  • -XX:G1HeapRegionSize

    每个 Region 的大小,范围 1MB ~ 32MB,且应为 2N 次幂。

  • -XX:MaxGCPauseMillis

    GC 停顿时间,默认 200 毫秒。

总结比较

收集器 面向 算法 运行方式 启用
Serial 新生代 复制 串行 -XX:+UseSerialGC
Serial Old 老年代 整理 串行 -XX:+UseSerialGC
-XX:+UseParNewGC
-XX:+UseParallelGC
ParNew 新生代 复制 并行 -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
Parallel Scavenge 新生代 复制 并行 -XX:+UseParallelGC
-XX:+UseParallelOldGC
Parallel Old 老年代 整理 并发 -XX:+UseParallelOldGC
CMS 老年代 清除 并发 -XX:+UseConcMarkSweepGC
G1 Region   并行于并发 -XX:+UseG1GC

参考

​ [1] 周志明. 深入理解Java虚拟机:JVM高级特性与最佳实践(第3版). 3.5 经典垃圾收集器 3.6 低延迟垃圾收集器

​ [2] 葛一鸣. 实战Java虚拟机. 5 垃圾收集器和内存分配

​ [3] 周明耀. 深入理解JVM & G1 GC. 2.3 Garbage Collection