G1收集器

在JDK9之后JVM默认使用G1(Garbage-First, 垃圾优先)收集器进行垃圾回收。G1是一款分代的 (generational),增量的 (incremental),并行的 (parallel),移动式(evacuating)的,软实时的垃圾回收器。其最大特点是用Region代替传统分代模型,分代成为逻辑上的概念;建立了可预测的停顿时间模型,让暂停时间可配置。从实践结果来看,它能及时有效的回收大对象,增加吞吐量,避免内存碎片,保障程序的长久运行。

一、内存模型对比

传统的垃圾收集器(串行、并行、CMS)都将堆结构分为三个部分:年轻代、老年代和固定内存大小的永久代

  • 传统收集器内存结构

G1中的堆被划分为N个Region,每个region内内存连续,其中一部分被分配为传统的角色(eden、survivor、old),但它们不是固定的位置和大小,这给内存分配提供了极大的灵活性。

  • G1内存结构

二、GC步骤

G1中有两种回收模式:

  • 年轻代垃圾回收(Young GC)
  • 混合垃圾回收(Mixed GC)

2.1 年轻代GC

年轻代GC是只选择年轻代区域(Eden/Survivor)进入回收集合(Collection Set,简称CSet)进行回收的模式。年轻代GC的过程和其他的分代回收器差不多,新创建的对象分配至Eden区域,然后将标记存活的对象移动至Survivor区,达到晋升年龄的就晋升到老年代区域,然后清空原区域(不过这里可没有年轻代复制算法中两个Survivor的交换过程)。

  1. G1在遵循GC暂停时间的基础上,选择最大年轻代Region数(CSSet)
  2. 根扫描(Root Scanning)
  3. RememberedSet扫描(RememberedSet简称RSet,会记录跨代引用的关系)
  4. 移动,遍历上面的标记栈,将栈内的所有所有的对象移动(复制)至Survivor区域

年轻代GC图示:

2.2 混合GC

混合GC会选择所有年轻代区域(Eden/Survivor,最大年轻代分区数)和部分老年代区域进去回收集合进行回收的模式。年轻代区域对象移动到Survivor区,老年代区域移动到老年代区域。

  1. 初始标记

    这个是捎带在年轻代GC完成的

  2. 并发标记

    并发标记的目的是标记存活对象,为移动过程做准备。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。

  3. 再次标记

    由于应用程序持续进行,需要修正上一次的标记结果。G1中采用了比CMS更快的初始快照算法:snapshot一at一the一beginning (SATB)。此时,所有区域的活性都被计算出来了。

  4. 复制清理阶段

    G1 选择“活跃度”最低的区域,即可以收集最快的区域。然后与年轻的GC同时收集这些区域

  5. 最终清理阶段

    所选择的区域都被复制,然后压缩

混合GC总结

阶段 是否STW 描述
初始标记 捎带在年轻代GC完成
根区域扫描 扫描幸存者区域以查找对老年代的引用
并行标记 查找整个堆上的活动对象
再次标记 遗漏的再标记一次
清理 对活动对象和完全自由区域执行记帐;擦洗RSet;重置空区域并将其返回到可用列表
复制 疏散或复制活动对象到未使用区域

三、GC日志

G1 GC的垃圾回收过程主要包括如下三个环节:
年轻代GC (Young GC)、老年代并发标记过程 (Concurrent Marking)、混合回收(Mixed GC)。并发标记是全局的,和回收过程是两个阶段。

年轻代GC日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
//[GC pause (G1 Evacuation Pause) (young) 代表完全年轻代回收
// 0.0182341 secs 是本次GC的暂停时间
0.184: [GC pause (G1 Evacuation Pause) (young), 0.0182341 secs 是本次GC的暂停时间]
// 并行GC线程,一共有8个
[Parallel Time: 16.7 ms, GC Workers: 8]
/*这一行信息说明的是这8个线程开始的时间,Min表示最早开始的线程时间,Avg表示平均开始时间,Max表示的是最晚开始时间,Diff为最早和最晚的时间差。这个值越大说明线程启动时间越不均衡。线程启动的时间依赖于GC进入安全点的情况。关于安全点可以参考后文的介绍。*/
[GC Worker Start (ms): 184.2 184.2 184.2 184.3 184.3 184.4 186.1 186.1
Min: 184.2, Avg: 184.7, Max: 186.1, Diff: 1.9]
/*根处理的时间,这个时间包含了所有强根的时间,分为Java根,分别为Thread、JNI、CLDG;和JVM根下面的StringTable、Universe、JNI Handles、ObjectSynchronizer、FlatProfiler、Management、SystemDictionary、JVMTI */
[Ext Root Scanning (ms): 0.3 0.2 0.2 0.1 0.1 0.0 0.0 0.0
Min: 0.0, Avg: 0.1, Max: 0.3, Diff: 0.3, Sum: 0.8]
/*Java线程处理时间,主要是线程栈。这个时间包含了根直接引用对象的复制时间,如果根超级大,这个时间可能会增加 */
[Thread Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[StringTable Roots (ms): 0.0 0.1 0.1 0.1 0.1 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.4]
[Universe Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[JNI Handles Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[ObjectSynchronizer Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[FlatProfiler Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Management Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[SystemDictionary Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[CLDG Roots (ms): 0.3 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.3, Diff: 0.3, Sum: 0.3]
[JVMTI Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
// CodeCache Roots实际上是在处理Rset的时候的统计值,它包含下面的
// UpdateRS,ScanRS和Code Root Scanning
[CodeCache Roots (ms): 5.0 3.9 2.2 3.3 2.1 2.2 0.6 2.2
Min: 0.6, Avg: 2.7, Max: 5.0, Diff: 4.4, Sum: 21.6]
[CM RefProcessor Roots (ms): 0.0
0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Wait For Strong CLD (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Weak CLD Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[SATB Filtering (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
// 这个就是GC线程更新RSet的时间花费,注意这里的时间和我们在Refine里面处理RSet
// 的时间没有关系,因为它们是不同的线程处理
[Update RS (ms): 5.0 3.9 2.2 3.3 2.1 2.2 0.6 2.2
Min: 0.6, Avg: 2.7, Max: 5.0, Diff: 4.4, Sum: 21.5]
// 这里就是GC线程处理的白区中的dcq个数
[Processed Buffers: 8 8 7 8 8 7 2 4
Min: 2, Avg: 6.5, Max: 8, Diff: 6, Sum: 52]
// 扫描RSet找到被引用的对象
[Scan RS (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): 0.0 0.0 0.0 0.0 0.0 0.1 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1]
// 这个就是所有活着的对象(除了强根直接引用的对象,在Java根处理时会直接复制)复制
// 到新的分区花费的时间。从这里也可以看出复制基本上是最花费时间的操作。
[Object Copy (ms): 11.3 12.5 14.2 13.1 14.3 14.2 14.2 12.5
Min: 11.3, Avg: 13.3, Max: 14.3, Diff: 3.0, Sum: 106.3]
// GC线程结束的时间信息。
[Termination (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: 1 1 1 1 1 1 1 1
Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 8]
// 这个是并行处理时其他处理所花费的时间,通常是由于JVM析构释放资源等
[GC Worker Other (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
// 并行GC花费的总体时间
[GC Worker Total (ms): 16.6 16.6 16.6 16.5 16.5 16.4 14.7 14.7
Min: 14.7, Avg: 16.1, Max: 16.6, Diff: 1.9, Sum: 128.7]
// GC线程结束的时间信息
[GC Worker End (ms): 200.8 200.8 200.8 200.8 200.8 200.8 200.8 200.8
Min: 200.8, Avg: 200.8, Max: 200.8, Diff: 0.0]
// 下面是其他任务部分。
// 代码扫描属于并行执行部分,包含了代码的调整和回收时间
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
// 清除卡表的时间
[Clear CT: 0.1 ms]
[Other: 1.5 ms]
// 选择CSet的时间,YGC通常是0
[Choose CSet: 0.0 ms]
// 引用处理的时间,这个时间是发现哪些引用对象可以清除,这个是可以并行处理的
[Ref Proc: 1.1 ms]
// 引用重新激活
[Ref Enq: 0.2 ms]
// 重构RSet花费的时间
[Redirty Cards: 0.1 ms]
[Parallel Redirty: 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[Redirtied Cards: 8118 7583 6892 4496 0 0 0 0
Min: 0, Avg: 3386.1, Max: 8118, Diff: 8118, Sum: 27089]
// 这个信息是是可以并行处理的,这里是线程重构RSet的数目
// 大对象处理时间
[Humongous Register: 0.0 ms]
[Humongous Total: 2]
// 这里说明有2个大对象
[Humongous Candidate: 0]
// 可回收的大对象0个
// 如果有大对象要回收,回收花费的时间,回收的个数
[Humongous Reclaim: 0.0 ms]
[Humongous Reclaimed: 0]
// 释放CSet中的分区花费的时间,有新生代的信息和老生代的信息。
[Free CSet: 0.0 ms]
[Young Free CSet: 0.0 ms]
[Non-Young Free CSet: 0.0 ms]
// GC结束后Eden从15M变成0,下一次使用的空间为21M,S从2M变成3M,整个堆从
// 23.7M变成20M
[Eden: 15.0M(15.0M)->0.0B(21.0M) Survivors: 2048.0K->3072.0K
Heap: 23.7M(256.0M)->20.0M(256.0M)]

并发标记日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//并发标记 - 初始标记阶段,在年轻代GC中完成
100.070: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0751469 secs]
[Parallel Time: 74.7 ms, GC Workers: 8]
[GC Worker Start (ms): Min: 100070.4, Avg: 100070.5, Max: 100070.6, Diff:
0.1]
[Ext Root Scanning (ms): Min: 0.1, Avg: 0.2, Max: 0.3, Diff: 0.2, Sum:
1.6]
[Update RS (ms): Min: 0.6, Avg: 1.1, Max: 1.5, Diff: 0.9, Sum: 8.9]
[Processed Buffers: Min: 1, Avg: 1.6, Max: 4, Diff: 3, Sum: 13]
[Scan RS (ms): Min: 1.0, Avg: 1.4, Max: 1.9, Diff: 0.9, Sum: 10.8]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum:
0.0]
[Object Copy (ms): Min: 71.5, Avg: 71.5, Max: 71.6, Diff: 0.1, Sum: 572.1]
[Termination (ms): Min: 0.3, Avg: 0.3, Max: 0.4, Diff: 0.1, Sum: 2.6]
[Termination Attempts: Min: 1382, Avg: 1515.5, Max: 1609, Diff: 227,
Sum: 12124]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.2]
[GC Worker Total (ms): Min: 74.5, Avg: 74.5, Max: 74.6, Diff: 0.1, Sum:
596.3]
[GC Worker End (ms): Min: 100145.1, Avg: 100145.1, Max: 100145.1, Diff:
0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
[Other: 0.4 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.1 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 23.0M(23.0M)->0.0B(14.0M) Survivors: 4096.0K->4096.0K Heap: 84.5M
(128.0M)->86.5M(128.0M)]
[Times: user=0.63 sys=0.00, real=0.08 secs]

// 把YHR中Survivor分区作为根,开始并发标记根扫描
100.146: [GC concurrent-root-region-scan-start]
// 并发标记根扫描结束,花费了0.0196297,注意扫描和Mutator是并发进行,同时有多个线程并行
100.165: [GC concurrent-root-region-scan-end, 0.0196297 secs]
// 开始并发标记子阶段,这里从所有的根引用:包括Survivor和强根如栈等出发,对整个堆进行标记
100.165: [GC concurrent-mark-start]
// 标记结束,花费0.08848s
100.254: [GC concurrent-mark-end, 0.0884800 secs]
// 这里是再标记子阶段,包括再标记、引用处理、类卸载处理信息
100.254: [GC remark 100.254: [Finalize Marking, 0.0002228 secs] 100.254:
[GC ref-proc, 0.0001515 secs] 100.254: [Unloading, 0.0004694 secs],
0.0011610 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
// 清除处理,这里的清除仅仅回收整个分区中的垃圾
// 这里还会调整RSet,以减轻后续GC中RSet根的处理时间
100.255: [GC cleanup 86M->86M(128M), 0.0005376 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]

混合GC日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 混合回收Mixed GC其实和YGC的日志类似,能看到GC pause(G1EvacuationPause)(mixed)这样的信息
// 日志分析参考Y年轻代GC。
122.132: [GC pause (G1 Evacuation Pause) (mixed), 0.0106092 secs]
[Parallel Time: 9.8 ms, GC Workers: 8]
[GC Worker Start (ms): Min: 122131.9, Avg: 122132.0, Max: 122132.0,
Diff: 0.1]
[Ext Root Scanning (ms): Min: 0.1, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.7]
[Update RS (ms): Min: 0.5, Avg: 0.7, Max: 0.9, Diff: 0.4, Sum: 5.4]
[Processed Buffers: Min: 1, Avg: 1.8, Max: 3, Diff: 2, Sum: 14]
[Scan RS (ms): Min: 1.0, Avg: 1.3, Max: 1.5, Diff: 0.5, Sum: 10.4]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum:
0.0]
[Object Copy (ms): Min: 7.5, Avg: 7.6, Max: 7.7, Diff: 0.2, Sum: 60.9]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[Termination Attempts: Min: 92, Avg: 105.1, Max: 121, Diff: 29, Sum: 841]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 9.7, Avg: 9.7, Max: 9.8, Diff: 0.1, Sum: 77.6]
[GC Worker End (ms): Min: 122141.7, Avg: 122141.7, Max: 122141.7, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.2 ms]
[Other: 0.7 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.1 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.5 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 3072.0K(3072.0K)->0.0B(5120.0K) Survivors: 3072.0K->1024.0K
Heap: 105.5M(128.0M)->104.0M(128.0M)]
[Times: user=0.00 sys=0.00, real=0.01 secs]

四、GC算法

算法 分代 优缺点
标记清除(Mark-Sweep) 年轻代 效率低,产生内存碎片
复制(Copying) 老年代 效率高,空间利用率低
标记压缩(Mark-Compact) 老年代 针对老年代对象特征优化
分代收集 年轻代使用复制,老年代使用标记压缩

4.1 标记清除

4.2 复制算法

4.3 标记压缩

五、参数设置

参数 含义
-XX:+UseG1GC JDK8需要设置
-XX:G1HeapRegionSize 设置每个Region的大小。值是2的幂,范围是1MB到32MB之间
-XX:MaxGCPauseMillis 期望达到的最大GC停顿时间指标,默认200ms
-XX:ParallelGCThread: 垃圾回收线程,最多为8,STW时
-XX:ConcGCThreads 并发标记线程数,一般=ParallelGCThreads/4

参数设置参考

由于是传统ERP项目,存量业务和大对象很多,所以Xmx设置的也比较大:

1
-Xms6g -Xmx12g -XX:+UseG1GC -XX:ParallelGCThreads=8 -XX:ConcGCThreads=8 -Xss512k -XX:MetaspaceSize=768m -XX:MaxMetaspaceSize=2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/log/erp-%t.hprof -XX:MaxDirectMemorySize=256m -XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:/data/log/erp-gc-%t.log

参考

G1垃圾回收期入门
https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html
G1详解
https://juejin.cn/post/7010034105165299725
最清晰易懂的G1
https://segmentfault.com/a/1190000039411521