您当前的位置: 首页 > 

水的精神

暂无认证

  • 3浏览

    0关注

    711博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

G1 垃圾回收器实现细节

水的精神 发布时间:2021-05-06 00:31:10 ,浏览量:3

  一直惦记着G1垃圾回收器,如果不出意外的话,G1今后一定会被大量用于生产。

 之前零零散散的看了一些关于G1的知识点,但是很难形成一个知识体系,都只是皮毛。趁着这个假期的尾巴,恶狠狠的啃了一遍谷歌出来的G1详解相关的文章,特地整理出来分享给大家。

 看了前几页的文章,每篇文章都有其精髓的地方,但是内容不是很连贯,我会加上我的理解我会把前辈写的好的地方融合到这篇文章。

 这篇文章会偏向与实现细节,适合进行有深度的关于G1的学习。

 

1. 一个G1的引子:为什么会有G1

  所有的垃圾回收器,都是朝着一个目标前进的,尽可能的降低垃圾回收的时间,从而降低系统因为垃圾回收带来的延迟。G1也不例外。

  此外G1也是一个承前启后的垃圾回收器。它在一定程度上,是要替代CMS。在实现上G1也和CMS有很多相似支持,不过G1更好的解决了CMS的一些问题。

  • 乱世造英雄。不同的垃圾回收器都是时代的产物,都是某一个阶段最适合的垃圾回收器。G1也是时代的产物,垃圾回收器的进步也是随着计算机硬件的进步而进步的。在现在这个重视低延迟,大内存的情况下。G1显得更有优势,接下来我们就从G1的实现细节,来分析一下,它又都哪些进步思想。
  • G1有更高的垃圾回收效率。为低延迟而生。
  • G1开辟了分区的思想,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷。熟悉垃圾回收器的朋友肯定知道垃圾回收算法,在之前的垃圾回收器中,年轻带使用的是复制算法,而老年代使用的是标记整理的算法。而标记整理,是为解决内存碎片问题而产生的。但是也有一个天生的缺陷,就是整理内存是需要时间的。 而G1这个分区的思想,G1将内存划分一个个固定大小的region,每个region可以是年轻代、老年代的一个。内存的回收是以region作为基本单位的。
  • G1是一个分代的,增量的,并行与并发的标记-复制垃圾回收器。
  • G1采用启发式算法,在老年代找出具有高收集收益的分区进行收集。这也正式G1(Garbage First)首先收集尽可能多的垃圾的原则。

  这里有一个异议。我在《深入理解JVM虚拟机》这本书里边看到,在G1的章节里边说:“G1从整理上来看是基于 标记-整理 算法实现的垃圾回收器,从局部上(两个Region之间)来看是基于复制算法实现的。但是无论如何这两种算法都意味着G1在运行期间不会产生碎片”。但是我个人觉得从G1的实现原理,以及运作原理上来看,没看到标记整理的痕迹。我始终任务标记整理算法是为了解决内存碎片化的问题的。既然G1在运行期间不会产生碎片,那为什么用标记整理呢?这是我对这本书的描述提出的异议。如果有懂的老哥,可以告诉我答案,我将不胜感激!

2. 一个适合G1的生态环境

计算机的发展,从单CPU到现在的多CPU,从CPU单核到现在的多核。也就是说硬件环境给G1带来了可能性。这也是为什么垃圾回收器从单线程演变成了多线程。并不是G1才开始使用多线程的,G1仍然沿用多线程,充分的利用计算机资源。

另外一个最大的原因,就是内存的资源的充分,内存资源开始稍微的变得廉价了起来。这使得公司愿意花费内存空间来换取时间。这也使得G1有更多的施展空间。G1是有以内存为代价来缩短垃圾回收的时间的。G1早在2004年,就有被提出,指导JDK7才被引入。在JDK9成为默认的垃圾回收器。

大的堆内存,造就了G1这种分区思想的垃圾回收器的使用空间。对于超大堆,即使是之前的多线程垃圾回收器,扫描起来整个堆空间也是非常吃力的。分区分块去回收的优势就得到无限的放大了。

接下来看一下G1的精妙绝伦之处。

 

3. G1 内存模型

  想要弄清楚G1,必须先拿下G1的内存模型,以及一些相关的概念。

  先来一张图来描绘G1垃圾回收器: 整块堆是一个空间;然后堆是由分区Region组成的;Region还不是最小的单位,Region是由card组成的。用过的分区有颜色,没有使用过的没有颜色。Global Card Table中,记录这哪些card可用,哪些card不可用。

G1ååºç¤ºæå¾

 

3.1 弱化分代,引入分区Region

  G1的第一个最大的特点,引入分区Region。它保留了逻辑上的分代思想。相比较之前的分代,是在物理上将堆分为了老年代和年轻带。在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可。并且这些不同的代可以自适应的去切换的。现在的整个堆就是一块儿,如下图

 其中E代表的是edan区;S代表的是servivor区;还引入了超大对象H区。因为G1是要保证回收效率的,那么特大对象在使用过程中如果不单独存放,在复制过程中会花费较长的时间。如果一个对象的大小超过分区大小的一半,该对象就被定义为巨型对象。巨型对象时直接分配到老年代分区,如果一个对象的大小超过一个分区的大小,那么会直接在老年代分配两个连续的分区来存放该巨型对象。巨型分区一定是连续的,分配之后也不会被移动。

 启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。

  分区的详细结构: 每一个Region 包含了5个指针,分别是bottom、previous TAMS、next TAMS、top和end, 其中previous TAMS、next TAMS是前后两次发生并发标记时的位置. 在prevTAMS和nextTAMS以上的对象就是新分配的

  • A是初始标记阶段。next TAMS尚未标记任何存活对象,而此时的previous TAMS被初始化为region内存地址起始值,next TAMS被初始化为top。top实际上就是一个region未分配区域和已分配区域的分界点;
  • B是并发标记之后,进入了再次标记阶段。此时存活对象的扫描已经完成了,因此next bitmap构造好了,代表的是当下状态中region中的内存使用情况。注意的是,此时top已经不再与next TAMS重合了,top和next TAMS之间的就是在前面标记阶段之时,新分配的对象
  • C代表的是clean up阶段。C和B比起来,next bitmap变成了previous bitmap,而在bitmap中标记为垃圾(也就是白色区域的)的对应的region的区域也被染成了浅灰色。这并不是指垃圾对象已经被清扫了,仅仅是标记出来了。
  • D代表的是下一个初始标记阶段,该阶段和A类似,next TAMS重新被初始化为top的值;
  • EF就是BC的重复;

 

 

3.2 卡片Card

  在每个分区内部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见RSet)。每次对内存的回收,都是对指定分区的卡片进行处理。

 

3.3 关于堆

  G1同样可以通过-Xms/-Xmx来指定堆空间大小。当发生年轻代收集或混合收集时,通过计算GC与应用的耗费时间比,自动调整堆空间大小。如果GC频率太高,则通过增加堆尺寸,来减少GC频率,相应地GC占用的时间也随之降低;目标参数-XX:GCTimeRatio即为GC与应用的耗费时间比,G1默认为9,而CMS默认为99,因为CMS的设计原则是耗费在GC上的时间尽可能的少。另外,当空间不足,如对象空间分配或转移失败时,G1会首先尝试增加堆空间,如果扩容失败,则发起担保的Full GC。Full GC后,堆尺寸计算结果也会调整堆空间。对于G1垃圾回收器,如果不是最够的熟悉,最好不要尝试去进行参数调优,它不像是之前的垃圾回收器,G1还有一个目的就是最少的参数调优,带来最好的性能。从设计的角度上,有在为开发运维人员减少负担。最好不要盲目的器进行参数调优。仅仅一个指定最大最小堆空间,保证不去动态的扩容缩容就不错了。

3.4 Remember Set (RSet)

 在下边的 5.1 会讲。

 

3.5  Region、 Remember Set和Card Table的关系

 

4. 关于分代

  为什么会一直保留着分代的思想。究其原因,分代垃圾收集可以将关注点集中在最近被分配的对象上,而无需整堆扫描,避免长命对象的拷贝,同时独立收集有助于降低响应时间。虽然分区使得内存分配不再要求紧凑的内存空间,但G1依然使用了分代的思想。与其他垃圾收集器类似,G1将内存在逻辑上划分为年轻代和老年代,其中年轻代又划分为Eden空间和Survivor空间。但年轻代空间并不是固定不变的,当现有年轻代分区占满时,JVM会分配新的空闲分区加入到年轻代空间。

  整个年轻代内存会在初始空间-XX:G1NewSizePercent(默认整堆5%)与最大空间(默认60%)之间动态变化,且由参数目标暂停时间-XX:MaxGCPauseMillis(默认200ms)、需要扩缩容的大小以-XX:G1MaxNewSizePercent及分区的已记忆集合(RSet)计算得到。当然,G1依然可以设置固定的年轻代大小(参数-XX:NewRatio、-Xmn),但同时暂停目标将失去意义。

4.1 本地缓冲区(TLAB)

 本地分配缓冲 Local allocation buffer (Lab)

  值得注意的是,由于分区的思想,每个线程均可以"认领"某个分区用于线程本地的内存分配,而不需要顾及分区是否连续。因此,每个应用线程和GC线程都会独立的使用分区,进而减少同步时间,提升GC效率,这个分区称为本地分配缓冲区(Lab)。其中,应用线程可以独占一个本地缓冲区(TLAB)来创建的对象,而大部分都会落入Eden区域(巨型对象或分配失败除外),因此TLAB的分区属于Eden空间;而每次垃圾收集时,每个GC线程同样可以独占一个本地缓冲区(GCLAB)用来转移对象,每次回收会将对象复制到Suvivor空间或老年代空间;对于从Eden/Survivor空间晋升(Promotion)到Survivor/老年代空间的对象,同样有GC独占的本地缓冲区进行操作,该部分称为晋升本地缓冲区(PLAB)。

 

5. 如何做到在GC的时候不扫描整个堆

  GC不扫描整个堆,这是GC减少STW时间的杀手锏。正因为G1采用的是分区,一次垃圾回收,扫描部分分区从而做到不扫描整个堆。不过这是以牺牲内存为代价的,使用一个RSet集合,来记录哪些分区需要扫描。这是典型的用空间换时间。

5.1 Remember Set (RSet)

  在串行和并行收集器中,GC通过整堆扫描,来确定对象是否处于可达路径中。然而G1为了避免STW式的整堆扫描,在每个分区记录了一个已记忆集合(RSet),内部类似一个反向指针,记录引用分区内对象的卡片索引。当要回收该分区时,通过扫描分区的RSet,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况。

  事实上,并非所有的引用都需要记录在RSet中,如果一个分区确定需要扫描,那么无需RSet也可以无遗漏的得到引用关系。那么引用源自本分区的对象,当然不用落入RSet中;同时,G1 GC每次都会对年轻代进行整体收集,因此引用源自年轻代的对象,也不需要在RSet中记录。最后只有老年代的分区可能会有RSet记录,这些分区称为拥有RSet分区(an RSet’s owning region)。

  那么还有一个问题,TSet的数据应该如何来。前边已经描述的比较清楚了,RSet是每个分区都有的,用来记录这个分区的对象都被哪些分区引用了。想必,是不是在对象赋值之前,就应该触发一个记录?比如说:Object obj1 = new Object(),此时obj1分配在了分区一,此时再来一个操作, Object obj2 = obj1, 此时是不需要再new对象出来的,此时obj2在另外一个分区。那就需要在分区一的 RSet里边记录它的对象被分区二引用了。那么何时去记录呢?这就是写屏障:在obj2 = obj1之前出发一个记录的操作。不过由于写屏障的开销是比较大的。所以并不是立刻去执行,这个记录到RSet的操作,而是记录到操作日志里边,批量去执行。

 

5.2 Per Region Table

RSet在内部使用Per Region Table(PRT)记录分区的引用情况。由于RSet的记录要占用分区的空间,如果一个分区非常"受欢迎",那么RSet占用的空间会上升,从而降低分区的可用空间。G1应对这个问题采用了改变RSet的密度的方式,在PRT中将会以三种模式记录引用:

  • 稀少:直接记录引用对象的卡片索引
  • 细粒度:记录引用对象的分区索引
  • 粗粒度:只记录引用情况,每个分区对应一个比特位

由上可知,粗粒度的PRT只是记录了引用数量,需要通过整堆扫描才能找出所有引用,因此扫描速度也是最慢的。

5.3 晋升本地分配缓冲区,PLAB

Promotion Local Allocation Buffer,晋升的过程,无论是晋升到S还是OLd区,都是在GC线程的PLAB中进行。每个GC线程都有一个PLAB。

 

6. 关于垃圾回收 6.1 哪些需要回收?CSet

CSetæ¶é示æå¾

  收集集合(CSet)代表每次GC暂停时回收的一系列目标分区。在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。因此无论是年轻代收集,还是混合收集,工作的机制都是一致的。年轻代收集CSet只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到CSet中。

候选老年代分区的CSet准入条件,可以通过活跃度阈值-XX:G1MixedGCLiveThresholdPercent(默认85%)进行设置,从而拦截那些回收开销巨大的对象;同时,每次混合收集可以包含候选老年代分区,可根据CSet对堆的总大小占比-XX:G1OldCSetRegionThresholdPercent(默认10%)设置数量上限。

由上述可知,G1的收集都是根据CSet进行操作的,年轻代收集与混合收集没有明显的不同,最大的区别在于两种收集的触发条件。

 

6.2 年轻代收集集合 CSet of Young Collection

  应用线程不断活动后,年轻代空间会被逐渐填满。当JVM分配对象到Eden区域失败(Eden区已满)时,便会触发一次STW式的年轻代收集。在年轻代收集中,Eden分区存活的对象将被拷贝到Survivor分区;原有Survivor分区存活的对象,将根据任期阈值(tenuring threshold)分别晋升到PLAB中,新的survivor分区和老年代分区。而原有的年轻代分区将被整体回收掉。

  同时,年轻代收集还负责维护对象的年龄(存活次数),辅助判断老化(tenuring)对象晋升的时候是到Survivor分区还是到老年代分区。年轻代收集首先先将晋升对象尺寸总和、对象年龄信息维护到年龄表中,再根据年龄表、Survivor尺寸、Survivor填充容量-XX:TargetSurvivorRatio(默认50%)、最大任期阈值-XX:MaxTenuringThreshold(默认15),计算出一个恰当的任期阈值,凡是超过任期阈值的对象都会被晋升到老年代。

6.3 混合收集集合 CSet of Mixed Collection

  年轻代收集不断活动后,老年代的空间也会被逐渐填充。当老年代占用空间超过整堆比IHOP阈值-XX:InitiatingHeapOccupancyPercent(默认45%)时,G1就会启动一次混合垃圾收集周期。为了满足暂停目标,G1可能不能一口气将所有的候选分区收集掉,因此G1可能会产生连续多次的混合收集与应用线程交替执行,每次STW的混合收集与年轻代收集过程相类似。

  为了确定包含到年轻代收集集合CSet的老年代分区,JVM通过参数混合周期的最大总次数-XX:G1MixedGCCountTarget(默认8)、堆废物百分比-XX:G1HeapWastePercent(默认5%)。通过候选老年代分区总数与混合周期最大总次数,确定每次包含到CSet的最小分区数量;根据堆废物百分比,当收集达到参数时,不再启动新的混合收集。而每次添加到CSet的分区,则通过计算得到的GC效率进行安排。

6.4 如何保证在并发标记过程中,如何解决错标的问题 ——SATB算法

  在并发标记过程中,应用程序在跑。标记线程也再跑。那么如何解决动态变化的问题呢?其实有必要了解一下三色标记算法:图解三色标记算法

  是为增量式标记清除垃圾收集器设计的快照标记算法,G1并发的基础就是SATB. 主要应用于垃圾收集的并发标记阶段. SATB可以理解成在GC开始之前对堆内存里的对象做一次快照,此时活的对象就认为是活的,从而形成一个对象图 步骤:

  • 在初始标记的时候生成一个快照图,标记存活对象
  • 在并发标记的时候出现了引用修改会把这些引用的原始值捕获下来,记录在log buffer中
  • 在再次标记阶段扫描SATB,修正SATB的误差

可能存在浮动垃圾,将在下次被收集。

 

7. GC 的完整周期

   初始标记——并发标记——最终标记——清理

G1å徿¶éæ´»å¨å¨æå¾

 

7.1 初始标记 Initial Mark

初始标记(Initial Mark)负责标记所有能被直接可达的根对象(原生栈对象、全局对象、JNI对象),根是对象图的起点,因此初始标记需要将Mutator线程(Java应用线程)暂停掉,也就是需要一个STW的时间段。事实上,当达到IHOP阈值时,G1并不会立即发起并发标记周期,而是等待下一次年轻代收集,利用年轻代收集的STW时间段,完成初始标记,这种方式称为借道(Piggybacking)。在初始标记暂停中,分区的NTAMS都被设置到分区顶部Top,初始标记是并发执行,直到所有的分区处理完。

7.2 根分区扫描 Root Region Scanning

在初始标记暂停结束后,年轻代收集也完成的对象复制到Survivor的工作,应用线程开始活跃起来。此时为了保证标记算法的正确性,所有新复制到Survivor分区的对象,都需要被扫描并标记成根,这个过程称为根分区扫描(Root Region Scanning),同时扫描的Suvivor分区也被称为根分区(Root Region)。根分区扫描必须在下一次年轻代垃圾收集启动前完成(并发标记的过程中,可能会被若干次年轻代垃圾收集打断),因为每次GC会产生新的存活对象集合。

7.3 并发标记 Concurrent Marking

和应用线程并发执行,并发标记线程在并发标记阶段启动,由参数-XX:ConcGCThreads(默认GC线程数的1/4,即-XX:ParallelGCThreads/4)控制启动数量,每个线程每次只扫描一个分区,从而标记出存活对象图。在这一阶段会处理Previous/Next标记位图,扫描标记对象的引用字段。同时,并发标记线程还会定期检查和处理STAB全局缓冲区列表的记录,更新对象引用信息。参数-XX:+ClassUnloadingWithConcurrentMark会开启一个优化,如果一个类不可达(不是对象不可达),则在重新标记阶段,这个类就会被直接卸载。所有的标记任务必须在堆满前就完成扫描,如果并发标记耗时很长,那么有可能在并发标记过程中,又经历了几次年轻代收集。如果堆满前没有完成标记任务,则会触发担保机制,经历一次长时间的串行Full GC。

7.4 存活数据计算 Live Data Accounting

存活数据计算(Live Data Accounting)是标记操作的附加产物,只要一个对象被标记,同时会被计算字节数,并计入分区空间。只有NTAMS以下的对象会被标记和计算,在标记周期的最后,Next位图将被清空,等待下次标记周期。

7.5 重新标记 Remark

重新标记(Remark)是最后一个标记阶段。在该阶段中,G1需要一个暂停的时间,去处理剩下的SATB日志缓冲区和所有更新,找出所有未被访问的存活对象,同时安全完成存活数据计算。这个阶段也是并行执行的,通过参数-XX:ParallelGCThread可设置GC暂停时可用的GC线程数。同时,引用处理也是重新标记阶段的一部分,所有重度使用引用对象(弱引用、软引用、虚引用、最终引用)的应用都会在引用处理上产生开销。

7.6 清除 Cleanup

紧挨着重新标记阶段的清除(Clean)阶段也是STW的。Previous/Next标记位图、以及PTAMS/NTAMS,都会在清除阶段交换角色。清除阶段主要执行以下操作:

  1. RSet梳理,启发式算法会根据活跃度和RSet尺寸对分区定义不同等级,同时RSet数理也有助于发现无用的引用。参数-XX:+PrintAdaptiveSizePolicy可以开启打印启发式算法决策细节;
  2. 整理堆分区,为混合收集周期识别回收收益高(基于释放空间和暂停目标)的老年代分区集合;
  3. 识别所有空闲分区,即发现无存活对象的分区。该分区可在清除阶段直接回收,无需等待下次收集周期。
8. 年轻代收集/混合收集周期

年轻代收集和混合收集周期,是G1回收空间的主要活动。当应用运行开始时,堆内存可用空间还比较大,只会在年轻代满时,触发年轻代收集;随着老年代内存增长,当到达IHOP阈值-XX:InitiatingHeapOccupancyPercent(老年代占整堆比,默认45%)时,G1开始着手准备收集老年代空间。首先经历并发标记周期,识别出高收益的老年代分区,前文已述。但随后G1并不会马上开始一次混合收集,而是让应用线程先运行一段时间,等待触发一次年轻代收集。在这次STW中,G1将保准整理混合收集周期。接着再次让应用线程运行,当接下来的几次年轻代收集时,将会有老年代分区加入到CSet中,即触发混合收集,这些连续多次的混合收集称为混合收集周期(Mixed Collection Cycle)。

8.1 GC工作线程数

GC工作线程数 -XX:ParallelGCThreads

JVM可以通过参数-XX:ParallelGCThreads进行指定GC工作的线程数量。参数-XX:ParallelGCThreads默认值并不是固定的,而是根据当前的CPU资源进行计算。如果用户没有指定,且CPU小于等于8,则默认与CPU核数相等;若CPU大于8,则默认JVM会经过计算得到一个小于CPU核数的线程数;当然也可以人工指定与CPU核数相等。

8.2 年轻代收集 Young Collection

每次收集过程中,既有并行执行的活动,也有串行执行的活动,但都可以是多线程的。在并行执行的任务中,如果某个任务过重,会导致其他线程在等待某项任务的处理,需要对这些地方进行优化。

并行活动

外部根分区扫描 Ext Root Scanning:此活动对堆外的根(JVM系统目录、VM数据结构、JNI线程句柄、硬件寄存器、全局变量、线程对栈根)进行扫描,发现那些没有加入到暂停收集集合CSet中的对象。如果系统目录(单根)拥有大量加载的类,最终可能其他并行活动结束后,该活动依然没有结束而带来的等待时间。

更新已记忆集合 Update RS:并发优化线程会对脏卡片的分区进行扫描更新日志缓冲区来更新RSet,但只会处理全局缓冲列表。作为补充,所有被记录但是还没有被优化线程处理的剩余缓冲区,会在该阶段处理,变成已处理缓冲区(Processed Buffers)。为了限制花在更新RSet的时间,可以设置暂停占用百分比-XX:G1RSetUpdatingPauseTimePercent(默认10%,即-XX:MaxGCPauseMills/10)。值得注意的是,如果更新日志缓冲区更新的任务不降低,单纯地减少RSet的更新时间,会导致暂停中被处理的缓冲区减少,将日志缓冲区更新工作推到并发优化线程上,从而增加对Java应用线程资源的争夺。

RSet扫描 Scan RS:在收集当前CSet之前,考虑到分区外的引用,必须扫描CSet分区的RSet。如果RSet发生粗化,则会增加RSet的扫描时间。开启诊断模式-XX:UnlockDiagnosticVMOptions后,通过参数-XX:+G1SummarizeRSetStats可以确定并发优化线程是否能够及时处理更新日志缓冲区,并提供更多的信息,来帮助为RSet粗化总数提供窗口。参数-XX:G1SummarizeRSetStatsPeriod=n可设置RSet的统计周期,即经历多少此GC后进行一次统计

代码根扫描 Code Root Scanning:对代码根集合进行扫描,扫描JVM编译后代码Native Method的引用信息(nmethod扫描),进行RSet扫描。事实上,只有CSet分区中的RSet有强代码根时,才会做nmethod扫描,查找对CSet的引用。

转移和回收 Object Copy:通过选定的CSet以及CSet分区完整的引用集,将执行暂停时间的主要部分:CSet分区存活对象的转移、CSet分区空间的回收。通过工作窃取机制来负载均衡地选定复制对象的线程,并且复制和扫描对象被转移的存活对象将拷贝到每个GC线程分配缓冲区GCLAB。G1会通过计算,预测分区复制所花费的时间,从而调整年轻代的尺寸。

终止 Termination:完成上述任务后,如果任务队列已空,则工作线程会发起终止要求。如果还有其他线程继续工作,空闲的线程会通过工作窃取机制尝试帮助其他线程处理。而单独执行根分区扫描的线程,如果任务过重,最终会晚于终止。

GC外部的并行活动 GC Worker Other:该部分并非GC的活动,而是JVM的活动导致占用了GC暂停时间(例如JNI编译)。

串行活动

代码根更新 Code Root Fixup:根据转移对象更新代码根。

代码根清理 Code Root Purge:清理代码根集合表。

清除全局卡片标记 Clear CT:在任意收集周期会扫描CSet与RSet记录的PRT,扫描时会在全局卡片表中进行标记,防止重复扫描。在收集周期的最后将会清除全局卡片表中的已扫描标志。

选择下次收集集合 Choose CSet:该部分主要用于并发标记周期后的年轻代收集、以及混合收集中,在这些收集过程中,由于有老年代候选分区的加入,往往需要对下次收集的范围做出界定;但单纯的年轻代收集中,所有收集的分区都会被收集,不存在选择。

引用处理 Ref Proc:主要针对软引用、弱引用、虚引用、final引用、JNI引用。当Ref Proc占用时间过多时,可选择使用参数-XX:ParallelRefProcEnabled激活多线程引用处理。G1希望应用能小心使用软引用,因为软引用会一直占据内存空间直到空间耗尽时被Full GC回收掉;即使未发生Full GC,软引用对内存的占用,也会导致GC次数的增加。

引用排队 Ref Enq:此项活动可能会导致RSet的更新,此时会通过记录日志,将关联的卡片标记为脏卡片。

卡片重新脏化 Redirty Cards:重新脏化卡片。

回收空闲巨型分区 Humongous Reclaim:G1做了一个优化:通过查看所有根对象以及年轻代分区的RSet,如果确定RSet中巨型对象没有任何引用,则说明G1发现了一个不可达的巨型对象,该对象分区会被回收。

释放分区 Free CSet:回收CSet分区的所有空间,并加入到空闲分区中。

其他活动 Other:GC中可能还会经历其他耗时很小的活动,如修复JNI句柄等。

 

9 并发标记周期后的年轻代收集 Young Collection Following Concurrent Marking Cycle

当G1发起并发标记周期之后,并不会马上开始混合收集。G1会先等待下一次年轻代收集,然后在该收集阶段中,确定下次混合收集的CSet(Choose CSet)。

9.1 混合收集周期 Mixed Collection Cycle

单次的混合收集与年轻代收集并无二致。根据暂停目标,老年代的分区可能不能一次暂停收集中被处理完,G1会发起连续多次的混合收集,称为混合收集周期(Mixed Collection Cycle)。G1会计算每次加入到CSet中的分区数量、混合收集进行次数,并且在上次的年轻代收集、以及接下来的混合收集中,G1会确定下次加入CSet的分区集(Choose CSet),并且确定是否结束混合收集周期。

9.2 转移失败的担保机制 Full GC

转移失败(Evacuation Failure)是指当G1无法在堆空间中申请新的分区时,G1便会触发担保机制,执行一次STW式的、单线程的Full GC。Full GC会对整堆做标记清除和压缩,最后将只包含纯粹的存活对象。参数-XX:G1ReservePercent(默认10%)可以保留空间,来应对晋升模式下的异常情况,最大占用整堆50%,更大也无意义。

G1在以下场景中会触发Full GC,同时会在日志中记录to-space-exhausted以及Evacuation Failure:

  1. 从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
  2. 从老年代分区转移存活对象时,无法找到可用的空闲分区
  3. 分配巨型对象时在老年代无法找到足够的连续分区

由于G1的应用场合往往堆内存都比较大,所以Full GC的收集代价非常昂贵,应该避免Full GC的发生。

 

参考文章:

https://www.cnblogs.com/grimm/p/13285623.html

https://zhuanlan.zhihu.com/p/343379986

 

关注
打赏
1664074814
查看更多评论
立即登录/注册

微信扫码登录

0.0429s