G1原理—3.G1是如何提升垃圾回收效率

news/2025/1/10 21:33:37/文章来源:https://www.cnblogs.com/mjunz/p/18664751

大纲

1.G1为了提升GC的效率设计了哪些核心机制

2.G1中的记忆集是什么

3.G1中的位图和卡表

4.记忆集和卡表有什么关系

5.RSet记忆集是怎么更新的

6.DCQ机制的底层原理是怎样的

7.DCQS机制及GC线程对DCQ的处理

 

提升G1垃圾回收器GC效率的黑科技

G1设计了一套TLAB机制 + 快速分配机制用来提升分配对象的效率

G1设计了一套记忆集 + 位图 + 卡表 + DCQ + DCQS机制用来提升垃圾回收的效率

 

1.G1为了提升GC的效率设计了哪些核心机制

(1)记忆集RSet(Remember Set)

(2)位图BitMap

(3)卡表CardTable

(4)这些数据结构是怎么提升GC效率的

 

(1)记忆集RSet(Remember Set)

一.可达性分析算法在分代模型下的问题

JVM中判断对象是否存活的算法是GC Roots可达性分析算法,即从GC Roots出发标记所有存活对象。然后遍历对象的每个字段继续标记,直到所有对象标记完毕。标记完毕后,一个对象要么是存活对象,要么是垃圾对象。

 

在分代模型的JVM GC中,老年代和新生代的回收,一般都是不同的。新生代会被先触发,实在无法腾出足够空间,才会进行老年代GC或FGC。

 

G1中的垃圾回收会分成三种:新生代回收、混合回收和FGC。新生代回收总是收集所有新生代分区,混合回收会收集所有新生代分区和部分老年代分区,FGC则是会对所有分区进行处理。

 

G1也采用分代模型:首先进行新生代回收,然后不够再进行Mixed GC,最后才可能会FGC。

 

问题:回收新生代时为什么需要遍历老年代?

因为新生代对象不一定只有新生代对象在引用,老年代对象也可能会引用新生代对象。在触发新生代GC时,老年代里的一些对象也会在新生代的GC Roots中。所以回收新生代时,根据可达性算法,遍历老年代就是为了完善GC Roots。

 

而这又会面临新的问题:遍历跨代的所有对象浪费时间。由于新生代和老年代GC的阶段是不同的,当只需要回收新生代时,如果还是按照可达性分析算法,那么就会把老年代也都全标记一遍,而此时并不回收老年代。同样如果只需要回收老年代,也要把新生代的所有对象遍历一遍,这样就非常浪费时间。

 

如何才能拿到老年代中引用新生代的那些对象呢?

 

二.记忆集是针对分代模型垃圾回收设计的用于解决跨代引用的数据结构

如何找出引用了新生代对象的所有老年代对象?难道只能遍历老年代?RSet记忆集,就是为了解决这个问题而设计的。RSet记忆集,会使用一组key-value结构来记录跨代引用的引用关系。这样在GC时就可以借助GC Roots + 记忆集,快速解决同代引用及跨代引用的可达性分析问题。如下图示:

(2)位图BitMap

一.JVM应该如何标记内存的使用状态

由于JVM管理的是内存,而内存的使用状态,其实是不太好标记的。为了标记JVM中的内存使用状态,最笨的方法就是:直接遍历整个内存块,看看它到底有没有内容。没有内容就认为它是空闲的,有内容就认为它是在使用中的。

 

但这种简单粗暴、直接遍历内存查看内存是否被使用的方式不适合JVM,如果JVM想查看哪些内存被使用,还要去遍历内存,这样效率就太低了。

 

二.JVM使用位图来标记内存的使用状态

为了描述内存的使用状态,JVM使用了位图,也就是JVM会在一个位图里记录所有内存的使用状态。如果JVM要要知道某一块内存是否被使用,就直接访问位图中这块内存对应的坐标内容来进行判断,看是否已使用。如下图示:

三.什么是位图

位图就是通过"位"来描述某一块内存的状态的图。计算机存储数据的最小单位是字节,一个字节占用了8个二进制位,每个二进制位的值是0或1。位图就是一组二进制位数据,位图的每个二进制位的0和1都标识其描述内容的状态,如内存是否使用。

 

G1便使用了位图来描述内存的使用状态,G1会在混合回收的并发标记阶段使用位图,以此来快速判断内存是否已被使用。

 

(3)卡表CardTable

一.卡表与位图的区别

卡表类似于位图,都是通过使用一段数据去描述一块内存的情况。

 

卡表和位图不一样的地方是:位图只能使用一个二进制位来描述内存的状态,也就是只能记录使用或未使用。卡表则可以描述更多的信息,比如内存是否使用、内存的引用关系等。卡表使用8个二进制位也就是一个字节来描述一块内存的使用情况。比如内存是否使用、使用了多少内存、内存的引用关系等。

 

所以,本质上卡表在数据结构层面和位图其实并没有什么太大区别,只不过卡表的描述符比位图长、描述的内容比位图多而已。

 

二.G1的卡表

G1的卡表会用1个字节(8位)来描述512字节的内存使用情况以及对象的引用关系,并且G1的卡表是一个全局卡表,也就是整个堆内存会共用一个卡表。G1会使用一个全局卡表来描述整个堆内存的使用情况以及对象的引用关系。

 

由于512字节的内存可能会被引用多次,即同一个对象被多个对象引用。所以卡表的描述,可以理解为一个大概的引用关系描述,卡表并不记录每个对象的具体引用关系。

 

G1使用了记忆集 + 卡表,来解决分代模型中,跨代引用关系的不好判定、不好追踪问题。

(4)这些数据结构是怎么提升GC效率的

一.标记过程很影响GC效率 + 记忆集可避免跨代遍历提升标记过程效率

JVM的垃圾回收效率主要体现在以下两方面:

第一.标记垃圾对象

第二.清理垃圾 + 整理内存

 

其中标记过程会分成多个步骤:初始标记、并发标记、重新标记、并发回收。可见标记过程是非常耗时的,不然没有必要分成这么多个阶段。

 

那么标记过程的耗时具体体现在哪里呢?其实标记过程耗时就是体现在引用关系不好判断、内存是否使用不好判断上面。

 

首先,初始标记要从GC Roots出发,标记所有被直接引用的对象。然后,并发标记要追踪所有被间接引用的对象。如果出现新生代对象被老年代引用了,通过RSet就可以避免对老年代的遍历。

 

二.为节约记忆集的内存会使用卡表

引用关系会通过记忆集RSet来进行查找,但是记忆集Rset里不能直接记录哪个对象引用了哪个对象。否则如果一个系统1000w个对象,引用关系还特别复杂,那么一个Rset可能占用的内存就会特别大。

 

所以这个时候卡表CardTable就出现了,卡表CardTable里可以用1个字节来描述512字节内存的引用关系。这样RSet里就可以只记录CardTable相关内容,这样就能节省很多内存。比如发现老年代有一块512字节的空间里的对象引用了新生代的一个对象,那么记忆集Rset只需要直接记录这个512字节的空间在卡表里的位置即可。

 

三.并发标记阶段会使用位图

位图和并发标记阶段是息息相关的。在并发标记阶段,可以借助位图描述内存使用情况,避免内存使用冲突,也避免GC线程无效遍历一些未使用的内存。

 

(5)总结

G1为了提升GC标记效率做了哪些设计:

一.记忆集RSet—避免跨代遍历对象

二.位图BitMap—描述内存使用(避免并发冲突 + 避免无效遍历未使用内存)

三.卡表CardTable—节约记忆集的内存(8个位1个字节描述512字节的引用关系)

 

问题:Rset到底是什么?它具体是怎么实现的?它里面会记录什么内容?

 

2.G1中的记忆集是什么

(1)怎么标记一个对象

(2)跨代引用下存在存活对象找不全的问题

(3)怎么记录跨代的引用关系—RSet记忆集

(4)在G1中有多少种引用关系 + 哪些需要记录

 

(1)怎么标记一个对象

如何判断一个对象需要被标记为垃圾对象,或者需要被标记为存活对象?其实就是通过可达性分析算法来判断对象是否存活,也就是通过追踪GC Roots引用关系来标记。如果一个对象被GC Roots引用或者被GC Roots引用的对象引用了,那么就标记为存活对象,否则就标记为垃圾对象。

 

那么如何知道引用关系呢?最简单最准确无误的方式就是,从GC Roots 出发,一个一个对象去遍历。把每个对象的每个字段引用的内容都进行遍历,找出所有引用关系。

但如果出现了跨代引用则会有问题,老年代的一个对象引用了新生代,那么用新生代的GC Roots不能找全。这时老年代的一些对象,也要加入到新生代的GC Roots里面,才能找全。

 

(2)跨代引用下存在存活对象找不全的问题

由于G1本身是有分代的,在某种特殊情况下,有一个老年代的对象引用了新生代的对象。那么此时如果要触发G1的YGC,怎么才能找到这个老年代的对象呢?如果找不到这个老年代的对象,就没办法找到它引用了哪些新生代对象。

 

由于YGC针对的是整个新生代的空间,也就是会选择所有新生代Region,拿到GC Roots,然后遍历整个新生代。所以如果找不到老年代对新生代的引用关系,垃圾回收时就可能误操作。要么多清理要么少清理,不管是多清理或者少清理,其实都比较麻烦。如果多清理了,系统就会直接报错。如果少清理了,垃圾对象占用新生代,可能会更加频繁GC。所以这个跨代引用关系是必须知道的。

 

应该怎么获取这个跨代引用关系呢?想要获取这些引用关系,那么就要找到哪些老年代的对象引用了新生代的对象,也就是要找到老年代里引用了新生代对象的那些对象。

 

最简单的方式就是直接把老年代也遍历一遍来看看引用关系。但是此时做的是新生代的回收,却要把老年代也遍历一遍,就不合适了。不仅标记时间长,且遍历老年代,从分代隔离回收的思路来看也不合适。那么应该怎么记录跨代的引用关系?

(3)怎么记录跨代的引用关系—RSet记忆集

一.记忆集会通过记录跨代的引用关系来避免遍历整个分代如老年代

举个例子:在G1中,如果老年代的对象引用了新生代的对象,那么直接针对被引用对象开辟一块内存,用来存储到底是谁引用了该对象。

当准备遍历新生代所有对象时,直接把这块内存里的老年代对象,也加入到GC Roots中,然后进行遍历。这样就能避免遍历整个老年代,而且从效率和分代隔离角度都非常合理。

二.记忆集的维度应该是什么

是针对新生代和老年代各创建一个?还是针对Region,每个Region都创建一个?

 

G1会为每个Region都创建一个RSet记忆集,G1的每个RSet记忆集都会用来存储对应Region里所有对象的引用关系。

 

G1针对Region来创建记忆集的原因如下:

原因一.老年代、新生代、大对象的Region在GC后可能会发生变化。

原因二.如果针对每个分代创建记忆集,因为Region不断变化可能有并发问题。

原因三.如果针对每个分代创建记忆集,老年代回收时会有额外遍历降低效率。

 

除了新生代的回收是需要选择所有新生代的Region之外,老年代回收需要寻找性价比高的Region来回收,即选择一部分去回收。这时如果还要遍历整个老年代的记忆集来筛选引用关系,效率就会很低。

总结:

G1选择RSet记忆集去记录跨代的引用关系,大大减少了不必要的遍历操作。

G1会针对Region去创建RSet记忆集,这也继续减少了不必要的遍历操作。

 

(4)在G1中有多少种引用关系 + 哪些需要记录

一.Region分区内部的引用关系

二.新生代Region到新生代Region有引用关系

三.新生代Region到老年代Region有引用关系

四.老年代Region到新生代Region有引用关系

五.老年代Region到老年代Region有引用关系

 

此时需要考虑如下问题:

G1的引用关系一共就这么几种,那么到底哪些需要记录?如果不记录,会不会造成漏标记或漏回收的情况?如果会出现这个现象,那么就必须要记录下来了。所以针对上述的几种情况,进行如下分析:

 

一.Region内部的引用关系

因为是内部的,不管是做新生代回收、老年代回收、还是FGC回收,在GC Roots标记的过程中,是一定可以追踪到这种引用关系的。因为被选中的Region分区,它里面的所有对象一定都会被遍历到。所以Region内部的引用关系,不需要记录;

 

二.新生代和新生代间的引用关系

这个和Region内部的原理是一样的。新生代GC时肯定可以全部遍历一遍,所以也不需要记录,老年代GC又和这种新生代之间的引用关系没什么相关联的地方。

 

三.新生代到老年代的引用关系

假如要进行新生代的回收,因为是新生代引用了老年代对象,所以即便遍历到了,新生代GC也不会去处理老年代的对象,并且这种引用关系在新生代回收过程中,也一定会被遍历到。

 

假如要进行老年代的回收,老年代Region需要知道谁引用了自己,不然可能会出错。但是我们知道GC回收过程是没有老年代的单独回收的,所以如果要回收老年代,肯定会带着一次新生代回收或者直接FGC的,所以也不需要记录。

 

四.老年代到新生代的引用关系

这个很明显是需要记录的。

 

五.老年代到老年代的引用关系

假如进入到Mixed回收阶段,那么此时只会选择老年代的部分Region来回收。由于只需选择部分Region进行回收,所以其他Region尽量就不要去遍历。因此需要记录老年代到老年代的引用,避免Mixed回收时遍历老年代。

 

(5)总结

G1中的记忆集:

一.记忆集是什么

二.标记对象怎么标记

三.跨代引用带来的问题

四.怎么解决跨代引用的问题

五.记忆集RSet里面记录了什么 + 哪些引用关系需要记录 + 哪些不需要记录

 

问题:

RSet里面到底是怎么存的?

G1是怎么设计引用关系存储结构的?

 

3.G1中的位图和卡表

(1)引入位图可以做什么

(2)位图在对象引用方面的应用会有什么问题(描述内容很少 + 位图过大)

(3)怎么优化位图

(4)什么是卡表

(5)卡表和位图到底是什么关系

 

(1)引入位图可以做什么

位图就是通过"位"来描述某一块内存的状态的图,位图就是一组二进制位数据,位图的每一个二进制位的0和1都标识了其描述内容的状态。

 

假设有两个分区,分区1有一个对象student,分区2有一个对象score。并且student.score = score,此时这两个分区就有了引用关系了。那么问题来了,如果要找到分区1是怎么引用到分区2,应该怎么做?

 

一.对分区1的内存按照一个字一个字地遍历

注意:一个字节8个位,在64位的电脑上,一个字是8个字节即64位。为什么用一个字一个字地进行遍历?虽然计算机底层存储单位是字节,但由于JVM中的对象会做对齐操作,所以不需要按照字节来移动。一个字其实就是一个机器字长,即处理器的寻址位数,如32位或64位。

 

所以为了找到分区1是怎么引用分区2,可以按一个字一个字地遍历分区1。在遍历时再看看每个字里的值是不是指向分区2,显然这种方式效率很低。这种方式其实就是按照一定的步长去遍历整个内存,然后步长就是一个字(即8个字节=64位)。

 

二.优化后的遍历

还是按照一定的步长去遍历整个内存,但是步长优化成了对象的长度。所以每次遍历时,都会按照当前遍历对象的长度来进行遍历。例如student长度为512K,那么在遍历student后的一个对象时,就从512K之后开始遍历,这样效率就会高出很多。

 

上面这种方式虽然经过了优化,但内存越大或者Region越大时遍历操作的效率就会越低。所以也不合适,因为找一个引用关系就要遍历一次,时间复杂度是O(n)。所以就有了第三种方式,借助位图来找到分区1是怎么引用到分区2。

 

三.用位图记录A和B的内存块之间是否发生引用

可以考虑在位图里用一个位,描述一个字的是否被引用的状态。比如在分区A中维护一个BitMap,BitMap的每一个位记录一下分区B对A中的一个字,是否存在引用关系。如果有引用关系,BitMap的位就记为1,如果没有就记位0。

 

这样查分区A是否被分区B引用时的时间复杂度就是O(1),因为直接通过位图就可以找到是否引用了,而不用遍历整个分区B才能确定是否存在引用分区A。

问题:这种位图的方式,会造成什么问题?

 

(2)位图在对象引用方面的应用会有什么问题(描述内容很少 + 位图过大)

一.描述内容太少

因为位图只有0和1这样的标记,只能证明是否有引用关系。但是JVM在垃圾回收时不单单只需要是否引用这个信息,它还需要一些其它的描述信息:比如使用量、内存起始位置等。否则GC可能因为信息缺乏,导致无法正常回收。

二.增加描述位又会存在位图过大问题

对于描述信息太少的问题,只能通过增加描述位来增加描述信息。比如不用一个位来描述一个字,而是使用一个字节来描述一个字。从使用1个二进制位描述64位内存的信息,到使用8个二进制位来描述64位内存的信息,这样就可以解决描述信息太少的问题。

 

但是新的问题来了:此时一个位图占用的内存过大了,例如一个Region的大小是1M。如果用一个字节描述一个字,就意味着需要128K来描述一个Region(1M)。这就相当于占用12.5%的Region空间去描述另外一个Region,这个内存占比就太大了,如果是在32位的机器上,甚至需要25%的空间。

 

如果要描述一个Region的内存情况:内存为1M就需要用掉128K额外内存来描述。内存为1G就需要用掉128M额外内存来描述,内存为10G就需要用掉1G多的描述数据。所以使用位图的最大问题,就是额外占用的内存空间太大了。

从图中可以看出来,一个字节描述一个字,还是很耗费内存的。相当于把描述能力降低了8倍,把内存空间上升了8倍。

 

(3)怎么优化位图

既然发现了内存占用空间太大的问题,那么就可以考虑把它优化掉。一般情况下,对象肯定不可能只占用一个字这么小的空间。所以可以直接把一个字节描述一个字的内存使用情况,优化成一个字节描述一个对象的内存使用情况。

 

但是对象本身又是不规则的,不一定多大,所以不能直接用对象的大小来作为描述的粒度。

 

最终JVM确定的是:用一个字节来描述512字节的内存,也就是用8位来描述512*8位的内存信息。这样就大大缩减在内存上的占用,使用2K的位图就可描述1M的内存空间。同时能兼顾引用关系查找的速度,不用采取遍历的方式来找到引用关系。

 

在查询分区A是否引用分区B时,就可以通过访问位图里快速确认是否有对应的引用关系。如果分区的大小是1M,那么只需要遍历2K的位图即可以确认,这时不用再遍历1M的Region内存块了。

(4)什么是卡表

G1中的卡表其实就是前面说的位图优化,就是把整个堆内存,按照512字节的粒度,拆分成一个个card,然后一个个card组合起来就形成了一个全局的卡表。卡表中每8个二进制位描述512字节内存的引用关系、使用情况等。

(5)卡表和位图到底是什么关系

卡表其实就是位图思想的一种实现方式,只是粒度不同罢了。位图使用每一个位来描述对应数据的状态,卡表使用一个字节来描述512字节内存的状态、引用等相关数据。总结来说就是,卡表是位图的增强版。

 

(6)总结

G1中的位图和卡表:

一.引入位图可以干什么

二.位图在对象引用方面的应用及存在的问题

三.怎么优化位图

四.什么是卡表

五.卡表和位图到底是什么关系

 

问题:卡表和Rset记忆集是怎么结合起来使用的?其实Rset中存储的就是卡表地址。

4.记忆集和卡表有什么关系

(1)卡表和记忆集到底存储了什么

(2)记忆集和卡表是怎么关联的

(3)RSet在空间和时间上做的平衡

 

(1)卡表和记忆集到底存储了什么

记忆集RSet里面存储的是"谁引用了当前Region"的引用信息,G1可以通过这些引用信息去找到引用当前Region的对象。

 

G1通过当前Region的记忆集可以找到:引用了当前Region对象的所有对象所在的Region以及这些对象的位置。

 

一.记忆集的数据结构和存储结构

那记忆集里面的数据结构到底是什么?存储的数据又是什么呢?

 

RSet全称叫Remember Set,它是一个Set结构,RSet可以理解成一个类似于Hash表的结构。一个Region会对应一个RSet,一个RSet记录了引用其Region的其他Region的对象引用关系。

 

一个RSet是由一个个key-value对组成的:key是引用了当前Region(被引用方)的其他Region(引用方)的地址。value是一个数组,数组元素是引用方的对象所在的卡页在卡表中的坐标,卡页就是512字节的内存页。如下图示:

也就是说,在遍历对象时,可以直接通过对象所在Region的记忆集,就能拿到所有引用了当前对象的对象所在Region的卡表数据,即卡页对应的512B内存块地址在卡表中的坐标。

 

所以记忆集存储的不是哪一些对象引用了当前Region,记忆集存储的是对当前Region有引用关系的对象的大概位置信息。也就是对象所在的Region地址 + 对象所在的卡页在卡表中的坐标,粒度相比对象来说会稍微大一些。

 

二.记忆集的存储结构和作用

一旦发现老年代的对象引用了一个新生代的Region中的对象,那么G1就会在这个新生代的Region的记忆集中维护一个key-value对。其中key是引用方对象对应的Region的地址,也就是那个老年代的对象所在Region的地址。其中value是一个数组,里面存储的数组元素是老年代对象所在卡页(512字节)在全局卡表中的坐标。

 

通过记忆集的key,可以快速定位Region,从而避免遍历老年代。通过记忆集的value,可以快速定位对象,从而避免遍历Region。

 

G1在遍历一个新生代的Region时,就能根据这个Region的记忆集,快速定位到引用该Region的对象所在的Region及这些对象所在的卡页,从而避免对老年代进行全局扫描。

 

三.卡表中存储了什么

G1中的卡表存储的不是引用关系信息,而是卡页的内存使用情况,比如是否使用、使用多少、GC中的状态信息、以及对应哪一块内存。

 

四.记忆集中存储了什么

G1中的记忆集存储的是内存之间的引用关系,也就是Region和卡页之间的映射关系。

 

之前介绍的卡表图:

这里介绍的卡表图:

因为记忆集已存储了引用关系,所以卡表的功能就是描述内存的使用。卡表会记录内存是否使用,对象位于哪个卡页,和GC过程中的状态信息。

 

通过记忆集可以描述出引用关系,通过卡表可以描述GC的状态信息,对象所处的内存区域,如第几个卡页。借助这些信息能快速知道内存使用情况,如是否使用、使用多少卡页等。结合记忆集和卡表,就能轻易获取引用关系和内存使用情况的详细信息。

 

(2)记忆集和卡表是怎么关联的

简单来说,RSet记忆集其实就是存储了一些卡表的信息。具体就是对当前Region有引用关系的对象的大概位置信息,也就是对象所在的Region地址 + 对象所在的卡页在卡表中的坐标。

 

下面举个例子:

一.新生代Region2中有一个对象Obj2。

二.老年代Region1和老年代Region5里有Obj1、Obj3、Obj4、Obj5,其中Obj1、Obj3、Obj4、Obj5均引用了Obj2这个对象。

三.其中Obj3、Obj4位于老年代Region1的同一个卡页CardPage里,该卡页CardPage在全局卡表的坐标是666。

四.Obj1和Obj5分别位于老年代Region5的两个卡页CardPage里面,这两个卡页CardPage在全局卡表的坐标为:1788和1756。

 

对象的引用关系以及对象所在的卡表坐标在卡表中的信息如下所示,通过这个卡表,我们就能准确的找到对象所在的CardPage页。如下图示:

Region2的RSet存储的信息,如下图示:

当G1在进行新生代垃圾回收时,就可以通过新生代Region2的记忆集RSet,结合新生代的GC Roots,来找到新生代的所有引用关系,以及老年代跨代引用的所有引用关系。这样就不会漏找,同时不需要在进行新生代回收时去遍历整个老年代。

 

问题:按照RSet的这个存储逻辑,也不能在回收时直接定位到老年代引用新生代的对象到底有哪些。

 

(3)RSet在空间和时间上做的平衡

一.如果没有RSet + 卡表

那么回收新生代时是需要遍历整个老年代的对象的,非常耗时。为了解决这个问题才引入了RSet + 卡表这两个额外的存储结构。

 

二.如果把RSet + 卡表的存储粒度,按每一个对象来存储,也不太合适

因为RSet + 卡表会占用很大的额外内存。

 

三.所以RSet + 卡表的机制是对空间和时间做了平衡后设计出来的结果

 

按照该设计,每次找老年代的引用对象时只需遍历一下对应的卡页即可。而且还可以结合对象的长度去做遍历,也就是按照一个对象长度的内存空间为步长去遍历来提升性能。

 

(4)总结

G1中的记忆集和卡表关系:

一.卡表和记忆集到底存储了什么

二.卡表和记忆集之间是怎么关联的

三.RSet在空间和时间上做的平衡

 

大量的引用关系变更时,更新RSet有可能会产生并发安全问题,以及并发性能问题。所以RSet什么时候维护、以怎样的方式维护,非常重要。

 

问题:Remember Set的数据到底是怎么维护的?什么时候维护?如果出现了大量的引用关系变更,怎么处理?大量引用关系变更的时候会不会出现性能问题,性能问题应该怎么优化?

 

RSet是一个垃圾回收的时候非常重要的数据,所以引用关系变更时,为了保证准确性,是否需要更改这个RSet?

 

5.RSet记忆集是怎么更新的

(1)引用关系发生改变时RSet是否需要更新

(2)每次引用关系变更都更新

(3)应该什么时候处理Rset的更新操作

(4)G1的脏数据队列—异步消费更新RSet

 

(1)引用关系发生改变时RSet是否需要更新

如果有对象引用变更,比如新增对象的引用,或失去对象的引用。此时RSet肯定是要更新的,否则就会出现回收时引用关系错误。回收时可能就会出现把正常对象当垃圾对象,或把垃圾对象当正常对象。所以在引用关系变更时,一定需要更新Rset。

 

例子如下:

一.新生代Region2中有一个对象Obj2。

二.老年代Region1和老年代Region5里有Obj1、Obj3、Obj4、Obj5,其中Obj1、Obj3、Obj4、Obj5均引用了Obj2这个对象。

三.其中Obj3、Obj4位于老年代Region1的同一个CardPage里面,这个CardPage在全局卡表的坐标位是666。

四.Obj1和Obj5分别位于老年代Region5的两个CardPage里面,这两个CardPage在全局卡表的坐标是1788和1756。如果Obj5这个对象,执行了Obj5.Obj2 = null,那么此时Obj5对Obj2的引用关系就会消失。此时引用关系发生变更,肯定是需要更新Rset的,那么到底应该怎么更新?更新的策略又是什么?如下图示:

所以接下来有两个问题:

一.每次更新引用关系,RSet一定要立即更新吗?如果不立即更新会有什么影响?

二.如果立即更新会产生什么问题?如果不立即更新的话,又有什么合理的方案?

 

(2)每次引用关系变更都更新

由于一个Region会有一个RSet,一旦发生同一个对象或同一个Region内对象的引用关系变更,那么是会出现并发访问同一个RSet的情况的。

 

所以第一个需要解决的问题就是并发问题,而解决并发问题的最好方式要么是串行化处理,要么是分而治之,时间不敏感可用异步队列。

 

问题一:假如使用串行化处理,那么每次引用关系变更,是否可以立即更新RSet?

如果每次更新引用关系后立即去加一把锁,然后修改RSet,则肯定是能保证RSet的准确性的。如下图示,此时Obj5这个对象所在的CardPage已经从新生代Region2的RSet中移除了。

但是这种方式最大的问题是:虽然保证了准确性,但对象的创建和更新等一系列操作是非常频繁的。如果用这种加锁的方式更新RSet,那么肯定会造成性能问题,毕竟一个Region共享一个RSet。每一次对象的创建和更新等操作,肯定都可能去访问这个RSet,这样对RSet加锁进行串行化处理,必然会造成整体性能的下降。

 

所以不能采取串行化处理的方式,那么应该采取什么方式来处理呢?

 

问题二:假如使用分治思想来处理,那么是否能保证系统运行的效率?

使用分治思想,那么就需要把一个RSet分割开来,交给多个线程去处理,同时在最后进行汇总。

 

但是在这个场景下,肯定不能这么做。

原因一:首先分治操作需要把RSet分开,那么按照什么维度分开呢?

原因二:分治思想因为要分开处理,最后汇总,所以肯定要通过几个线程专门来处理,并且这几个线程最好是固定的,那么这些线程是不是都是系统的工作线程?系统的工作线程专门用来做分治是否合适?如果专门新开几个线程,但本来就有上千个Rset,现在还要分治,那要增加多少线程?

原因三:分治处理后的结果如何汇总、何时汇总?

 

所以也不能采取分治思想来处理,那么还有没有什么其他的方案比较合适呢?

 

(3)应该什么时候处理Rset的更新操作

问题:RSet的数据什么时候才会用到的?

JVM最核心的两大操作就是对象分配和垃圾回收,下面分析JVM的这两个核心过程。

 

一.分配对象时是否需要用到RSet的数据

分配对象时是并不需要用到RSet的数据的。因为G1在分配一个对象时,只需要看内存够不够,剩余空间是否能够分配一个对象。分配完成后,直接更新一下内存的使用情况即可,并不需要借助RSet。

 

二. 垃圾回收时是否需要用到RSet的数据

RSet本身就是为了垃圾回收时更加方便、不需要遍历空间而设计的,所以在垃圾回收时肯定需要用到RSet。

 

那么可以得出一个结论:在大多数时间里,RSet即使需要更新而没有把它更新,其实也无所谓,因为不影响程序运行。

 

如下图示,当新创建一个对象Obj6时,完全和RSet没有任何关系,直接创建即可。

只有在垃圾回收时,才需要使用最新的RSet,否则就会出错,如下图示:

如果此时需要垃圾回收,而Rset没有更新最新的引用关系,肯定会出错。上图RSet里的值,很明显1756这个值是需要剔除的。因为Obj5已经执行过Obj5.obj2 = null操作了,它已经不引用Obj2了。所以为了保证RSet最新,这时GC的遍历过程可能会有额外的遍历。

 

因此这个更新RSet的操作应该在垃圾回收前完成,既然是在垃圾回收之前完成,那在程序运行到需要垃圾回收的长时间里,就可以通过后台线程把这个更新的事情处理好即可。

 

(4)G1的脏数据队列—异步消费更新RSet

基于前面的分析,可知只要在垃圾回收前把所有引用的更新处理好即可。而在垃圾回收前,G1是有大量的时间慢慢更新这个引用关系的。

 

所以G1就设计了一个叫做DCQ(Dirty Card Queue)的队列。在每次有引用关系变更时,就把这个变更操作,发送一个消息到DCQ里,然后有一个专门的线程去异步消费。

 

这样G1的回收线程在读RSet时就能读到正确数据,同时不影响系统工作,如下图示:

(5)总结

RSet记忆集的更新问题:

一.如果引用关系发生了改变,RSet是否需要更新,应该怎么更新

二.每次引用关系变更都需要更新RSet

三.Rset的更新操作应该在垃圾回收前完成

四.G1的脏数据队列—异步消费更新RSet

 

问题:异步消费是谁在消费?应该给多少个线程才不会损耗CPU大量的资源?如果到了最后垃圾回收时还是没有消费完毕,应该怎么办?

 

6.DCQ机制的底层原理是怎样的

(1)异步消费是谁在消费

(2)多个线程可能会消费同一个DCQ里的数据

(3)DCQ的长度和DCQ的并发消费问题

(4)DCQ里的数据到底是怎么写进去的(写屏障)

 

概述:

DCQ机制:

一.DCQ有多少个

二.谁来消费DCQ

三.消费线程的数量是怎么确定的

四.DCQ到底是怎么写进去的

 

RSet的更新并不是同步完成的。G1会把所有的引用关系都先放入一个队列中,称为DCQ,然后使用线程来消费这个DCQ队列以完成更新。正常会有G1ConcRefinementThreads个线程处理,除了Refine线程会更新RSet,GC线程或Mutator(Java的应用线程Mutator)也可能会更新RSet。DCQ会通过DCQS来管理,为了能并发地处理,每个Refine线程只负责DCQS中的某几个DCQ。

 

(1)异步消费是谁在消费

G1有个Refine并发线程池,线程数默认是G1ConcRefinementThreads+1。Refine线程的主要工作,就是去消费DCQ里的消息,然后去更新RSet。

 

当然Refine线程还有其他的作用。Refine线程还会进行新生代分区的抽样工作,动态扩展新生代。Refine线程会在满足响应时间的前提下,根据抽样数据(GC时间、垃圾数量等),去调整新生代的Region个数。

 

总结:Refine线程的主要工作是管理RSet,即消费DCQ里的消息然后更新RSet里的引用关系。Refine线程中会有一个线程一直进行新生代分区的抽样。

 

问题:如果Refine线程忙不过来了怎么办?DCQ中的变更消息很多,Refine线程超负荷工作无法全部消费变更消息会怎样?

 

(2)多个线程可能会消费同一个DCQ里的数据

平常只会有少量的Refine线程去消费DCQ里的数据。当系统并发非常高的时候,DCQ里面的变更消息可能会非常非常多,那么此时Refine线程肯定是忙不过来的。这时就需要有多个Refine线程一起处理引用变更数据,甚至还有可能会有其他线程参与进来一起处理。

 

我们可以指定Refine线程的数量,Refine线程的默认数量是G1ConcRefinementThreads + 1。

如果实在忙不过来,那么就只能在指定参数时多设置几个Refine线程了。因为G1一般是大内存、多核机器,所以设置4-5个线程其实也无所谓。

如果Refine线程已经够多了,此时还是忙不过来,那么G1就会借助系统的工作线程。即创建完对象后,这个线程很可能没有去等着接收其他请求,而是帮忙去消费DCQ了。

 

(3)DCQ的长度和DCQ的并发消费问题

如果一个队列无限长无限增加,单独一个DCQ,很多线程都往里面写。由于DCQ的消费必须保证顺序性,而且只有一个DCQ。那么不管是写入还是消费,都会有并发写和并发消费的问题的,所以G1设计了二级缓存来解决并发冲突的问题。

 

一.第一级缓存是在线程这一层

每一个工作线程都会关联一个DCQ,一个DCQ对应一个Region,多个工作线程可能会对应同一个DCQ。每个线程在执行引用更新操作时,都会往自己那个DCQ里写入变更信息。如果没有第一级缓存,那么所有工作线程都会关联到一个DCQ。DCQ长度默认是256,如果一个DCQ写满,就重新申请一个新的DCQ,并把这个满了的DCQ提交到第二级缓存DCQ Set。

 

二.第二级缓存是在一个DCQ Set里面

一般叫这个二级缓存为DCQS,在线程持有的DCQ满了以后,会把DCQ提交到DCQS中去。

 

这样就解决了并发写的问题,因为每个线程只写自己持有的DCQ即可,写满了就提交到DCQS,顶多这时会加一个锁。

 

此外,由于一个DCQ会对应一个Region,所以按照时间去消费DCQS里的DCQ时,不需要考虑是否会顺序消费的问题。

当Refine线程实在是忙不过来时是怎么处理的?由于Refine线程是直接从DCQS取DCQ去消费的,那么如果Refine线程忙不过来,也就意味着:DCQS已经不能再放下更多满了的DCQ了。

 

此时工作线程的DCQ满时,就会去判断能否提交到DCQS。如果发现不能提交,工作线程就会自己去处理这个DCQ,然后更新RSet。

 

(4)DCQ里的数据到底是怎么写进去的(写屏障)

G1会给每个变更操作前都加了一层写屏障,注意这个写屏障和内存屏障可不一样,这个写屏障是类似于增强代码或者AOP切面的一个概念。

 

写屏障的意思是:我们在修改了内存值时(就是往内存里写东西时),额外执行的一些操作。比如判断是否需要写到DCQ(新生代与新生代的引用就不需要写),比如判断本次的写入操作是否改变了引用,比如发送一条消息到DCQ等。这么一层加强代码,就是所谓的写屏障。

 

那么这层加强代码到底由什么用呢?作用其实很大,因为不是任何写内存的操作都会改变引用关系什么的。如果没有改变引用关系,就不需要写这么一条数据到DCQ里面了。这样写屏障就可以大大减少后续Refine线程处理DCQ的压力,同时写屏障也避免DCQ快速填满导致Refine线程被快速启动。

 

所以这个写屏障的作用主要有两点:第一过滤掉不需要写的引用变更操作,比如新生代到新生代的引用、同一分区内的引用等这些引用是不用写的,第二就是把正常的变更数据写入到DCQ里。

 

(5)总结

DCQ机制:

一.谁来消费DCQ

二.消费线程的数量是怎么确定的

三.DCQ里的数据到底是怎么写进去的

 

问题:DCQS到什么时候算满?多少个Refine线程去处理DCQS才合适?如果到了GC时,Refine线程还是没有处理完,应该怎么办?

 

7.DCQS机制及GC线程对DCQ的处理

(1)DCQS是什么

(2)DCQS的白绿黄红四个挡位

(3)如果DCQS直到GC时还没处理完会怎样

 

概述:

DCQS机制:

一.DCQS是什么

二.DCQS是怎么更新的

三.如果DCQ没有处理完毕就进入GC会怎么样

 

JVM声明了一个全局的静态变量DCQS,DCQS里面存放的是DCQ。为了性能考虑,所有处理引用关系的线程共享一个DCQS。每个线程(Java的应用线程Mutator)在初始化时都会关联这个DCQS,每个线程都会关联一个DCQ。

 

DCQ的最大长度默认256,最多存放256个引用关系对象。在本线程中如果产生新的对象引用关系,则把引用者放入DCQ队列中。当本线程的DCQ队列满256个时,就把这个DCQ队列放入到DCQS中。DCQS可以被所有线程共享,所以放入DCQ时需要加锁。而DCQ的处理则是通过Refine线程来处理。

 

(1)DCQS是什么

G1设计了一个二级缓存来解决DCQ写入引用关系变更数据时的并发冲突。第一层缓存是在线程这一层,每一个工作线程都会关联一个DCQ。每个线程在执行引用更新操作时,都会往自己那个DCQ里写入变更信息。

 

DCQ的长度默认是256,如果写满了,就重新申请一个新的DCQ,并把这个满了的DCQ提交到第二级缓存,也就是一个DCQ Set里面去。

 

这个二级缓存叫DCQS,所以所谓的DCQS其实不过就是一个存放DCQ的地方。

 

(2)DCQS的白绿黄红四个挡位

一.设置Refine线程个数要考虑的问题

DCQS肯定是有上限的。当达到一定的阈值不能再提交时,工作线程就得自己去处理了。这时说明系统负载已经很重了,而且系统的运行速度可能会比较慢,因为工作线程要去处理DCQ更新RSet的引用关系。

 

负载很重时,G1就要考虑应该启动多少个线程:避免出现负担很重的情况、避免大量Refine线程导致CPU资源占用问题。

 

那么应该怎么设计Refine线程的个数?

如果启用Refine线程过多,则可能会导致程序整体性能一直都不高。如果启用Refine线程过少,则可能会导致运行一段时间,DCQS就满了。从而导致工作线程要做一些额外的更新RSet操作,这时整体性能也不高。

二.G1对DCQS的数量进行了四个区域划分

那么到底Refine线程的数量怎么设置?什么时候应该用多少个线程?针对这个问题G1对DCQS的数量进行了四个区域划分。

 

有三个参数来划分这四个区域:

G1ConcRefinementGreenZone

G1ConcRefinementYellowZone

G1ConcRefinementRedZone

这三个值默认都是0,如果没有设置,G1就会自动推断这个三个值。

 

区域一.白区:[0, green)

如果在这个区域,则Refine线程不会去处理DCQ,只会进行新生代抽样。

区域二.绿区:[green, yellow)

Refine线程开始启动,并根据DCQS的大小来计算启动Refine线程的个数。

区域三.黄区:[yellow, red)

所有Refine线程都会参与到DCQS的处理。

区域四.红区:[red, 正无穷)

所有Refine线程以及系统工作线程都会参与到DCQ的处理。

 

三.Refine线程个数的推断

Refine线程的个数可以通过参数来设置:

如果没有设置,就和ParallelGCThreads相等。如果ParallelGCThreads没有设置,则根据CPU核数来自动推断有多少个。

 

推断策略为:

如果核数 小于等于 8,则ParallelGCThreads = CPU核数;
如果核数 大于 8,则ParallelGCThreads = 8 + (核数 - 8) * 5 / 8;
如果没有设置Refine个数,则Refine总个数 = ParallelGCThreads;
如果设置了Refine个数为x,那么Refine的实际值为x + 1;
因为会有1个Refine线程在做抽样,另外根据DCQS的size来启动x个线程;

注意:G1会一直有1个Refine线程在处理抽样,下面说的Refine线程特指处理DCQ。

 

G1会根据DCQS的大小来启动不同的Refine个数,可以理解为让每个Refine线程处理多少个DCQ。比如DCQS大小为9,步长为3,那么就会启动3个Refine线程,当然步长也可以通过参数设置。如果不设置步长,则会自动推断参与处理DCQ的Rrefine线程个数。

 

DCQS的几个阈值会和ParallelGCThreads有关系:Green = ParallelGCThreads,Yellow = 3 * Green,Red = 6 * Green。

 

假如总共4个Refine线程处理DCQS(还有一个线程在处理新生代的抽样),那么绿黄红按步长(步长和Refine总线程数量相等)来计算就是[4, 12, 24]:

 

DCQS在少于4个DCQ时,不启动Refine线程;

在大于等于4个小于9个DCQ时启动1个线程;

在大于等于9个DCQ时,启动第二个线程;

在大于等于11个DCQ时,启动第三个线程;

在达到24个DCQ时工作线程也在处理DCQ;

 

(3)如果DCQS直到GC时还没处理完会怎样

如果DCQ数量在小于绿区的阈值时,是没有Refine线程在处理的。如果DCQS直到GC时还没有处理完,这时也不需要Refine线程处理了。

 

G1为了保证运行过程中的效率,会在GC时由GC线程来处理这些DCQ。因为GC线程也是会有多个的,在STW时处理这些DCQ,速度也不会很慢。

 

总结:启动了Refine线程,然而DCQ在GC开始时还是没有处理完。这时就会由GC线程参与处理没有完成处理的DCQ,直到全部处理完成,然后才会进行下一步GC操作。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/867340.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

1.10日学习笔记之C++的类

类其实就是一种数据类型,和结构相似。类的成员包括两类,属性(成员变量)和行为(成员函数)。 成员函数定义的两种方法(可能有多种,觉得这两种比较常用) 1、将类的成员函数定义在类体内,如 class CPerson {public: short age;short getage(){return age;}};2、将类的成…

着火智能监测识别摄像机

着火智能监测识别摄像机利用了先进的图像处理技术和人工智能算法,能够快速、准确地检测环境中的着火点,从而及时采取必要的安全措施。其工作原理如下:首先,摄像机配备高清摄像头,能够实时捕捉并传输环境图像。这些图像通过内置的处理单元,进行高效处理和分析。其次,利用…

BurpSuite实操之测试器功能

对web应用程序进行自动化攻击。此功能有多种用途,如漏洞利用、模糊测试、进行暴力pojie等1、目标设置代理发送过来请求的目标主机及端口信息。 输入要攻击的目标及端口2、位置设置在这个模块可以设置攻击的参数,及攻击使用的类型(1)Sniper-狙击手(2)Battering ram-破城锤…

采沙船智能监测识别摄像机

采沙船智能监测识别摄像机还支持数据记录和分析功能,可以生成详尽的非法采沙统计报告和趋势分析,为河流管理决策提供科学依据。管理部门可以通过监控系统的数据分析,制定精准的禁采政策和优化河流资源管理,有效保护水域生态环境,防止非法采砂对生态系统的破坏。在实际应用…

C api简单查询需要的几步

C api查询简单需要的几步 1.准备sql语句 2.绑定变量 通过sqlstatement类bindout方法将查询出来的值与存储他们的变量绑定。 这里绑定是为了以后从结果集中提取一行时只需要一行就可以完成将提取出 来的变量赋值。 3.执行execute 在这一步将sql语句执行。在这里有几个重点 mysql…

iterm2

外观 这个github上的项目提供了很多主题 https://iterm2colorschemes.com/ 亮色主题感觉还可以的几个: OneHalfLight、BuiltinLight、BlulocoLight、Materil、ProLight、Tango Half Adapted 下面截图是基于 Tango Half Adapted用蓝色代替青色,黄色加深之后的效果(vim括号插件…

22. MDI窗口设计

一、什么是MDI窗口MDI 窗口(Multiple-Document Interface),又称多文档界面,它主要用于同时显示多个文档,每个文档显示在各自的窗口中。MDI 窗口中通常包含子菜单和窗口菜单,用于在窗口或文档之间进行切换。用 QMainWindow 建立的主界面,通常会同时建立或打开多个相互独立…

详解:订单履约系统规划

大家好,我是汤师爷~ 什么是订单履约系统? 订单履约是从消费者下单支付到收到商品的全流程管理过程,包括订单接收、订单派单、库存分配、仓储管理和物流配送等环节,核心目标是确保商品准时、准确地送达消费者手中。 通过订单履约系统,消费者可以实时了解商品的物流状态和预…

文件单独编译生成

编译设备树也最好在 source过的那个窗口中来编译错误提示如下: 同时在makefile文件修改时也要注意

In‐band Network Telemetry

#卫星 #遥测技术 #INT 一、INT是什么? INT,In‐band Network Telemetry ,带内网络遥监测。telemetry,英文原意是遥测技术。从其英文名称可以了解如下: a.In-band,说明监测指令及数据均在带内传输 b.telemetry ,说明是长距离,远程获取网络数据的方法。 想象一下卫星在…

潮汐指纹识别工具 : 在线网站识别利器

最近发现了一个免费在线收集网站信息的工具,感觉挺好用的,给各位推荐下。潮汐在线指纹识别是山东新潮信息技术有限公司安全团队提供的一个免费开源在线网站信息收集工具。 功能亮点: 全面扫描 只需输入目标网站的URL或IP地址,即可获取该网站的标题、中间件、操作系统、域名…

JAVA-Day 11:数组的静态初始化和遍历

数组的静态初始化和遍历 数组静态初始化格式 数组的静态初始化与遍历 完整格式:数据类型 [] 数组名=new 数据类型[]{元素1,元素2,元素3,....} 简化格式:数据类型 [] 数组名={元素1,元素2,元素3,....} []在数组名前后都可以 代码如下: int number[]={1,2,3,4,5};for (int …