写点什么

G1 原理—G1 垃圾回收过程之 Full GC

作者:EquatorCoco
  • 2025-01-14
    福建
  • 本文字数:10293 字

    阅读完需:约 34 分钟

1.FGC 的一些前置处理

 

(1)对象分配失败就会进入 FGC 的流程


一.对象分配的流程


在对象分配过程中:如果 TLAB 不够,就会从 Region 中扩展新的 TLAB。如果 Region 不够,就会从堆内存的自由分区拿空闲 Region。如果自由分区不够,就会扩展堆内存。如果堆内存扩展失败,就会进行 YGC + MGC 清理出空闲 Region。如果在 YGC + MGC 后,还是无法分配对象,就会进入 FGC。

 

所以对象分配的整体流程是:TLAB 分配 --> 扩展 TLAB 进行分配 --> 申请新的 TLAB --> 从自由分区获取新的 Region 给新生代--> 堆内存扩展分区给新生代 --> 垃圾回收(YGC + MGC) --> FGC(第一次) --> FGC(第二次回收软引用)

 

可见在 FGC 前,G1 会做一系列尝试去获取一块内存给系统程序分配对象,如果实在获取不了失败了才会进入 FGC。

 

二.FGC 前的尝试操作出现复制失败时的处理


如果 FGC 前的一系列操作过程只进行了一半,比如在进行 GC 时发现:一部分对象复制成功了,一部分找不到足够的空闲空间来复制了。此时想去扩展内存,但又扩展不到新的内存,应该怎么办?如下图示:



此时有些对象已经复制完成了,而有些对象还在准备复制,但又无法找到多余的空间进行复制。比如上图示的对象 O3,找不到多余的空间给它进行复制,那么这次的 GC 则意味着失败了。

 

对于已复制成功的对象,可以不用管它,因为接下来可对它们进行回收。但对于没有复制成功的对象,应该如何处理?

 

(2)对复制成功的对象更新 RSet


在 YGC 时,会先把存活对象复制到一个新分区。然后更新 RSet 引用关系,最后清理垃圾对象。

 

那么在上述场景里:对于部分已经复制成功的对象,可以考虑直接让它保持原状不变,即让这些已经复制成功的对象在新位置上继续保持原来的 RSet 引用关系。所以就需要把原来的 RSet 引用关系更新到新的位置上,并且标记这些已经复制成功的对象为 dummy 对象。

 

比如下图中所有复制成功的对象,都会被标记成 dummy 对象,然后接下来要更新这些复制成功的对象对应的卡表 + RSet 信息。更新完成后,这些复制成功的对象就可以正常使用。而它们原来所在位置的对象由于是 dummy 对象,那么在下次 GC 时就可以被识别到进行回收。



注意:被标记为 dummy 对象是因为其所在 Region 可能复制失败了,这个 Region 不能清空释放。

 

(3)对复制失败的对象进行恢复处理


对象的复制过程其实是一个相对比较复杂的过程。由于对象头里有一些地址信息等,而复制对象又会改变对象的地址,所以如果要复制一个对象,那么就需要改变这个对象的对象头。

 

而处理对象的对象头,会发生在复制对象之前。因此如果要复制一个对象,不管对象是否复制成功,对象头都会改变。如果对象复制失败了,此时对象头已经发生了改变。所以复制失败的对象需要恢复其对象头,否则这个对象就会有问题。

 

(4)如何恢复对象头(自引用指针)


首先,复制失败的对象不能被识别成 dummy 对象或者垃圾对象,所以复制失败的对象需要被对象引用。否则,如果复制失败的对象被识别成了 dummy 对象或者垃圾对象,那么这些复制失败的对象就会被回收掉了。

 

但又不能随便找其他对象去引用它,因为这个其他对象也可能会被回收。当这个其他对象被回收后,它也会成为垃圾对象,从而也会被回收掉。

 

所以 G1 设计了一个自引用指针,让复制失败的对象的指针指向它自己。当 G1 在任何一个过程发现一个对象的指针指向自己,就可以认为它是需要恢复对象头的,然后 G1 就可以去恢复这个对象的对象头。而 G1 在对象复制过程中会比较慎重,首先会把对象复制成功前的一些对象头信息专门保存起来。当需要恢复一个对象的对象头时,就能从保存的地方获取信息进行恢复。

 

总结:G1 会通过一个简单的方式(自引用方式)来标识一个对象需要恢复对象头。如果发现一个对象需要恢复对象头,就会找原来的对象头信息进行恢复。这个简单的方式(自引用的方式)就是:让对象的指针指向它自己。

 

(5)恢复对象头之后的其他恢复操作——Redirty 恢复 RSet


对象头恢复后,首先需要删除这个对象的自引用指针。然后因为一部分对象复制失败,一部分对象复制成功,所以此时要执行 Redirty 操作,重构整个 RSet,把 RSet 更新到最新状态。当复制成功的对象和复制失败的对象都恢复到正常状态后,就可以执行后续的 FGC 操作。


2.FGC 的整体流程


(1)标记存活对象


各种尝试失败后的 FGC 和 YGC、MGC 的区别?

 

在 YGC + MGC 的过程中,基本的思路都是:标记存活对象 -> 复制存活对象到空闲分区 -> 集中回收存在垃圾的 Region。

 

FGC 的过程与 YGC + MGC 则有很大不同。FGC 的第一步也会进入标记阶段,标记阶段会标记出所有的存活对象,这个过程和 YGC + MGC 没有什么太大的区别。本质都是标记对象,然后遍历对象的所有 Field,最终找到所有存活对象。为了找到所有存活对象,也使用三色标记法 + SATB + 写屏障等。

 

FGC 的标记阶段如下图示:



(2)计算存活对象的新地址


G1 在 FGC 第一步标记完所有存活对象后,会对每个 Region 进行整体遍历。对 Region 的整体遍历会从一个 Region 的底部(也就是起始位置)开始,然后会在 Region 的底部(也就是起始位置)设置一个 compact top 指针,也就是 compact top 指针会指向 Region 的起始位置。

 

在对 Region 的整体遍历的过程中,如果找到第一个存活对象:就把该存活对象的对象头里指向对象地址位置的指针设置为 compact top。

 

因为这是遍历到的第一个存活对象,说明前面遍历的对象都是垃圾对象,所以提前把这个存活对象规划到接下来要被回收的那块区域的起始位置,后面找到的存活对象以此类推。

 

计算存活对象的新地址也可以理解成:计算出每个存活对象在垃圾回收后,在所在 Region 的新位置。

 

如下图示:因为在垃圾回收之前,Region 的起始位置的对象是垃圾对象。而在垃圾回收后,Region 的起始位置的对象就会被清理掉。所以 Obj0 对象在垃圾回收后的位置,就是 Region 的起始位置。



注意:此时还没完成对象的复制,也没完成垃圾对象的清理,此时只是做了对象新地址的计算。

 

(3)更新存活对象之间的引用地址


一个对象的对象头里会有一个引用对象。这个引用对象会引用该对象,这个引用对象里会有一个地址,这个地址就是该对象的内存起始地址。

 

上一步已把对象头中引用自己的对象的一个地址,更新为一个计算后的新地址了。接下来就需要把所有的存活对象遍历一遍,把对象间的引用地址,也指向到新位置上。也就是遍历所有存活对象以及存活对象的字段,然后把所有对象之间的引用地址,更新到新位置上。

 

注意:此过程更新的是存活对象引用存活对象的新位置。除了对象复制,对象的新位置、对象间的引用新位置,都已处理好了。

 

(4)移动对象完成压缩


在更新完对象之间的引用地址后,接着就要把存活对象移动到新位置上。这会起到释放垃圾对象占用的空间、让存活对象排列更加紧密的作用,所以 FGC 使用的是整理压缩算法,而不是复制算法。



需要注意:遍历整个 Region 的过程,是从前向后遍历的,这个遍历过程和给存活对象计算新地址的过程是一致的。

 

(5)移动对象后的处理(调整堆分区 + 重构 RSet + 清除 DCQ + 更新新生代)


可以看到,完成这四步之后:标记存活对象 + 计算对象新地址 + 更新引用对象地址 + 移动对象完成压缩,垃圾对象已经完成回收,存活对象在各自的 Region 也实现了紧密排列。

 

注意,这个过程并不一定能整理出空闲的 Region,因为存活的对象都是在各自的 Region。而 YGC 和 MGC 都是使用复制算法把存活对象复制到一块空闲 Region,然后集中回收那些已经复制完成的存活对象原先所在的 Region 的。

 

到了 FGC 这一步,就说明已经没有多余的 Region 可提供使用,所以 FGC 只能对每个 Region 进行压缩整理。

 

FGC 在完成压缩整理后,会做如下调整操作:


一.尝试调整整个堆分区的数量大小

二.遍历整个堆,然后重构 RSet,因为对象的位置已经发生了改变

三.清除 DCQ,并把所有分区设置为老年代分区

四.记录一些 GC 信息,同时更新新生代大小(YGC CSet 的大小)

 

其中更新新生代也就是选择一些老年代分区作为新生代分区,然后重新构建 Eden 分区,对一些 Region 分区打上 Eden 分区的标识。以便可以支持下一次的对象分配,以及 YGC 等各种操作。注意:上面的这些调整操作的整个过程都是单线程执行的。


3.传统 FGC 只能串行化 + G1 的 FGC 可以并发化


(1)优化方向是串行变并行


一.G1 的 FGC 在 JDK 版本演进方向中的优化


在 JDK10 之前,FGC 的整体处理流程都是串行回收。即单线程串行执行,最终回收垃圾对象,同时把存活对象进行压缩整理。在 JDK10 后,G1 因为分区这个结构的存在,让并行化 FGC 有了一些可能,比如标记对象时就可以并行标记。

 

二.为什么标记对象可以并行处理


在 Mixed GC 时,会有多个线程并行处理 GC Roots。并且因为 RSet 的存在,对于 GC 线程来说:并不需要遍历完所有引用了当前 Region 的 GC Roots,才能把当前 Region 的存活对象标记出来。由于 RSet 本身存储了外界对当前 Region 的引用关系,只要结合 GC Roots + RSet 来遍历当前 Region 就能完成其存活对象的标记。

 

Mixed GC 的并发标记阶段:会根据"Survivor + GC Roots 直接引用的老年代对象 + RSet"来进行标记,从而完成对整个堆内存的对象标记。如下图示:



经过并发标记阶段和重新标记阶段之后,所有的对象都会标记成白色或者黑色。最终白色对象被回收,黑色对象被集中复制到一个新的分区里。经过 Mixed GC 的多次回收后,最终的状态可能如下图示,整个过程其实就是并发的过程。



Mixed GC 的并发标记过程为什么可以并发?


由于并发标记是从 GC Roots 出发,遍历全部存活对象。所以多个线程可以从多个 GC Roots 出发遍历就能完成全部引用链的标记。

 

(2)传统的 FGC 为什么要串行化


原因一:要计算新对象的位置


如果使用多线程去遍历,计算对象新位置,就很容易出现位置冲突。出现冲突就要引入一些额外的机制去解决冲突,导致性能可能会更差。

 

原因二:要压缩整理整个堆内存,更新所有对象的引用关系


如果一个对象在多个 GC Roots 的引用链上,那就会出现位置冲突、或引用更新不全的情况。

 

所以传统的分代模型,如果要并行进行 FGC,则可能会出现很多冲突。如下图示,如果两个线程同时对 Obj3 和 Obj1 两个对象做遍历,那么 Obj1 对象和 Obj0 对象的位置就不太好处理(起始位置可能会冲突)。如果要设计一套复杂的机制处理冲突,还不如单线程串行化处理更加高效。当 JVM 堆内存是一整块的时候,更容易发生这些冲突情况。



(3)G1 本身有什么优势可支持 FGC 的并行处理


首先 G1 本身的分区就是一个相对独立的内存区域。其次每个 Region 都有一个 RSet,根据 GC Roots + RSet 就能完整地标记某个 Region 的所有存活对象。

 

上面的两个条件,是 G1 得天独厚的优势。可以利用分区机制,让一个线程对一部分分区进行标记,而这部分分区只需要找到 GC Roots + RSet 即可完成整个 Region 的标记。

 

如下图示,GC Roots 引用了 Obj 这个对象,而 Obj3 这个对象对 Obj0 的引用又记录在这个 Region 对应的 RSet 中,所以不需要找到 Obj3 这个 GC Roots 就可对 Obj0 做好标记,判断是否存活。所以即使有多个线程对多个分区同时进行处理,也得到一组正确的结果。因此,G1 本身的分区机制 + RSet,天然就支持 FGC 并行处理。



如下图示:最终存活对象 Obj、Obj1、Obj3、Obj4 都能按照规则正常到达自己的位置。



4.并行化 FGC 之并行标记 + 任务窃取


(1)标记存活对象的优化之并行化标记


G1 的 Region 分区机制 + RSet 机制,天然就支持并行处理。所以多个线程可以各自负责一些分区的标记、整理、压缩过程。比如一个线程负责 100 个 Region 的整理工作,当这个线程整理完成后继续找 100 个 Region 进行处理。

 

这样每个线程处理其负责的某个 Region 时,不需要找太多的东西就可以完成标记、压缩整理的工作。而且多个线程之间也不会发生冲突,所以整体效率肯定比单线程串行化处理要高很多。如下图示:



(2)FGC 并行标记存活对象开始前的工作


在 FGC 开始前还要做一些准备操作,即前置工作。比如将对象头、锁、GC 标记等信息进行保存处理。

 

每一个对象都是有对象头的,对象头里保存了对象的位置信息、锁信息、GC 标识等信息。FGC 要移动对象,所以对象头的这些信息需要预先保存起来,因为这些信息对于对象恢复、数据恢复是很重要的。



在保存完一些对象头相关信息后,就要开始 FGC 了。具体步骤和串行化的 FGC 是类似的:

一.标记存活对象

二.计算对象的新地址

三.更新对象间的引用地址

四.移动对象完成压缩

五.对象移动后的后续处理

 

(3)FGC 并行标记开始时 STW + 分配线程任务栈


FGC 变成并行后,并行标记存活对象和串行标记存活对象差别不大,因为都是要标记出 Region 内的所有存活对象。

 

需要注意:并行标记时每个线程都会有一个任务栈来进行标记处理,串行标记时就不会有任务栈。FGC 的并行标记会把 GC Roots 对象分成多份,每个 GC 线程持有一部分。



(4)FGC 并行标记和 Mixed GC 并发标记的区别


这个 FGC 的并行标记和 Mixed GC 的并发标记,并不是一个概念。FGC 的并行标记,指多个线程可以并行的执行标记任务,不会互相影响。Mixed GC 的并发标记,指 GC 标记过程可以和系统程序同时运行。在 FGC 的并行标记过程中,系统程序是会 STW 的。

 

Mixed GC 在并发标记时,会有一个非常重要的 SATB 及 SATB 队列。在 FGC 里其实就不需要 SATB 和 SATB 队列了,因为 FGC 在并行标记时会 STW 系统程序,所以不会出现标记前后对象标记不一致的问题。

 

Mixed GC 会出现标记前后不一致的两种情况:

情况一.程序运行和 GC 标记同时进行造成不一致(如给黑色对象添加白色引用)。

情况二.并发标记过程可能被中断然后再次进入造成的不一致,如中断后到再次进入前如果有新的引用关系出现可能会导致标记不一致。

 

(5)FGC 在并行标记过程中的任务窃取


因为 STW + 不同线程是针对不同分区进行标记,所以 FGC 的标记过程不存在正确性问题,但是效率上可能会存在问题。

 

比如:一个线程分配的 Region,因引用关系简单存活对象少,易标记速度很快。一个线程分配的 Region,因引用关系复杂存活对象多,难标记速度很慢。所以此时就需要做一些平衡操作了,也就是任务窃取。

 

如图所示:此时线程 1 对分配给它的所有 GC Roots 对象都已经处理完了(标为红色),而线程 2 对分配给它的第一个 GC Roots 对象都还没有处理完。



这时为了整体的性能,线程 1 就会从线程 2 那里窃取一些任务,这样就能尽可能保证所有线程都一直处于工作状态。即使有空闲的线程,也会去窃取任务来执行。如果无法窃取,那么就说明标记工作已经到了尾声,这样的处理能够充分发挥多核 CPU 的性能。如下图示:


 

5.并行化 FGC 之跨分区压缩 + 避免对象跨分区


(1)计算对象新地址可以做哪些改进(跨 Region 压缩腾出完全空闲 Region)


在 FGC 的标记存活对象的过程中,为了优化性能:G1 采取了多线程并行处理 + 通过任务窃取的方式保证多线程执行的效率。那么在计算对象新地址的过程中,G1 又会做哪些优化?

 

一.整理压缩算法的弊端


如果 FGC 只是完成对 Region 的压缩回收整理,没腾出完整的空闲 Region。那么系统程序在 FGC 后要用一个空闲 Region,就只能进行扩展堆内存了。

 

如下图示:FGC 后,存活对象在 Region 内完成压缩处理,却没有完整的空闲 Region。假如此时需要分配一个大对象:那么由于没有一个完全空闲的 Region,此时是会无法分配的。



二.如何解决 FGC 后没有空闲 Region 的问题


要解决这个问题:要么直接跨 Region 的进行压缩,把存活对象集中放到一个 Region 中。要么就只能扩展内存,扩展出一个新的 Region 出来。

 

串行化遍历 Region 处理的弊端:


因为串行化只能一个个 Region 遍历去处理,所以难以将全部 Region 的存活对象都集中复制到其中一些 Region 中,从而腾空出完全空闲的 Region。

 

并行化处理多个 Region 时跨 Region 压缩尝试:


根据 FGC 并行标记的特点,由于一个线程会对多个 Region 处理,且这些 Region 不会被其他线程干扰。所以可尝试把对象集中压缩到其中一些 Region,从而腾出空闲 Region。

 

如下图示:线程 1 处理了 3 个分区,此时存活的对象可能只有 2 个,那么完全可以用一个分区去存放这 2 个存活对象(标为红色)。



经过新地址计算,对象的位置被确定在第一个分区的头部位置。注意:此时对象还没有移动到新位置,只是计算出了新位置,并且把对象头里对这个对象的引用修改到了新位置。(对象头里对这个对象的引用可理解为对象头中存储该对象的位置信息)



(2)如何避免普通对象压缩整理时出现跨分区存放


在计算新位置的过程中,因为可以把对象位置定位到其他分区。所以可能出现:一个分区剩余的内存空间不足以放下另外一个存活对象。如下图示,第一个 Region 此时还剩下一些空间,比如 1K 的空间。



此时又有一个存活对象要计算新位置,但是新对象是 2K,应怎么存放?肯定不能跨分区存放,因为在 G1 里只有大对象才能跨分区存放,其他对象都不允许跨分区存放。此时 2K 的这个对象想要尝试放到第一个 Region 里肯定是不会成功的,所以只能进入第二个 Region 中,并且这个 2K 的对象的起始位置,就是第二个 Region 内存的开始地址。



所以这个过程中,G1 引入了一个组件,叫 G1FullGCCompactionPoint。它用来记录某个 GC 线程在计算对象位置时所使用的分区情况,比如用了哪个分区,用到了哪个位置。如果要为对象计算新位置,那么就可以通过该组件进行判断:应该放到哪个分区哪个位置,能不能放得下。

 

整个计算对象新地址的过程处理完成后,所有的对象的对象头存储的就是对象的新位置了。

 

注意:此时只是计算出对象需要存储的位置,还没把对象复制到对应的位置上。

 

6.并行化 FGC 之更新引用位置 + 移动对象处理


(1)计算对象新位置后的引用更新操作


前面一系列操作完成后,就需要把对象引用更新到新位置上去了。此时对象头已指向新位置了,于是需要把所有的存活对象遍历一遍。然后把存活对象间的引用,也指向到新位置上去。即遍历所有存活对象以及存活对象的字段,然后把所有存活对象间的引用,更新到最新的位置上。

 

此过程更新的是存活对象引用的存活对象的新位置,完成该过程后,除了对象复制的工作之外,对象的新位置和对象间的引用位置都已经处理好了。

 

假如一个 Student 对象引用了 score 对象,socre 对象的新位置已确定了,如下图示:



在更新了引用之后,Student 引用的就是新位置的对象,如下图示:



然后下一步就会进行压缩处理:也就是把所有的存活对象都复制到新位置,然后把垃圾对象和复制后的旧对象全部清理掉。

 

(2)移动对象完成压缩


完成并发标记存活对象+计算存活对象新地址+更新存活对象引用地址后:所有存活对象的新位置已确定、所有存活对象的引用也更新到最新位置。

 

那么接下来就会进入到移动对象、压缩空间的操作:把对象给复制到新位置,然后把复制后的老对象以及垃圾对象全部回收掉。



经历了前面的完整操作后,就有可能会空闲出一些完整的空闲 Region。当然,此时 FGC 还没有完全结束,因为还要进行移动对象后的善后处理。

 

(3)移动对象后的处理


可以看到,完成前面步骤后,其实垃圾对象就已经完成了回收。由于并行化的操作会进行一些优化,所以 FGC 的整体效率会得到提升,而且完全有可能整理出一些完全空闲的 Region 给程序使用。

 

当完成复制回收工作后,G1 会进行如下操作:

一.恢复对象头

二.遍历整个堆,然后重构 RSet,因为对象的位置已经发生了改变

三.清除 DCQ,并把所有分区设置为老年代分区

四.记录一些 GC 信息,同时更新新生代大小(YGC CSet 的大小)

也就是选择一些老年代分区作为新生代分区,然后重新构建 Eden 分区。具体来说会对一些分区打上 Eden 分区的标识,以便可以支持下一次的对象分配,以及 YGC 等各种操作。

 

注意:FGC 结束后,会把所有分区标记成 Old,然后再重新选择一些 Region 成为 Eden 区。


7.G1 新特性之字符串去重优化

 

(1)如何产生字符串的冗余和重复问题


在早期的 JVM 中,对于字符串的使用,其实是比较被动的。系统程序经常会创建字符串类型(String)的对象,而大量的创建就有可能出现同样的字符串存在多个不同的实例。如下图示:



方法栈中 a,b 两个局部变量是不同的变量。a != b 是因为引用地址不同,但是 a.equals(b)的结果却是 true。这就会在堆内存里有两份相同的字符串"abc"实例对象,占用着两份空间。

 

JDK 虽然提供了 String.intren()方法以及字符串常量池来解决这个问题,但是 String.intren()方法需要我们找出哪些字符串需要复用,所以不太方便。字符串常量池主要应对 String a = "abc"和 String b = "abc"这种情况。在这种情况下,会将"abc"放字符串常量池。

 

大量重复字符串实例会占用额外内存,所以急需一种方式来该解决问题,这种方式就是字符串去重。

 

(2)什么是字符串去重 + 通过 char 数组是否一致来去重


字符串去重的意思就是:假如多个变量引用的字符串对象的值是相同的,那么就让这多个变量共享这个字符串对象。这样就能大大节省因为 String 对象的使用而造成的内存浪费,如下图示:



Java 7 开始,每个 String 都会有一个自己的 char 数组,而且是私有的。这个私有的 char 数组,在 JVM 的底层中就支持了这种去重操作。由于每个字符串数组,都是 String 对象自己持有的一个私有的 char 数组,并且 Java 代码本身非常慎重的没有对 char 数组做任何改动,基于此,JVM 就可以完成优化。如果 new 一个 String,里面的 char 数组是没有接口可以修改的,而且是私有的。

 

JVM 具体的优化方法是,判断这两个 char 数组是否一致。如果一致,那么就可以考虑把两个 char 数组给不同的字符串来共享使用。如果一致,则说明可以共享,经过判断后就去掉一个冗余的重复字符串。



(3)使用 G1 时发生字符串去重的 YGC 阶段和 FGC 阶段


字符串去重的这个特性是从 Java 8 的一次更新中引入的。在 G1 中,去重的操作主要发生在两个阶段:第一个阶段是 YGC 阶段,第二个阶段是 FGC 的标记阶段。

 

为什么是这两个阶段?因为这两个阶段会对整个 CSet 区域做垃圾回收,同时 YGC 会对整个新生代做扫描,FGC 会对整个堆内存做压缩。

 

在这两个阶段中:


一.YGC 是经常发生的,在这个阶段需要对一些存活对象做复制操作,所以 YGC 适合做字符串去重操作。

二.在 FGC 的标记阶段,会做大量的计算对象新位置和逻辑压缩的工作,所以在 FGC 的标记阶段中,就完全可以进行 String 字符串的去重操作。



(4)字符串去重第一步是筛选需要去重的 String 对象


如何找到需要去重的 String 对象呢?由于去重操作会发生在 YGC 阶段或者是 FGC 的标记阶段,所以可以在这两个阶段中对 String 对象进行判断看看是否需要去重。注意:去重操作不是发生在 String 对象的创建阶段。

 

一.如果在 YGC 阶段会通过如下条件判断出哪些字符串需要进行去重


条件一:假如字符串是要复制到 S 区的,则判断它的年龄是否超过年龄阈值。如果字符串对象的年龄大于等于年龄阈值,则参与去重,否则不参与。

StringDeduplicationAgeThredshold 参数可以控制这个年龄阈值。为什么这么判断?因为大量字符串其实很快就不用了、变成垃圾对象、直接被清理掉。所以没必要浪费时间、浪费 CPU 去做一次去重处理。

 

条件二:假如字符串是要晋升到老年代分区,则判断它的年龄是否小于年龄阈值。如果字符串对象的年龄小于年龄阈值,则去重,否则不去重。

这个条件和上一个条件结合起来看,逻辑是比较严密的。在老年代的字符串对象如果年龄大于阈值那么肯定不需要去重了,因为年龄大于阈值说明该对象在 YGC 已去重过。所以在老年代的字符串对象不会出现年龄大于阈值都还没参加过去重的,故此时只需判断字符串对象年龄是否小于阈值。

 

在老年代中年龄小于阈值的、而且还没去过重的字符串对象,可能会是一些大对象或动态年龄判断规则触发晋升到老年代的对象,它们确实容易逃过被复制到 S 区而没经历条件一的判断。

 

按照上面两个条件,就可以保证在 YGC 阶段:把在新生代区域和晋升到老年代区域的字符串进行去重处理。

 

二.如果在 FGC 阶段只需要考虑字符串对象年龄是否小于阈值


这个阶段只需考虑字符串对象的年龄是否小于阈值即可。因为在 FGC 完成后,所有的分区都会被标记成老年代分区,所以可理解成所有对象都要晋升至老年代。

 

(5)字符串去重第二步是对需要去重的 String 对象处理


找到所有需要去重的字符串后,G1 会把这些字符串加入到一个队列中进行去重处理。



把字符串加入到待去重队列后,G1 就会开启一个后台线程完成去重操作。去重操作首先会判断一个字符串是否存在。如果不存在,那么就创建一组键值对,加入到一个 HahsTable 中。如果已存在,那么就把 String 变量的引用指向该 HashTable 对应字符串的指针。如下图示:



(6)回收被当作垃圾对象的 String 对象


当发生 GC 时,会尝试对去重后的字符串对象进行回收。回收的时机和去重的时机是一致的,还是在 YGC 或者 FGC 的时候。例如,当 Java 代码里面执行了:


String a = new String("abc");String b = new String("abc");
复制代码


那么在 YGC 或 FGC 时就有可能发生去重。去重后,其中一个字符串对象会成为垃圾对象,如下图示。GC 后就会把垃圾对象回收掉,只剩下 HahsTable 中的一个字符串给多个变量引用,以此来节省空间。



据官网数据表明,经过这样的去重操作后,能节省大约 13%的内存使用。所以在内存使用上,是一个非常大的提升。


8.总结 G1 对 FGC 的优化处理


G1 的并行化 FGC 的处理及优化:


一.优化标记存活对象之并行化标记(Region 分区机制 + RSet 机制支持并行)

二.并行标记的过程(标记存活 + 计算新地址 + 更新引用地址 + 移动对象等)

三.FGC 并行标记存活对象的前置工作(保存对象头)

四.FGC 并行标记开始时 STW + 分配给每个线程一个 GC Roots 任务栈

五.FGC 并行标记和 Mixed GC 并发标记的区别(STW + 不 STW 导致的不一致)

六.FGC 在并行标记过程中使用任务窃取,提升整体处理效率

七.一个线程在处理多个 Region 时会进行跨 Region 压缩以产生空闲 Region

八.使用一个组件避免普通对象压缩整理过程中出现跨分区存放

九.计算对象新位置后的引用更新操作

十.移动对象完成压缩整理

十一.移动对象后的处理

 

G1 的新特性—字符串去重优化:


一.如何产生字符串的冗余和重复问题

二.什么是字符串去重 + 通过 char 数组是否一致来去重

三.使用 G1 时发生字符串去重的 YGC 阶段和 FGC 阶段

四.字符串去重第一步是筛选需要去重的 String 对象

五.字符串去重第二步是对需要去重的 String 对象处理

六.回收被当作垃圾对象的 String 对象


文章转载自:东阳马生架构

原文链接:https://www.cnblogs.com/mjunz/p/18669453

体验地址:http://www.jnpfsoft.com/?from=001YH

用户头像

EquatorCoco

关注

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
G1原理—G1垃圾回收过程之Full GC_Java_EquatorCoco_InfoQ写作社区