G1垃圾收集器原理详解

更新时间:2023-05-28 21:53:36 阅读: 评论:0

G1垃圾收集器原理详解
⼀、G1 垃圾收集器的开发背景:
1、CMS 垃圾收集器的缺陷:
JVM 团队设计出 G1 收集器的⽬的就是取代 CMS 收集器,因为 CMS 收集器在很多场景下存在诸多问题,缺陷暴露⽆遗,具体如下:
(1)CMS收集器对CPU资源⾮常敏感。在并发阶段,虽然不会导致⽤户线程停顿,但是会占⽤CPU资源⽽导致引⽤程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4
(2)CMS收集器⽆法处理浮动垃圾,由于CMS并发清理阶段⽤户线程还在运⾏,伴随程序的运⾏⾃然会有新的垃圾不断产⽣,这⼀部分垃圾出现在标记过程之后,称为“浮动垃圾”,CMS ⽆法在本次收集中处理它们,只好留待下⼀次GC时将其清理掉。
(3)由于垃圾收集阶段会产⽣“浮动垃圾”,因此CMS收集器不能像其他收集器那样等到⽼年代⼏乎完全被填满了再进⾏收集,需要预留⼀部分内存空间提供并发收集时的程序运作使⽤。在默认设置下,CMS收集器在⽼年代使⽤了68%的空间时就会被激活,也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提⾼触发百分⽐,以降低内存回收次数提⾼性能。要是CMS运⾏期间预留的内存⽆法满⾜程序其
转氨酶高怎么办他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启⽤Serial Old收集器来重新进⾏⽼年代的垃圾收集,这样停顿时间就很长了。所以参数 -XX:CMSInitiatingOccupancyFraction 设置的过⾼将会很容易导致 “Concurrent Mode Failure” 失败,性能反⽽降低。
(4)CMS是基于“标记-清除”算法实现的收集器,会产⽣⼤量不连续的内存碎⽚。当⽼年代空间碎⽚太多时,如果⽆法找到⼀块⾜够⼤的连续内存存放对象时,将不得不提前触发⼀次Full GC。为了解决这个问题,CMS收集器提供了⼀个-
XX:UCMSCompactAtFullCollection开关参数,⽤于在Full  GC之后增加⼀个碎⽚整理过程,还可通过-
XX:CMSFullGCBeforeCompaction参数设置执⾏多少次不压缩的Full  GC之后,跟着来⼀次碎⽚整理过程。
2、G1 垃圾收集器的特点:
G1(Garbage First)收集器是 JDK7 提供的⼀个新收集器,在 JDK9 中更被指定为官⽅GC收集器,与CMS收集器相⽐,最突出的改进是:
在介绍G1的垃圾收集流程之前,我们先简单了解下G1中的内存模型以及主要的数据结构,这些数据结果对我们了解G1的垃圾回收流程⼗分重要
⼆、G1 垃圾收集器的内存模型:
G1 收集器不采⽤传统的新⽣代和⽼年代物理隔离的布局⽅式,仅在逻辑上划分新⽣代和⽼年代,将整个堆内存划分为2048个⼤⼩相等的独⽴内存块Region,每个Region是逻辑连续的⼀段内存,具体⼤⼩根据堆的实际⼤⼩⽽定,整体被控制在 1M - 32M 之间,且为2的N次幂(1M、2M、4M、8M、16M和32M),并使⽤不同的Region来表⽰新⽣代和⽼年代,G1不再要求相同类型的 Region 在物理内存上相邻,⽽是通过Region的动态分配⽅式实现逻辑上的连续。
G1收集器通过跟踪Region中的垃圾堆积情况,每次根据设置的垃圾回收时间,回收优先级最⾼的区域,避免整个新⽣代或整个⽼年代的垃圾回收,使得stop the world的时间更短、更可控,同时在有限的时间内可以获得最⾼的回收效率。
通过区域划分和优先级区域回收机制,确保G1收集器可以在有限时间获得最⾼的垃圾收集效率。
1、分区Region:
G1 垃圾收集器将堆内存划分为若⼲个 Region,每个 Region 分区只能是⼀种⾓⾊,Eden区、S区、⽼年代O区的其中⼀个,空⽩区域代表的是未分配的内存,最后还有个特殊的区域H区(Humongous),专门⽤于存放巨型对象,如果⼀个对象的⼤⼩超过Region容量的50%以上,G1 就认为这是个巨型对象。在其他垃圾收集器中,这些巨型对象默认会被分配在⽼年代,但如果它是⼀个短期存活的巨型对象,放⼊⽼年代就会对垃圾收集器造成负⾯影响,触发⽼年代频繁GC。为了解决这个问题,G1划分了⼀个H区专门存放巨型对象,如果⼀个H区装不下巨型对象,那么G1会寻找连续的H分区来存储,如果寻找不到连续的H区的话,就不得不启动 Full GC 了。
2、Remember Set:
在串⾏和并⾏收集器中,GC时是通过整堆扫描来确定对象是否处于可达路径中。然⽽G1为了避免STW式的整堆扫描,为每个分区各⾃分配了⼀个 RSet(Remembered Set),它内部类似于⼀个反向指针,记录了其它 Region 对当前 Region 的引⽤情况,这样就带来⼀个极⼤的好处:回收某个Region时,不需要执⾏全堆扫描,只需扫描它的 RSet 就可以找到外部引⽤,来确定引⽤本分区内的对象是否存活,进⽽确定本分区内的对象存活情况,⽽这些引⽤就是 initial mark 的根之⼀。
事实上,并⾮所有的引⽤都需要记录在RSet中,如果引⽤源是本分区的对象,那么就不需要记录在 RSet 中;同时 G1 每次 GC 时,所有的新⽣代都会被扫描,因此引⽤源是年轻代的对象,也不需要在RSet中记录;所以最终只需要记录⽼年代到新⽣代之间的引⽤即可。
3、Card Table:
如果⼀个线程修改了Region内部的引⽤,就必须要去通知RSet,更改其中的记录。需要注意的是,如果引⽤的对象很多,赋值器需要对每个引⽤做处理,赋值器开销会很⼤,因此 G1 回收器引⼊了 Card Table 解决这个问题。
⼀个 Card Table 将⼀个 Region 在逻辑上划分为若⼲个固定⼤⼩(介于128到512字节之间)的连续区域,每个区域称之为卡⽚Card,因此 Card 是堆内存中的最⼩可⽤粒度,分配的对象会占⽤物理上连续的若⼲个卡⽚,当查找对分区内对象的引⽤时便可通过卡⽚Card 来查找(见RSet),每次对内存的回
收,也都是对指定分区的卡⽚进⾏处理。每个 Card 都⽤⼀个 Byte 来记录是否修改过,Card Table 就是这些 Byte 的集合,是⼀个字节数组,由 Card 的数组下标来标识每个分区的空间地址。默认情况下,每个 Card 都未被引⽤,当⼀个地址空间被引⽤时,这个地址空间对应的数组索引的值被标记为”0″,即标记为脏被引⽤,此外 RSet 也将这个数组下标记录下来。
⼀个Region可能有多个线程在并发修改,因此也可能会并发修改 RSet。为避免冲突,G1垃圾回收器进⼀步把 RSet 划分成了多个HashTable,每个线程都在各⾃的 HashTable ⾥修改。最终,从逻辑上来说,RSet 就是这些 HashTable 的集合。哈希表是实现 RSet 的⼀种常见⽅式,它的好处就是能够去除重复,这意味着,RS的⼤⼩将和修改的指针数量相当,⽽在不去重的情况下,RS的数量和写操作的数量相当。
深居简出
HashTable 的 Key 是别的 Region 的起始地址,Value是⼀个集合,⾥⾯的元素是Card Table的Index。
前⾯三个数据结构的关系如下:
图中RS的虚线表明的是,RSet 并不是⼀个和 Card Table独⽴的、不同的数据结构,⽽是指RS是⼀个概念模型。实际上,Card Table 是 RS 的⼀种实现⽅式。
G1对内存的使⽤以分区(Region)为单位,⽽对对象的分配则以卡⽚(Card)为单位。
4、RSet 的写屏障:
写屏障是指,每次 Reference 引⽤类型在执⾏写操作时,都会产⽣ Write Barrier 写屏障暂时中断操作并额外执⾏⼀些动作。
对写屏障来说,过滤掉不必要的写操作是⼗分有必要的,因为写栅栏的指令开销是⼗分昂贵的,这样既能加快赋值器的速度,也能减轻回收器的负担。G1 收集器的写屏障是跟 RSet 相辅相成的,产⽣写屏障时会检查要写⼊的引⽤指向的对象是否和该 Reference 类型数据在不同的 Region,如果不同,才通过 CardTable 把相关引⽤信息记录到引⽤指向对象的所在 Region 对应的 RSet 中,通过过滤就能使 RSet ⼤⼤减少。
(1)写前栅栏:即将执⾏⼀段赋值语句时,等式左侧对象将修改引⽤到另⼀个对象,那么等式左侧对象原先引⽤的对象所在分区将因此丧失⼀个引⽤,那么JVM就需要在赋值语句⽣效之前,记录丧失引⽤的对象。但JVM并不会⽴即维护RSet,⽽是通过批量处理,在将来RSet更新
(2)写后栅栏:当执⾏⼀段赋值语句后,等式右侧对象获取了左侧对象的引⽤,那么等式右侧对象所在分区的RSet也应该得到更新。同样为了降低开销,写后栅栏发⽣后,RSet也不会⽴即更新,同样只是记录此次更新⽇志,在将来批量处理
G1垃圾回收器进⾏垃圾回收时,在GC根节点枚举范围加⼊RSet,就可以保证不进⾏全局扫描,也不会有遗漏。另外JVM使⽤的其余的分代的垃圾回收器也都有写屏障,举例来说,每次将⼀个⽼年代对
象的引⽤修改为指向年轻代对象,都会被写屏障捕获并记录下来,因此在年轻代回收的时候,就可以避免扫描整个⽼年代来查找根。
写个人自传G1的垃圾回收器的写屏障使⽤⼀种两级的log buffer结构:
global t of filled buffer:所有线程共享的⼀个全局的,存放填满了的log buffer的集合
thread log buffer:每个线程⾃⼰的log buffer。所有的线程都会把写屏障的记录先放进去⾃⼰的log buffer中,装满了之后,就会把log buffer放到 global t of filled buffer中,⽽后再申请⼀个log buffer;
5、Collect Set:
Collect Set(CSet)是指,在 Evacuation 阶段,由G1垃圾回收器选择的待回收的Region集合,在任意⼀次收集器中,CSet 所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。G1 的软实时性就是通过CSet的选择来实现的,对应于算法的两种模式fully-young generational mode 和 partially-young mode,CSet的选择可以分成两种:
1. fully-young generational mode:也称young GC,该模式下CSet将只包含 young region,G1通过调整新⽣代的 region 的数量
来匹配软实时的⽬标;
2. partially-young mode:也称 Mixed GC,该模式会选择所有的 young region,并且选择⼀部分的 old region,old region 的选择
将依据在Marking cycle pha中对存活对象的计数,筛选出回收收益最⾼的分区添加到CSet中(存活对象最少的Region进⾏回收)
候选⽼年代分区的CSet准⼊条件,可以通过活跃度阈值 -XX:G1MixedGCLiveThresholdPercent(默认85%) 进⾏设置,从⽽拦截那些回收开销巨⼤的对象;同时,每次混合收集可以包含候选⽼年代分区,可根据CSet对堆的总⼤⼩占⽐ -
XX:G1OldCSetRegionThresholdPercent(默认10%) 设置数量上限。
由上述可知,G1的收集都是根据CSet进⾏操作的,年轻代收集与混合收集没有明显的不同,最⼤的区别在于两种收集的触发条件。
三、G1的的垃圾收集过程:
G1提供了两种GC模式,Young GC 和 Mixed GC,两种都是Stop The World(STW)的,不过讲垃圾回收之前,我们先介绍下 G1的对象分配策略。
1、对象分配策略:
每⼀个分配的 Region 都可以分成两个部分,已分配的和未被分配的,它们之间的界限被称为top。总体上来说,把⼀个对象分配到Region内,只需要简单增加top的值。过程如下:
(1)线程本地分配缓冲区 Thread Local allocation buffer (TLab):
如果对象在⼀个共享的空间中分配,那么我们就需要采⽤同步机制来解决并发冲突问题,⽽为了减少并发冲突损耗的同步时间,G1 为每个应⽤线程和GC线程分配了⼀个本地分配缓冲区TLAB,分配对象内存时,就在这个 buffer 内分配,线程之间不再需要进⾏任何的同步,提⾼GC效率。但是当线程耗尽了⾃⼰的Buffer之后,需要申请新的Buffer。这个时候依然会带来并发的问题。G1回收器采⽤的是CAS(Compate And Swap)操作。
显然的,采⽤TLAB的技术,就会带来碎⽚。举例来说,当⼀个线程在⾃⼰的Buffer⾥⾯分配的时候,
虽然Buffer⾥⾯还有剩余的空间,但是却因为分配的对象过⼤以⾄于这些空闲空间⽆法容纳,此时线程只能去申请新的Buffer,⽽原来的Buffer中的空闲空间就被浪费了。Buffer的⼤⼩和线程数量都会影响这些碎⽚的多寡。
每次垃圾收集时,每个GC线程同样可以独占⼀个本地缓冲区(GCLAB)⽤来转移对象,将存活对象复制到Suvivor空间或⽼年代空间;
对于从Eden/Survivor空间晋升(Promotion)到Survivor/⽼年代空间的对象,同样有GC独占的本地缓冲区进⾏操作,该部分称为晋升本地缓冲区(PLAB)。
(2)Eden区中分配:
对TLAB空间中⽆法分配的对象,JVM会尝试在Eden空间中进⾏分配。如果Eden空间⽆法容纳该对象,就只能在⽼年代中进⾏分配空间。
(3)Humongous区分配:
巨型对象会独占⼀个、或多个连续分区,其中第⼀个分区被标记为开始巨型(StartsHumongous),相邻连续分区被标记为连续巨型(ContinuesHumongous)。由于⽆法享受 TLab 带来的优化,并且确定⼀⽚连续的内存空间需要扫描整堆,因此确定巨型对象开始位置的成本⾮常⾼,如果可以,应⽤程序应
避免⽣成巨型对象。
G1内部做了⼀个优化,⼀旦发现没有引⽤指向巨型对象,则可直接在年轻代收集周期中被回收。
2、G1 Young GC:
当Eden区已满,JVM分配对象到Eden区失败时,便会触发⼀次STW式的年轻代收集young GC,将 Eden 区存活的对象将被拷贝到to survivor 区;from survivor 区存活的对象则根据存活次数阈值分别晋升到 PLAB、to survivor 区和⽼年代中;如果 survivor 空间不够,Eden区的部分数据会直接晋升到年⽼代空间。最终Eden空间的数据为空,GC停⽌⼯作,应⽤线程继续执⾏。
young GC 还负责维护对象的年龄(存活次数),辅助判断⽼化(tenuring)对象晋升时的去向。young GC ⾸先将晋升对象尺⼨总和、年龄信息维护到年龄表中,再根据年龄表、Survivor尺⼨、Survivor填充容量 -XX:TargetSurvivorRatio(默认50%)、最⼤任期阈值 -XX:MaxTenuringThreshold(默认15),计算出⼀个恰当的任期阈值,凡是超过任期阈值的对象都会被晋升到⽼年代。
这时,我们需要考虑⼀个问题,如果仅仅 GC 新⽣代对象,我们如何找到所有的根对象呢? ⽼年代的所有对象都是根么?那这样扫描下来会耗费⼤量的时间。于是就需要使⽤到我们上⽂介绍到的 RSet 了,RSet 中记录了其他 region 对当前 region 的引⽤,因此,在进⾏Young GC 时,扫描根时,仅仅需要扫描这⼀块区域,⽽不需要扫描整个⽼年代。
2.1、young GC的详细回收过程:
(1)第⼀阶段,根扫描:高考励志语录
根是指static变量指向的对象、正在执⾏的⽅法调⽤链上的局部变量等。根引⽤连同 RSet 记录的外部引⽤作为扫描存活对象的⼊⼝。(2)第⼆阶段,更新RSet:
处理 dirty card 队列中的 card,更新 RSet,此阶段完成后,RSet 可以准确的反映⽼年代对所在的region 分区中对象的引⽤
法国甜点(3)第三阶段:处理RSet:
识别被⽼年代对象指向的 Eden 中的对象,这些被指向的Eden中的对象被认为是存活的对象
(4)第四阶段:对象拷贝:
将 Eden 区存活的对象将被拷贝到 to survivor 区;from survivor 区存活的对象则根据存活次数阈值分别晋升到 PLAB、to survivor 区和⽼年代中;如果 survivor 空间不够,Eden区的部分数据会直接晋升到年⽼代空间。
(5)第五阶段:处理引⽤:
处理软引⽤、弱引⽤、虚引⽤,最终Eden空间的数据为空,GC停⽌⼯作,⽽⽬标内存中的对象都是连续存储的、没有碎⽚,所以复制过程可以达到内存整理的效果,减少碎⽚。
3、G1 Mixed GC:
年轻代不断进⾏垃圾回收活动后,为了避免⽼年代的空间被耗尽。当⽼年代占⽤空间超过整堆⽐ IHOP 阈值 -
XX:InitiatingHeapOccupancyPercent(默认45%)时,G1就会启动⼀次混合垃圾回收Mixed GC,Mixed GC不仅进⾏正常的新⽣代垃圾收集,同时也回收部分后台扫描线程标记的⽼年代分区。Mixed GC步骤主要分为两步:
好词好句二年级
(1)全局并发标记(global concurrent marking)
(2)拷贝存活对象(evacuation)童话故事作文100字
这⾥需要特别注意的是 Mixed GC 并不是 Full GC,只有当 Mixed GC 来不及回收old region,也就说在需要分配⽼年代的对象时,但发现没有⾜够的空间,这个时候就会触发⼀次 Full GC
3.1、全局并发标记(global concurrent marking)
在进⾏混合回收前,会先进⾏ global concurrent marking,在 G1 GC 中,它并不是⼀次GC过程的必须环节,主要是为 Mixed GC 提供标记服务的。global concurrent marking的执⾏过程分为五个步骤:
>关于元旦

本文发布于:2023-05-28 21:53:36,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/fan/82/802316.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:对象   垃圾   回收
相关文章
留言与评论(共有 0 条评论)
   
验证码:
推荐文章
排行榜
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图