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

大纲

1.FGC的一些前置处理

2.FGC的整体流程

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

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

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

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

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

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

1.FGC的一些前置处理

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

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

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

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

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

(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)标记存活对象

(2)计算对象的新地址

(3)更新引用对象的地址

(4)移动对象完成压缩

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

(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)优化方向是串行变并行

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

(3)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)标记存活对象的优化之并行化标记

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

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

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

(5)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)

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

(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)计算对象新位置后的引用更新操作

(2)移动对象完成压缩

(3)移动对象后的处理

(4)关于并行化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)如何产生字符串的冗余和重复问题

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

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

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

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

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

(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对象

后端技术栈的基础修养 文章被收录于专栏

详细介绍后端技术栈的基础内容,包括但不限于:MySQL原理和优化、Redis原理和应用、JVM和G1原理和优化、RocketMQ原理应用及源码、Kafka原理应用及源码、ElasticSearch原理应用及源码、JUC源码、Netty源码、zk源码、Dubbo源码、Spring源码、Spring Boot源码、SCA源码、分布式锁源码、分布式事务、分库分表和TiDB、大型商品系统、大型订单系统等

全部评论

相关推荐

投了一个小厂的后端基本没问java,忘了录音了,题目还记得的大概如下:自我介绍3分钟八股10-15分钟1.mysql的事务特性?如何保证事务?mvcc的实现原理?2.redolog和undolog的区别?缓存池上的数据什么时候会持久化到硬盘上?和redolog的联系?3.CAS机制解决超卖问题可能会出现什么问题,使用CAS会出现超卖吗,CAS的版本号机制,java中的CAS如何实现?(这里答的不好,似乎是想让我说unsafe的使用方法但是完全没用过,应该是用unsafe保证版本号的更新是原子的)4.spring中如何解决循环依赖?解决循环依赖的过程?为什么是三级缓存(这里答的不好,虽然答出来了用来实现代理但还是没背熟,问到了代理对象放在哪一级缓存,为什么要这么设置)5.sql的语法糖,批处理中如果id存在则修改不存在则插入,这个语法糖是什么(没答出来,面试官说是select duplicate,不是很了解)6.mysql什么情况会出现死锁,是什么导致的死锁,如何解除死锁(解除死锁没答出来,我说是回滚即可面试官说找到长时间没解决事务id然后删掉)mysql问的好多,答的也一般,技术问的很细,头一次面试还是不熟悉,希望下次答的更好吧两道算法题:中间忘了StringBuilder的用法问了一下面试官能不能查说可以查1.2384 最大回文数字2.1190 反转每对括号间的字串全A了反问公司上班时间,业务等
查看8道真题和解析
点赞 评论 收藏
分享
评论
3
1
分享
牛客网
牛客企业服务