JVM G1 源码分析02 新生代回收 && 混合回收 && full gc

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

猜你喜欢

转载自blog.csdn.net/kuaipao19950507/article/details/106501149