YGC的流程如下:
首先STW,YGC全过程都在STW时进行,不需要考虑并发场景
选择CSet(Collection Set),YGC中CSet即为全部新生代Region
根扫描
更新RSet
深度复制更新对象到Survivor Region
重构RSet
释放CSet
大对象回收
动态扩展内存
动态调整新生代Region数量
启动并发标记,判断是否需要紧接着进行一次混合式GC
GC并行任务包括跟扫描、更新RSet、对象复制,主要逻辑在g1CollectedHeap.cpp G1ParTask类的work方法中;evacuate_roots为根扫描。
- 处理java根
- 处理jvm根
- 处理string table根
- 处理所有已加载类的元数据
- 处理所有Java线程当前栈帧的引用和虚拟机内部线程
- 处理JVM内部使用的引用(Universe和SystemDictionary)
- 处理JNI句柄
- 处理对象锁的引用
- 处理java.lang.management管理和监控相关类的引用
- 处理JVMTI(JVM Tool Interface)的引用
- 处理AOT静态编译的引用
处理StringTable JVM字符串哈希表的引用
根据age判断copy到新生代还是老年代
先尝试在PLAB中分配对象
PLAB分配失败后的逻辑与TLAB类似,先申请一个新的PLAB,在旧PLAB中填充dummy对象,在新PLAB中分配,如果还是失败,则在新生代Region中直接分配
如果还是失败,则尝试在老年代Region中重新分配
age加1,由于锁升级机制,当对象锁状态是轻量级锁或重量级锁时,对象头被修改为指向栈锁记录的指针或者互斥量的指针,修改age需要特殊处理
对于字符串去重的处理
如果是数组,且数组长度超过ParGCArrayScanChunk(默认50)时,将对象放入队列而不是深度搜索栈中,防止搜索时溢出
2.2.3 深度搜索复制
G1ParTask的work函数调用evac.do_void()进行对象复制
并行线程处理完当前任务后,可以窃取其他线程没有处理完的对象
调用do_oop_evac复制一般对象,调用do_oop_partial_array处理大数组对象
如果对象已经复制,则无需再次复制
否则,调用copy_to_survivor_space复制对象
更新引用者field地址
如果引用者与当前对象不在同一个分区,且引用者不在新生代分区中,则更新RSet信息入队
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
混合式GC
1. 简介
YGC整个过程都在STW下进行,出于减少停顿时间的考量,对于老年代的回收显然需要与Mutator同时进行,G1引入了混合式GC,与CMS算法类似,均采用了并发标记。
混合式回收主要分为如下子阶段:
初始标记子阶段
并发标记子阶段
再标记子阶段
清理子阶段
垃圾回收
2. 算法概览
2.1 标记算法概览
由于混合式GC使用的是并发标记,Mutator可能会随时改变对象引用关系,从而导致漏标和错标。
错标仅导致浮动垃圾,并不会导致运行错误。而漏标会导致对象被错误的回收,进而产生严重错误;为了避免漏标,G1引入了三色标记法。
白色:垃圾收集器未探测到的对象
灰色:活着的对象,但是依然没有被垃圾收集器扫描过
黑色:活着的对象,并且已经被垃圾收集器扫描过
2.2 STAB机制简介
SATB(start at the beginning),在并发标记时,如果对象引用关系发生变化,G1会通过putfield字节码中的写屏障将这一引用关系的变化写入G1SATBMarkQueueSet和G1SATBMarkQueue中。并在并发标记子阶段和再标记子阶段处理G1SATBMarkQueueSet和G1SATBMarkQueue中的数据。
- YGC最后阶段判断是否启动并发标记
- 判断的依据是分配和即将分配的内存占比是否大于阈值
- 阈值受JVM参数InitiatingHeapOccupancyPercent控制,默认45
如果需要进行并发标记,则通知并发标记线程
3.2 初始标记子阶段
初始标记子阶段需要STW。
混合式GC的根GC就是YGC的Survivor Region。
扫描根Region的入口在g1ConcurrentMark.cpp
在GC并发线程组中,调用G1CMRootRegionScanTask
- while循环遍历根Region列表
- 调用scan_root_region,扫描每个根Region
执行闭包G1RootRegionScanClosure,遍历整个Region中的对象
调用mark_in_next_bitmap标记根Region中的对象
3.3 并发标记子阶段
并发标记子阶段与Mutator同时进行。
并发标记的入口在G1CMConcurrentMarkingTask的work方法
- 调用do_marking_step进行并发标记
- G1ConcMarkStepDurationMillis JVM参数定义了每次并发标记的最大时长,默认10毫秒
do_marking_step函数的代码非常长且复杂,这里不再贴出,该函数主要功能如下:
- 处理STAB队列,STAB的处理模式与DCQS类似
- 扫描全部的灰色对象,并对它们的每一个field进行递归并发标记
- 当前任务完成后,窃取其他队列的任务
3.4 再标记子阶段
由于并发标记子阶段与Mutator同时执行,对象引用关系仍然有可能发生变化,因此需要再标记阶段STW后处理完成全部STAB。
再标记子阶段入口在G1CMRemarkTask
仍然调用do_marking_step函数处理,但是target time为1000000000毫秒,表示任何情况下都要执行完成
3.5 清理子阶段
清理子阶段是指RSet清理、选择回收的Region等,但并不会复制对象和回收Region。清理子阶段仍然需要STW,入口在cleanup方法:
- G1UpdateRemSetTrackingAfterRebuild中将Region的RSet状态置为Complete
- 调用record_concurrent_mark_cleanup_end选择哪些Region需要回收
调用G1RemSetTrackingPolicy的update_after_rebuild方法
将RSet状态置为Complete
- 调用CollectionSetChooser rebuild方法选择CSet
- 调用record_concurrent_mark_cleanup_end,判断CSet中可回收空间占比是否小于阈值
- 使用ParKnownGarbageTask并行判断分区的垃圾情况
- 对Region继续排序,从order_regions函数可以看出,排序依据是gc_efficiency
gc_efficiency=可回收的字节数 / 预计的回收毫秒数
判断CSet中可回收空间占比是否小于阈值
阈值受JVM参数 G1HeapWastePercent控制,默认5。只有当可回收空间占比大于阈值时,才会启动混合式GC回收
4. 新一代GC算法的探讨
JDK11和JDK12中加入了Shenandoah和ZGC,对停顿时长做了进一步的优化,达到了毫秒级。
G1混合式GC中对象复制时,仍然需要STW,而Shenandoah和ZGC通过在读屏障和写屏障中的处理,使得对象复制也可以和Mutator并发执行了。
Shenandoah和ZGC尚处于实验阶段,目前谈论替代G1为时尚早。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Full GC
1. 简介
当晋升失败、疏散失败、大对象分配失败、Evac失败时,有可能触发Full GC,在JDK10之前,Full GC是串行的,JEP 307: Parallel Full GC for G1之后引入了并行Full GC。本文主要介绍并行Full GC机制。
Full GC的入口在g1CollectedHeap.cpp的G1CollectedHeap::do_full_collection
- 准备回收,prepare_collection
- 回收,collect
- 回收后处理,complete_collection
- Full GC应当清理软引用
- 由于Full GC过程中,永久代(元空间)中的方法可能被移动,需要保存bcp字节码指针数据或者转化为bci字节码索引
- 保存轻量级锁和重量级锁的对象头
- 清理和处理对象的派生关系
2.2 回收阶段
- phase1 并行标记对象
- phase2 并行准备压缩
- phase3 并行调整指针
- phase4 并行压缩
2.2.1 并行标记
从GC roots出发,递归标记所有的活跃对象。
标记对象,具体逻辑在G1FullGCMarkTask中
清理弱引用
卸载类的元数据(complete_cleaning)或仅清理字符串(partial_cleaning)
清理字符串会清理StringTable和字符串去重(JEP 192: String Deduplication in G1)
如果允许卸载类的元数据,则调用process_strong_roots;否则调用process_all_roots_no_string_table
process_strong_roots的GC roots仅强根
process_all_roots_no_string_table的GC roots包括弱根、强根,但是不含StringTable
遍历标记栈中的所有对象
2.2.2 准备压缩
计算每个活跃对象应该在什么位置,即计算对象压缩后的新位置指针并写入对象头。
- 调用G1FullGCPrepareTask准备压缩
- 如果任务没有空闲Region,则调用prepare_serial_compaction串行合并所有线程的最后一个分区,以避免OOM
G1FullGCPrepareTask
- 压缩对象具体逻辑在G1FullGCCompactionPoint中实现,执行完成后,对象头存储了对象的新地址
- 如果是大对象分区,且对象已经都死亡,则直接释放分区
2.2.3 调整指针
在上一步计算出所有活跃对象的新位置后,需要修改引用到新地址。
- 调整之前保存的轻量级锁和重量级锁对象的引用地址
- 调整弱根
- 调整全部根对象
- 处理字符串去重逻辑
- 一个region一个region的调整引用地址
2.2.4 移动对象
对象的新地址和引用都已经更新,现在需要把对象移动到新位置
- 具体压缩对象逻辑在G1FullGCCompactTask
- 如果phase2计算位置中使用了串行处理,则移动对象时也要使用串行处理移动每任务最后一个分区的对象
G1FullGCCompactTask
- 迭代处理每个Region
- 调用闭包G1CompactRegionClosure的apply函数移动对象到Region头部
- 如果Region中的全部对象都已清理,则回收该Region