-
Notifications
You must be signed in to change notification settings - Fork 2
G1 收集器
G1是一款面向服务端应用的垃圾收集器,与其他GC收集器相比,G1具备如下特点
- 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间
- 分代收集:分代概念在G1中依然保留。G1可以独立管理整个GC堆,且采用不同的方式去处理分代对象
- 空间整合:G1从整体来看是基于“标记—整理”算法实现的,从局部(两个Region之间)上来看是基于“复制”算法实现的;G1收集后能提供规整的可用内存。
- 可预测的停顿:G1能建立可预测的停顿时间模型,能明确指定垃圾收集相对于时间段的吞吐量。
G1收集器将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
G1根据各个Region回收所获得的空间大小以及回收所需时间等指标在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,从而可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
- -XX:+UseG1GC:使用G1收集器
- -XX:MaxGCPauseMillis:用于指定目标最大停顿时间。如果任何一次停顿超过这个设置值时,G1会尝试调整新生代和老年代的比例、堆大小、晋升年龄手段,以试图达到这个预设目标。但如果停顿时间过短,必然会增加新生代的GC频率,更有可能使Full GC触发的次数增加。
- -XX:InitiatingHeapOccupancyPercent:用于指定当整个堆使用率达到多少时,触发并发标记周期的执行,默认是45,即当整个堆使用率达到45%时会触发并行标记周期。G1始终不会自己修改这个值。如果该值偏大,会导致因并发标记周期过少而引起Full GC操作,过小会导致并发标记周期过于频繁,从而抢占用户线程资源。
- 数据结构:TLAB、PLAB、Collection Sets、Card Table、Remembered Sets;
- 算法:SATB。
分代模型
分代
分代垃圾收集可以将关注点集中在最近被分配的对象上,而无需整堆扫描,避免长命对象的拷贝,同时独立收集有助于降低响应时间。虽然分区使得内存分配不再要求紧凑的内存空间,但G1依然使用了分代的思想。与其他垃圾收集器类似,G1将内存在逻辑上划分为年轻代和老年代,其中年轻代又划分为Eden空间和Survivor空间。但年轻代空间并不是固定不变的,当现有年轻代分区占满时,JVM会分配新的空闲分区加入到年轻代空间。
整个年轻代内存会在初始空间-XX:G1NewSizePercent(默认整堆5%)与最大空间-XX:G1MaxNewSizePercent(默认60%)之间动态变化,且由参数目标暂停时间-XX:MaxGCPauseMillis(默认200ms)、需要扩缩容的大小以及分区的已记忆集合(RSet)计算得到。
TLAB
TLAB全称是Thread Local Allocation Buffer,即本地线程缓冲区。G1 GC会默认会启用TLAB优化。其作用就是在并发情况下,基于CAS的独享线程(Mutator Threads)可以优先将对象分配在一块内存区域(属于Java堆的Eden中),因为是Java线程独享的内存区,没有锁竞争,所以分配速度更快,每个TLAB都是一个线程独享的。如果待分配的对象被判断是巨型对象,则不使用TLAB。
PLAB
PLAB全称是Promotion Local Allocation Buffer,即晋升本地分配缓冲区。在新生代GC中,对象会将全部Eden区存活的对象转移(复制)到Survivor区。也会存在Survivor区对象晋升(Promotion)到老年代。这个决定晋升的阀值可以通过-XX:MaxTenuringThreshold设定。晋升的过程,无论是晋升到Survivor区还是Old区,都是在GC线程的PLAB中进行。每个GC线程都有一个PLAB。
Collection Sets
简称CSets,待收集集合。GC中待回收的Region的集合。CSet中可能存放着各个分代的Region,对于新生代GC,CSet只包含新生代Region,对于Mixed GC,CSet包含新生代Region和老年代Region。CSet中的存活对象会在GC过程中被移动(复制),GC后CSet中的Region会成为可用分区。
Card Table
卡表,Card Table用于在对新生代做GC Root枚举时避免对老年代进行全扫描。Java虚拟机将Java堆划分为相等大小的一个个区域,这个小的区域(一般大小在128 ~ 512字节)被称做Card,而Card Table维护着所有的Card。Card Table的结构是一个字节数组,Card Table用单字节的信息映射着一个Card。当Card中存储了对象时,称为这个Card被脏化了(Dirty Card)。对于一些热点Card会存放到Hot Card Cache中。同Card Table一样,Hot Card Cache也是全局的结构。
Remembered Sets
简称RSets,已记忆集合。G1中每个Region都有一个与之对应的RSet,并且每个分区只有一个RSet。其中存储着其他分区中的对象对本分区对象的引用,是一种points-in结构。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中,如果是,便通过Card Table把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。
当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。 新生代GC过程中,根据RSets来扫描老年代Region以甄别老年代对象对新生代对象的引用; Mixed GC过程中,根据RSets来扫描其他老年代Region中对象对于本老年代Region中对象的引用,避免对整个老年代Region的全面扫描,提高了GC效率。 因为每次GC都会扫描所有新生代Region的对象,所以RSet只有在扫描老年代Region对象引用新生代Region对象和老年代Region对象引用老年代Region对象时会被使用。
为了防止RSet溢出,对于一些比较热点的RSet会通过存储粒度级别来控制。RSet有三种粒度——Sparse, Fine和Coarse,对于热点RSet在存储时,根据细粒度的存储阀值,可能会采取粗粒度。这三种粒度的RSet都是通过Per Region Table来维护内部数据的。一个Per-Region-Table (PRT)是RSet存储颗粒度级别一个抽象。
- Sparse PRT是一个包含Card目录的Hash Table,G1收集器内部维护这些Card。Card包含来自Region的引用,这个Region的引用是Card到Owning Region的关联的地址。
- Fine-Grain PRT是一个开放的Hash Table,每一个Entry代表一个指向Owning Region的引用的Region,Region里面的Card目录,是一个Bitmap。
- 当达到Fine-Grain PRT的最大容量,Coarse Grain Bitmap里面的相应的Coarse-Grained bit被设置,相应地Entry从Fine-Grain PRT删除。Coarse bitmap有一个每个Region对应的bit。Coarse Grain map设置bit意味着关联的Region包含到Owning Region的引用。
Per Region Table RSet在内部使用Per Region Table(PRT)记录分区的引用情况。由于RSet的记录要占用分区的空间,如果一个分区非常"受欢迎",那么RSet占用的空间会上升,从而降低分区的可用空间。G1应对这个问题采用了改变RSet的密度的方式,在PRT中将会以三种模式记录引用:
- 稀少(Sparse):直接记录引用对象的卡片索引
- 细粒度(Fine):记录引用对象的分区索引
- 粗粒度(Coarse):只记录引用情况,每个分区对应一个比特位 由上可知,粗粒度的PRT只是记录了引用数量,需要通过整堆扫描才能找出所有引用,因此扫描速度也是最慢的。
分区模型
G1对内存的使用以分区(Region)为单位,而对对象的分配则以卡片(Card)为单位。
巨型对象(Humongous Region) 一个大小达到甚至超过分区大小一半的对象称为巨型对象(Humongous Object)。当线程为巨型分配空间时,不能简单在TLAB进行分配,因为巨型对象的移动成本很高,而且有可能一个分区不能容纳巨型对象。因此,巨型对象会直接在老年代分配,所占用的连续空间称为巨型分区(Humongous Region)。G1内部做了一个优化,一旦发现没有引用指向巨型对象,则可直接在年轻代收集周期中被回收。
巨型对象会独占一个、或多个连续分区,其中第一个分区被标记为开始巨型(StartsHumongous),相邻连续分区被标记为连续巨型(ContinuesHumongous)。由于无法享受Lab带来的优化,并且确定一片连续的内存空间需要扫描整堆,因此确定巨型对象开始位置的成本非常高,如果可以,应用程序应避免生成巨型对象。
SATB
- 对象漏标 根据三色标记算法,The Garbage Collection Handbook中将在对象存在定义为三种状态:
- 白:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉。
- 灰:对象被标记了,但是它的field还没有被标记或标记完。
- 黑:对象被标记了,且它的所有field也被标记完了。
由于并发阶段的存在,Mutator和Garbage Collector线程同时对对象进行修改,就会出现白对象漏标的情况(漏标的情况只会发生在白色对象中), 这种情况发生的前提是:
- Mutator赋予一个黑对象给白对象的引用。
- Mutator删除了所有从灰对象到该白对象的直接或者间接引用。
注:Mutator可理解为应用线程。
第一个条件中,白对象的情况有需要分别对待:
- 如果该白对象是在并发标记阶段新new出来的,且没有被灰对象持有。
对于这种情况,Region中有两个top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS。在TAMS以上的对象是新分配的,这是一种隐式的标记。
- 在GC时已经存在的白对象。但这种情况下如果它是活着的,必然会被另一个灰对象引用(因为黑对象的field都被标记了,所以不可能是黑对象引用的)。
对于这种情况,如果灰对象到白对象的直接引用或者间接引用被替换或删除了(即条件二成立),白对象就会被漏标(黑色对象和它的field不会重新标记),从而导致被回收掉,这是非常严重的错误。SATB破坏了第二个条件。也就是说,一个对象的引用被更改时,可以通过write barrier将旧引用记录下来。
注:可以理解为:对于第一种情况,利用post-write barrier,记录所有新增的引用关系,然后根据这些引用关系为根重新扫描一遍。对于第二种情况,利用pre-write barrier,将所有即将被删除的引用关系的旧引用记录下来,最后以这些旧引用为根重新扫描一遍。
- SATB详解 SATB的全称是Snapchat At The Beginning,是在G1 GC在并发标记阶段使用的增量式的标记算法。并发标记是并发多线程的,但并发线程在同一时刻只扫描一个分区。
2.1 解决新创建对象产生的漏标问题
SATB算法机制中,会在GC开始时先创建一个对象快照,在并发标记时所有快照中当时的存活对象就认为是存活的,标记过程中新分配的对象也会被标记为存活对象,不会被回收。这种机制能够很好解决新创建对象漏标的情况。STAB核心的两个结构就是两个BitMap。
/* from G1 Concurrent Mark 可以认为bitmap的内部存储着对象地址(reference 是8byte,所以bitmap存储着一个个64bit结构) */
G1CMBitMap * _prev_mark_bitmap; /* 全局的bitmap,存储preTAMS偏移位置,也即当前标记的对象的地址(初始值是对应上次已经标记完成的地址) */
G1CMBitMap * _next_mark_bitmap; /* 全局的bitmap,存储nextTAMS偏移位置。标记过程不断移动,标记完成后会和_prev_mark_bitmap互换。 */
bitmap分别存储着每个Region中,并发标记过程里的两个重要的变量: preTAMS(pre-top-at-mark-start,代表着Region上一次完成标记的位置) 以及 nextTAMS(next-top-at-mark-start,随着标记的进行会不断移动,一开始在top位置)。 SATB通过控制两个变量的移动来进行标记,移动规则如下:
- 假设第n轮并发标记开始,将该Region当前的Top指针赋值给nextTAMS,在并发标记标记期间,分配的对象都在[ nextTAMS, Top ]之间,SATB能够确保这部分的对象都会被标记,默认都是存活的。
- 当并发标记结束时,将nextTAMS所在的地址赋值给previousTAMS,SATB给[ Bottom, previousTAMS ]之间的对象创建一个快照Bitmap,所有垃圾对象能通过快照被识别出来。
- 第n+1轮并发标记开始,过程和第n轮一样。
如下示意图显示了两轮并发标记的过程:
对上图的逐步解释如下: 第A步:初始标记阶段,需要“stop the world”,将扫描Region的Top值赋值给nextTAMS。 第A ~ B步之间:会发生并发标记阶段。 第B步:重新标记阶段,此时并发标记阶段生成的新对象都会被分配在[ nextTAMS, Top ]之间,这些对象会被定义为“隐式对象”,同时_next_mark_bitmap也开始存储nextTAMS标记的对象的地址。 第C步:清除阶段,_next_mark_bitmap和_prev_mark_bitmap会进行交换,同时清理[ Bottom, previousTAMS ]之间被标记的所有对象,对于“隐式对象”会在下次垃圾收集过程进行回收(如第F步),这也是SATB存在弊端,会一定程度产生未能在本次标记中识别的浮动垃圾。
注:图中省略了根分区扫描和并发标记阶段,同时清理阶段还可以细分为独占清理和并发清理。
2.2 解决对象引用被修改产生的漏标问题 SATB利用pre-write barrier,将所有即将被修改引用关系的白对象旧引用记录下来,最后以这些旧引用为根重新扫描一遍,以解决白对象引用被修改产生的漏标问题。 代码如下:
- 在引用关系被修改之前,插入一层pre-write barrier,如第16行调用第2行的方法:
// openjdk/hotspot/src/share/vm/oops/oop.inline.hpp, lines 534 ~ 553
template <class T> inline void update_barrier_set_pre(T* p, oop v) {
oopDesc::bs()->write_ref_field_pre(p, v);
}
template <class T> inline void oop_store(T* p, oop v) {
if (always_do_update_barrier) {
oop_store((volatile T*)p, v);
} else {
update_barrier_set_pre(p, v);
oopDesc::encode_store_heap_oop(p, v);
update_barrier_set((void*)p, v); // cast away type
}
}
template <class T> inline void oop_store(volatile T* p, oop v) {
update_barrier_set_pre((T*)p, v); // cast away volatile
// Used by release_obj_field_put, so use release_store_ptr.
oopDesc::release_encode_store_heap_oop(p, v);
update_barrier_set((void*)p, v); // cast away type
}
- pre-write barrier最终执行逻辑:
// openjdk/hotspot/src/share/vm/gc_implementation/g1/g1SATBCardTableModRefBS.cpp lines 52 ~ 65
void G1SATBCardTableModRefBS::enqueue(oop pre_val) {
// Nulls should have been already filtered.
assert(pre_val->is_oop(true), "Error");
if (!JavaThread::satb_mark_queue_set().is_active()) return;
Thread* thr = Thread::current();
if (thr->is_Java_thread()) {
JavaThread* jt = (JavaThread*)thr;
jt->satb_mark_queue().enqueue(pre_val);
} else {
MutexLocker x(Shared_SATB_Q_lock);
JavaThread::satb_mark_queue_set().shared_satb_queue()->enqueue(pre_val);
}
}
通过G1SATBCardTableModRefBS::enqueue(oop pre_val)把原引用保存到satb_mark_queue中,和RSet的实现类似,每个应用线程都自带一个satb_mark_queue。在下一次的并发标记阶段,会依次处理satb_mark_queue中的对象,确保这部分对象在本轮GC是存活的。
SATB也是有副作用的,如果被修改引用的白对象就是要被收集的垃圾,这次的标记会让它躲过GC,这就是float garbage。因为SATB的做法精度比较低,所以造成的float garbage也会比较多。
G1收集器的运作过程可以分为四大阶段
- 新生代GC。
- 并发标记周期。
- 混合回收周期。
- Full GC(可能会发生)
新生代GC
新生代GC阶段也常被称作疏散(Evacuation)阶段,G1收集器在这个阶段的操作其实和其他收集器是类似的,即收集Eden区的数据到Survivor区,并清理无用区域。
如下面的示意图:
从上图可以看到,新生代GC完成之后,所有的Eden区都被清空了,而Survivor区只会清理一部分数据,另外会有新的老年代Region出现,这是由于新生代中有对象晋升到了老年代。
日志分析如下:
1 5.538: [GC pause (G1 Evacuation Pause) (young), 0.0055861 secs]
2 [Parallel Time: 5.3 ms, GC Workers: 4]
3 [GC Worker Start (ms): Min: 5537.7, Avg: 5538.6, Max: 5540.6, Diff: 2.9]
4 [Ext Root Scanning (ms): Min: 0.0, Avg: 0.2, Max: 0.4, Diff: 0.4, Sum: 0.7]
5 [Update RS (ms): Min: 0.0, Avg: 0.5, Max: 0.8, Diff: 0.8, Sum: 2.1]
6 [Processed Buffers: Min: 0, Avg: 3.2, Max: 5, Diff: 5, Sum: 13]
7 [Scan RS (ms): Min: 0.0, Avg: 0.8, Max: 1.1, Diff: 1.1, Sum: 3.0]
8 [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
9 [Object Copy (ms): Min: 2.4, Avg: 3.0, Max: 3.2, Diff: 0.8, Sum: 11.8]
10 [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
11 [Termination Attempts: Min: 1, Avg: 54.8, Max: 88, Diff: 87, Sum: 219]
12 [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
13 [GC Worker Total (ms): Min: 2.4, Avg: 4.4, Max: 5.3, Diff: 2.9, Sum: 17.7]
14 [GC Worker End (ms): Min: 5543.0, Avg: 5543.0, Max: 5543.0, Diff: 0.0]
15 [Code Root Fixup: 0.0 ms]
16 [Code Root Purge: 0.0 ms]
17 [Clear CT: 0.0 ms]
18 [Other: 0.2 ms]
19 [Choose CSet: 0.0 ms]
20 [Ref Proc: 0.1 ms]
21 [Ref Enq: 0.0 ms]
22 [Redirty Cards: 0.0 ms]
23 [Humongous Register: 0.0 ms]
24 [Humongous Reclaim: 0.0 ms]
25 [Free CSet: 0.0 ms]
26 [Eden: 4096.0K(4096.0K)->0.0B(3072.0K) Survivors: 2048.0K->1024.0K Heap: 23.0M(32.0M)->21.4M(32.0M)]
27 [Times: user=0.02 sys=0.00, real=0.01 secs]
这里对上面的日志进行逐行拆解:
- 5.538: [GC pause (G1 Evacuation Pause) (young), 0.0055861 secs]:表示在第5.538秒的时候进行了一次Young Generation的Evacuation操作,耗时0.0055861秒(墙钟检测时间)。
- [Parallel Time: 5.3 ms, GC Workers: 4]:使用4个GC线程,并行时间5.3毫秒,包含了第3 ~ 14行代表的任务,并行执行。
- [GC Worker Start (ms): Min: 5537.7, Avg: 5538.6, Max: 5540.6, Diff: 2.9]:线程开始活动的合计时间,在阶段的开始时间匹配时间戳。如果Min和Max差别很大,它也许表明太多线程被使用或者JVM里的GC进程CPU时间被机器上其他进程盗用。
- [Ext Root Scanning (ms): Min: 0.0, Avg: 0.2, Max: 0.4, Diff: 0.4, Sum: 0.7]:扫描外部(非Heap)Root消耗的时间,例如ClasLoader、JNI引用、JVM系统等等。Sum是CPU时间。
- [Update RS (ms): Min: 0.0, Avg: 0.5, Max: 0.8, Diff: 0.8, Sum: 2.1]:更新Remembered Set的时间信息。-XX:MaxGCPauseMillis参数是限制G1的暂停之间,一般RSet更新的时间小于10%的目标暂停时间是比较可取的。如果花费在RSet Update的时间过长,可以修改其占用总暂停时间的百分比-XX:G1RSetUpdatingPauseTimePercent,这个参数的默认值是10。
- [Processed Buffers: Min: 0, Avg: 3.2, Max: 5, Diff: 5, Sum: 13]:已处理缓冲区。这个阶段处理的是在优化线程中处理Dirty Card分区扫描时记录的日志缓冲区。
- [Scan RS (ms): Min: 0.0, Avg: 0.8, Max: 1.1, Diff: 1.1, Sum: 3.0]:如果RSet中的Bitmap是粗粒度的,那么就会增加RSet扫描的时间。如下所示的扫描时间,说明还没有粗化的RSet。
- [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]:代码根的扫描。只有在分区的RSet有强代码根时会检查CSet的对内引用,例如常量池。
- [Object Copy (ms): Min: 2.4, Avg: 3.0, Max: 3.2, Diff: 0.8, Sum: 11.8]:对CSet中存活对象进行转移(疏散回收,即类似“复制算法”过程)。对象拷贝的时间一般占用暂停时间的主要部分,如果拷贝时间和“预测暂停时间”相差很大,也可以调整年轻代尺寸大小。
- [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]:主要是终止工作线程。Work线程在工作终止前会检查其他工作线程的任务,看是否有未清理完的Reference。如有则帮助其他线程清理,之后再尝试终止。如果终止时间较长,可能是某个Work线程在某项任务执行时间过长。
- [Termination Attempts: Min: 1, Avg: 54.8, Max: 88, Diff: 87, Sum: 219]:尝试终止次数。
- [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]:花在GC之外的工作线程的时间,比如因为JVM的某个活动,导致GC线程被停掉。这部分消耗的时间不是真正花在GC线程上,只是作为日志的一部分记录。
- [GC Worker Total (ms): Min: 2.4, Avg: 4.4, Max: 5.3, Diff: 2.9, Sum: 17.7]:并行阶段GC线程耗时,包含了GC Worker Other时间。
- [GC Worker End (ms): Min: 5543.0, Avg: 5543.0, Max: 5543.0, Diff: 0.0]:GC Worker阶段总耗时。
- [Code Root Fixup: 0.0 ms]:释放用于管理并行活动的数据结构。
- [Code Root Purge: 0.0 ms]:清除更多数据结构,通常应该非常快。
- [Clear CT: 0.0 ms]:清空Card Table,RS是依赖Card Table记录区域存活对象的。
- [Other: 0.2 ms]:其他任务耗时,包含了第19 ~ 25行代表的任务,大多都是并行执行的。
- [Choose CSet: 0.0 ms]:选择CSet,因为年轻代的所有分区都会被收集,所以CSet不需要选择,消耗时间都是0ms。Choose CSet任务一般都是在Mixed GC的过程中触发。
- [Ref Proc: 0.1 ms]:处理非强引用(清除或者确定不需要清理)。
- [Ref Enq: 0.0 ms]:将上述清理后剩下的非强引用排列到相应的Reference队列中。
- [Redirty Cards: 0.0 ms]:重新脏化卡片。排队引用可能会更新RSet,所以需要对关联的Card重新脏化(Redirty Cards)。
- [Humongous Register: 0.0 ms]:新生代巨型对象的信息注册操作。
- [Humongous Reclaim: 0.0 ms]:新生代巨型对象的回收操作,该阶段会对RS中有引用的短命的巨型对象进行回收,巨型对象会直接回收而不需要进行转移(转移代价巨大,也没必要)。
- [Free CSet: 0.0 ms]:释放被回收区域的耗时(包含他们的RS)
- [Eden: 4096.0K(4096.0K)->0.0B(3072.0K) Survivors: 2048.0K->1024.0K Heap: 23.0M(32.0M)->21.4M(32.0M)]:阶段完成前后的Eden区使用大小和容量大小、Survivor区的大小、Heap区使用大小和容量大小变化。这里的记录表明Eden区被清空了,同时总大小也做了调整,G1 GC的停顿时间是可预测的,所以新生代GC之后,会根据停顿时间的目标重新计算需要的Eden分区数,进行动态调整;而Survivor区的空间减小了,说明有对象被回收;整个堆区GC前后已使用空间减小了,G1 GC会动态调整堆区,但这次回收中没有改变堆区的容量。
- [Times: user=0.02 sys=0.00, real=0.01 secs]:阶段总耗时。
注:在上述日志类似于Min: 0.0, Avg: 0.2, Max: 0.4, Diff: 0.4, Sum: 0.7片段中,Diff是偏移平均时间的值,Diff越小越好,说明每个工作线程的速度都很均匀,如果Diff值偏大,就要看下面具体哪一项活动产生的波动。Avg代表平均时间值。如果Avg跟Min、Max偏差不大是比较正常的,否则也要详细分析具体的偏差值大的任务。
并发标记周期 并发标记周期细分步骤
- 初始标记(Initial Marking):需要“stop the world”。这个阶段用于标记GC Root直接可达对象,一定会有一次新生代GC。
- 根区域扫描(Root Region Scan):可与用户线程并发执行。初始标记阶段的新生代GC会将Eden清空,存活对象被转入Survivor区,所以在本阶段将扫描并标记由Survivor区直接可达的老年代区域。由于依赖Survivor的当前状况,此阶段不能和新生代GC同时执行。
- 并发标记(Concurrent Marking):可与用户线程并发执行。扫描、查找并标记整个堆的存活对象,该过程可以被一次新生代GC打断,也有一定可能直接被取消,如果在这一步被取消,整个并发标记周期会放弃重来。
- 重新标记(Remarking):需要“stop the world”。对并发标记的结果进行修正和补充。
- 独占清理(Cleanup):需要“stop the world”。根据优先列表进行相应Region的垃圾回收,更新Remembered Set。
- 并发清理(Concurrent Clean):可与用户线程并发执行。识别并清理完全空闲的区域。
并发标记周期的运行流程示意图如下
- 初始标记 初始标记一定会有一次新生代GC的发生。初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改nextTAMS的值(将Top的值赋值给nextTAMS),让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。如下面的日志(G1 Evacuation Pause) (young) (initial-mark)表示一次初始标记步骤的执行:
5.549: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0020293 secs]
[Parallel Time: 1.7 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 5549.3, Avg: 5549.4, Max: 5549.4, Diff: 0.1]
[Ext Root Scanning (ms): Min: 0.2, Avg: 0.2, Max: 0.3, Diff: 0.1, Sum: 0.9]
[Update RS (ms): Min: 0.9, Avg: 1.0, Max: 1.1, Diff: 0.1, Sum: 4.0]
[Processed Buffers: Min: 4, Avg: 5.5, Max: 10, Diff: 6, Sum: 22]
[Scan RS (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.3]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 0.3, Avg: 0.3, Max: 0.3, Diff: 0.0, Sum: 1.2]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[GC Worker Total (ms): Min: 1.6, Avg: 1.6, Max: 1.6, Diff: 0.1, Sum: 6.4]
[GC Worker End (ms): Min: 5551.0, Avg: 5551.0, Max: 5551.0, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
[Other: 0.3 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: 3072.0K(3072.0K)->0.0B(2048.0K) Survivors: 1024.0K->1024.0K Heap: 24.4M(32.0M)->22.3M(32.0M)]
[Times: user=0.01 sys=0.00, real=0.01 secs]
同时,如果在碰到巨型对象的分配时,可能由于内存不足必然会导致一次并发标记周期的执行,这种情况下初始标记周期的头日志说明如下:
5.465: [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0012359 secs]
[Parallel Time: 1.0 ms, GC Workers: 4]
...
其中G1 Humongous Allocation表明有巨型对象正在被分配内存
- 根区域扫描 下面的日志展示的是一次并发的根区域扫描,并发扫描过程不能被新生代GC中断,且由于是并发执行,根区域扫描不会产生停顿:
5.551: [GC concurrent-root-region-scan-start]
5.552: [GC concurrent-root-region-scan-end, 0.0001604 secs]
- 并发标记 并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。下面的日志展示的即是一次并发标记步骤的开始和完成:
5.552: [GC concurrent-mark-start]
5.580: [GC concurrent-mark-end, 0.0282578 secs]
如果碰到类似于下面的日志,说明并发标记步骤被打断了:
10.999: [GC concurrent-mark-start]
11.018: [GC pause (G1 Evacuation Pause) (young), 0.0017091 secs]
[Parallel Time: 1.4 ms, GC Workers: 4]
...
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 0.2 ms]
...
[Eden: 1024.0K(1024.0K)->0.0B(1024.0K) Survivors: 1024.0K->1024.0K Heap: 27.7M(32.0M)->27.3M(32.0M)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
11.034: [GC concurrent-mark-end, 0.0351422 secs]
从日志中可以看出,并发标记步骤开始后,执行了一次新生代的GC操作打断了并发标记,在新生代GC操作完成后并发标记步骤才结束。 同时,如果遇到内存不足导致的内存分配失败,会使并发标记阶段直接被终止:
5.397: [GC concurrent-mark-start]
5.398: [Full GC (Allocation Failure) 32M->12M(32M), 0.0648675 secs]
[Eden: 0.0B(1024.0K)->0.0B(12.0M) Survivors: 0.0B->0.0B Heap: 32.0M(32.0M)->12.1M(32.0M)], [Metaspace: 14734K->14728K(1062912K)]
[Times: user=0.06 sys=0.00, real=0.07 secs]
5.463: [GC concurrent-mark-abort]
一旦并发标记步骤被终止,这次的并发标记周期就直接取消了,会直接开始进行下一次并发标记周期的执行(新的并发标记周期中初始标记步骤必然会进行一次新生代GC以腾出空闲内存空间);同时,新的并发标记周期会有很大几率伴随一次因巨型对象的创建需要的内存分配导致的新生代GC操作:
5.397: [GC concurrent-mark-start]
5.398: [Full GC (Allocation Failure) 32M->12M(32M), 0.0648675 secs]
[Eden: 0.0B(1024.0K)->0.0B(12.0M) Survivors: 0.0B->0.0B Heap: 32.0M(32.0M)->12.1M(32.0M)], [Metaspace: 14734K->14728K(1062912K)]
[Times: user=0.06 sys=0.00, real=0.07 secs]
5.463: [GC concurrent-mark-abort]
5.465: [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0012359 secs]
[Parallel Time: 1.0 ms, GC Workers: 4]
...
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 0.2 ms]
...
[Eden: 1024.0K(12.0M)->0.0B(9216.0K) Survivors: 0.0B->1024.0K Heap: 13.4M(32.0M)->13.4M(32.0M)]
[Times: user=0.01 sys=0.00, real=0.00 secs]
- 重新标记 重新标记(也叫最终标记)步骤则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,重新标记步骤需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
5.581: [GC remark 5.581: [Finalize Marking, 0.0000953 secs] 5.581: [GC ref-proc, 0.0000619 secs] 5.582: [Unloading, 0.0017910 secs], 0.0020940 secs]
[Times: user=0.01 sys=0.00, real=0.00 secs]
- 独占清理 重新标记后会进行独占清理,独占清理会重新计算各个Region的存活对象,并以此可以得到每个Region进行GC的效用,从而对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。独占清理的日志如下:
5.584: [GC cleanup 21M->20M(32M), 0.0002219 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
- 并发清理 并发清理会根据独占清理步骤计算出来GC效用排序和每个Region的存活数量,直接回收已经不包含存活对象的区域。它的日志如下:
5.584: [GC concurrent-cleanup-start]
5.584: [GC concurrent-cleanup-end, 0.0000046 secs]
整个并发标记周期的GC示意图如下:
从图中可以得知,并发标记周期完成后,原有的Eden Region和Survivor Region会被清理掉,同时因为某些步骤是用户线程和GC线程并发执行,也会有新的Eden Region生成。另外一些Old Region会被标记为G Region,这些G Region会被记录在CSets中
混合回收周期 混合回收即Mixed GC,之所以称之为混合回收,是因为它同时对新生代和老年代同时进行GC操作。在并发标记周期中,虽然也有部分对象被回收,但是比例非常低。在并发标记周期之后,含有较多垃圾对象的Region已经被筛选出来了,G1会在混合回收周期统一对这些Region进行垃圾回收处理。混合回收的日志示例如下:
5.681: [GC pause (G1 Evacuation Pause) (mixed), 0.0026889 secs]
[Parallel Time: 2.5 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 5680.9, Avg: 5680.9, Max: 5681.0, Diff: 0.1]
[Ext Root Scanning (ms): Min: 0.2, Avg: 0.9, Max: 2.4, Diff: 2.2, Sum: 3.5]
[Update RS (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.2]
[Processed Buffers: Min: 0, Avg: 1.8, Max: 5, Diff: 5, Sum: 7]
[Scan RS (ms): Min: 0.0, Avg: 0.2, Max: 0.4, Diff: 0.4, Sum: 0.9]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 0.0, Avg: 1.1, Max: 1.5, Diff: 1.5, Sum: 4.4]
[Termination (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.5]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[GC Worker Total (ms): Min: 2.3, Avg: 2.4, Max: 2.4, Diff: 0.1, Sum: 9.6]
[GC Worker End (ms): Min: 5683.3, Avg: 5683.3, Max: 5683.3, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 0.2 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.1 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 1024.0K(1024.0K)->0.0B(1024.0K) Survivors: 1024.0K->1024.0K Heap: 21.9M(32.0M)->19.6M(32.0M)]
[Times: user=0.01 sys=0.00, real=0.01 secs]
从日志中(G1 Evacuation Pause) (mixed)可以得知这是一次混合回收
在多次混合回收之后,G1又会触发数次新生代GC的操作,然后又开始下一个运行周期,周而复始。
混合回收周期的GC示意图如下
从图中可以得知,混合回收周期完成后,因为是新生代GC和老年代GC同时操作,原有的Eden Region和Survivor Region会被清理掉,也会有Survivor对象晋升到老年代Region;同时在并发标记周期被标记为G Region区域会被回收。
Full GC 由于G1收集器的运行流程中,GC线程和用户线程是交互执行了,所以也会出现某些回收周期中出现内存不足的情况,这种情况下会触发一个Full GC。像上面提到的终止并发标记步骤的情况,就是因为内存不足导致出现了一次Full GC操作。