目前,有关ZGC的文档还不是很多,我在阅读 http://cr.openjdk.java.net/~pliden/slides/ZGC-FOSDEM-2018.pdf 等文档时,还是有很多不理解的地方,通过一些实验,以及对源代码进行分析,本文试图还原出一个完整的GC 流程,如果你对ZGC的内部实现感兴趣的话,可以结合本文,来更好的理解ZGC的完整流程中,是如何完成Color的转换的。
本文选择的一个测试程序如下:
public class Hello {
public static void main(String args[]) throws Exception {
int i = 0;
System.out.println("beginning");
while(true){
mem1();
i++;
System.out.println("loop for " + i);
}
}
static String mkString(int len) {
StringBuilder sb = new StringBuilder();
for(int i = 0; i < len; i++) sb.append('x');
return sb.toString();
}
static String[] mem1() throws Exception {
String[] arr = new String[1000];
java.util.Random r = new java.util.Random();
for(int i = 0; i < 1000; i++) {
arr[i] = mkString(r.nextInt(50) + 20);
}
Thread.sleep(10);
return arr;
}
}
编译如上程序后,使用如下命令行,既可以跟踪整个的 GC 过程:/root/jdk/bin/java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -Xms512m -Xmx512m -Xlog:gc*=debug::u: Hello
JVM 启动时, GoodMask = Remapped
[0.054s] Initializing The Z Garbage Collector
[0.054s] Version: 12-internal+0-adhoc..zgc-4bbc9ad465a5 (release)
!!! set_good_mask 100000000000 goodMask = 100000000000 bad = 2c0000000000
[0.054s] NUMA Support: Disabled
[0.054s] CPUs: 4 total, 4 available
[0.055s] Memory: 3947MWarmup 过程
zgc 有一个很慢的warm过程,具体做什么,还有待研究,在我们的这个项目中,花了接近10s,才开始完成warm,开始执行应用代码。
[9.924s] Allocation Rate: 20.000MB/s, Avg: 6.000(+/-0.980)MB/s[9.924s] Rule: Warmup 10%, Used: 46MB, UsedThreshold: 51MBbeginning[10.025s] Allocation Rate: 0.000MB/s, Avg: 6.000(+/-0.980)MB/s[10.025s] Rule: Warmup 10%, Used: 46MB, UsedThreshold: 51MBloop for 1[10.125s] Allocation Rate: 20.000MB/s, Avg: 8.000(+/-1.200)MB/s[10.125s] Rule: Warmup 10%, Used: 48MB, UsedThreshold: 51MBloop for 2loop for 3开始GC0,进入到 Pause Mark Start阶段。耗时 4.7ms。
Pause Mark Start 阶段时一个 STW操作,这个阶段的主要任务:
[10.325s] GC(0) Heap before GC invocations=0 (full 0): ZHeap used 52M, capacity 512M, max capacity 512M
[10.325s] GC(0) Metaspace used 6858K, capacity 6911K, committed 7168K, reserved 8192K
[10.325s] GC(0) Garbage Collection (Warmup)
[10.326s] GC(0) Pause Mark Start
!!! set_good_mask 80000000000 goodMask = 80000000000 bad = 340000000000
[10.327s] GC(0) Using 1 mark stripes
[10.327s] GC(0) Mark Worker/Stripe Distribution
[10.327s] GC(0) Worker 0(1) -> Stripe 0(1)
[10.327s] GC(0) Pause Roots Setup (VM Thread)
[10.331s] GC(0) Pause Mark Start 4.742ms暂停所有的应用线程
将 GoodMask 切换到 Marked1
从root集合开始标记所有的对象
进入到 GC0 的 Concurrent Mark 阶段,耗时 6.4ms(这个耗时根据堆的大小而变化,对较大的Heap,由于需要遍历,时间应该是比较长的。)
Mark阶段是与应用线程并行进行的,这个从日志中可以看得出来,这个阶段的主要任务是:
[10.331s] GC(0) Concurrent Mark
[10.331s] GC(0) Executing Task: ZMarkTask, Active Workers: 1
loop for 20[10.332s] GC(0) Concurrent Mark (ZWorker#1)
[10.338s] GC(0) Concurrent Mark 6.401ms
从root 结合出发,遍历heap,对每一个对象进行标记。被标记的对象是live的。这个遍历算法另行分析
当前的GoodMask是Marked1,当前Heap中的引用有两个颜色:Remapped(bad)、Marked1(Good)。
此时,如果应用线程中访问一个 bad colored 引用(a = obj.field1 ),将通过 Read Barrier 完成颜色重写。field1 将修改为 Marked1,对应的 a 也调整为 Marked1。
此时,新分配的对象引用都具有颜色 Marked1
在 Mark 阶段,是否会对所有遍历的引用,都完成颜色重写的过程呢?目前的猜测是的,这样的话,在Mark结束后,所有的活跃饮用都具有颜色 Marked1。
进入到 GC0. Pause Mark End阶段。这是一个STW阶段,耗时 1.14ms。
[10.338s] GC(0) Pause Mark End
[10.338s] GC(0) Pause Roots Setup (VM Thread)
...
[10.339s] GC(0) Pause Mark End 1.140ms
这个阶段的主要任务是对Weak roots进行清理。具体待分析。
进入到 GC0. Concurrent Prepare for Reloc 阶段。
这个阶段是并行的,这个可以从日志中,看出来,有应用的日志产生了。这个阶段用时一般较短。主要任务:
[10.339s] GC(0) Concurrent Process Non-Strong References
...
[10.341s] GC(0) Concurrent Select Relocation Set
loop for 21
[10.346s] GC(0) Relocation Set (Medium Pages): 0->0, 0 skipped
...
[10.347s] GC(0) Concurrent Prepare Relocation Set 0.609ms
对 reference 进行处理
清理 weak reference
选择 relocatin set,并为其准备好 forward table
进入到 GC0的Pause Relocate Start阶段
这是一个STW阶段,需要暂停的所有的应用线程,对Root集合进行 relocate处理。主要任务:
这个阶段结束以后,会达到如下状态:
[10.347s] GC(0) Pause Relocate Start
!!! set_good_mask 100000000000 goodMask = 100000000000 bad = 2c0000000000
[10.347s] GC(0) Pause Remap TLABS (VM Thread)
......
[10.348s] GC(0) Pause Relocate Start 1.429ms
当前Heap中存在两种颜色:Marked1(bad color)、Remapped(good color)。大部分处在 bad color。
root 集合的颜色全部是 good color(如果对象在relocation set中,则已完成了重定位)。
切换Good Color颜色为 Remapped。
遍历所有的 root 集合,将root集合的全部饮用调整为 Remapped。
进行颜色切换的时候,需要检查这个指针是否在relocation set中,如果在的话,则进行重定位工作。
进入到 GC0 的 Concurrent Relocate 阶段。
承接上一阶段,本阶段的主要目的是完成对relocation set的重定位工作,即将选择进行回收的空间中的全部对象,移动到一个新的地址中,完成后,原来的这个area就可以被释放了。这个阶段一般也会比较快。由于是与应用线程并行的,并且目前存在两种颜色:
本阶段结束后,Heap 会达到如下状态:
[10.349s] GC(0) Concurrent Relocate
[10.349s] GC(0) Executing Task: ZRelocateTask, Active Workers: 1
[10.352s] GC(0) Concurrent Relocate 2.817ms
大部分对象是 Marked1(bad color)
部分对象是 Remapped(good color)。全部的root应用已调整为 good。
relcocation set 的对象已经全部移动到新的位置,但指向这些对象的引用大部分还没有被更新。
relocation set所占用的area 可以整块区间被释放掉。
大部分对象是Marked1(bad color)
部分对象是 Remapped(good color)。全部的root应用已调整为 good。
在这个过程中,应用线程如果访问到 bad color 对象( a = obj.field ),将通过load barrier完成颜色的重写。重写的过程中可能会完成对象的重定位。
在这个过程中,GC 线程会完成将 重定位的对象复制到新的空间的工作,并且建立 forward table。
完成了 Concurrent Relocate 后,GC0 就告一段落。此时的Heap状态是:
由于已经释放了足够的空间,应用线程可以高速的执行
大部分对象是 Marked1(bad color)
部分对象是 Remapped(good color)。全部的root应用已调整为 good。
有一个当前的 forward table,记录了移动前后的对象映射关系
当应用线程访问一个bad color的过程时,load barrier 会自动的将其更新为 Remapped。
应用线程执行 4s 之后,再次出现GC需求,此时启动 GC1
这个过程与GC0 是相似的。不同之处,GC0刚开始时,所有的引用都是 Good Color,而GC1 开始时,heap中 混杂着 Good Color + Bad Color(Marked1)。
GC1. Pause Mark Start。
Remapped. 这些引用是上一个GC阶段后正常的引用,只需要简单的切换为 Marked 0即可。
Marked1:这些引用是上一个GC阶段后,还没有完成 relocate 的,如果后续要切换为 Marked0,还需要进行relocate 的地址转换。
Marked0: Good Color:所有的root颜色已经是 Marked0
切换 Good Color 为 Marked 0,此时系统中混杂着3中颜色:
GC1. Concurrent Mark
这个阶段的目标是遍历heap,主要进行两个操作。
Mark 标记所有的活跃的对象。
Remap 将所有的引用颜色修正为 Marked0(包括从 Remapped -> Marked0, 从 Marker1 -> Marked0 )
Mark 和 Remap 两者可以同时进行
GC1. Pause Mark End
自此之后的处理,与 GC0的完全一致。




