JVM G1(Garbage First)垃圾收集器

14 篇文章 0 订阅

本文参考:https://blog.csdn.net/coderlius/article/details/79272773

G1收集器

  • 概述:即可以收集新生代,也可以收集老年代;G1最大的特点是引入分区的思路,弱化了分代的概念。

之前的几个垃圾收集器组合都有几个共同的特点:

  • 年轻代、老年代是独立且连续的内存块;
  • 年轻代收集使用单eden、双survivor进行复制算法;
  • 老年代收集必须扫描整个老年代区域;
  • 都是以尽可能少而快地执行GC为设计原则。

G1的不同

  • G1的设计原则是首先收集尽可能多的垃圾,因此G1并不会等内存耗尽或者快耗尽的时候开始收集垃圾,而是内部采用启发式算法,在老年代找出具有高收集收益的分区进行收集,同时G1可以根据用户设置的暂停时间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间越大;
  • G1采用内存分区的思路,将内存划分为一个个大小相等的内存分区,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区进行操作,因此G1天然就是一种压缩方案;
  • G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代和老年代的区别,也不需要完全独立的survivor堆作为复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换;
  • G1的收集都是STW的,但是年轻代和老年代的收集界限比较模糊,采用了混合收集的方式,即每次收集既可能只收集年轻代分区,也可能在收集年轻代分区的同时,包含部分的老年代分区,这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。
    在这里插入图片描述
    卡片
    在每个分区内部又被分为若干个大小为512Byte卡片,标识堆内存最小可用粒度,所有分区的卡片将会记录在全局卡片表中,分配的对象会占用物理上连续的若干卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象。每次对内存的回收,都是对指定分区的卡片进行处理。


当发生年轻代收集或混合收集时,通过计算GC与应用的耗费时间比,自动调整对空间大小。如果GC频率太高,则通过增加堆尺寸来减少GC频率,相应第GC占用的时间也随之降低;目标参数-XX:GCTimeRatio即为GC与应用的耗费时间比,G1默认为9,而CMS默认为99,因为CMS的设计原则是耗费在GC上的时间尽可能的少。另外,当空间不足时,如对象空间分配或转移失败时,G1会首先尝试增加堆空间,如果扩容失败,则发起担保Full GC。Full GC后,堆尺寸计算结果也会调整堆空间。

分代
分代垃圾收集可以将关注点集中在最近被分配的对象上,而无需整堆扫描,避免长命对象的拷贝,同时独立收集有助于降低响应时间,虽然分区使得内存分配不再要求紧凑的内存空间,但G1依然使用了分代的思想,与其它分代规则一样,但是G1的年轻代空间并不是固定不变的,当现有年轻代分区占满时,JVM会分配新的空闲分区加入到年轻代空间中,大小在初始空间(默认为整堆5%)-(默认整堆60%)之间动态变化,且由参数目标暂停时间(默认200ms)、需要扩容的大小以及分区的已记忆集合计算得到。

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

由于分区的思想,每个线程均可以"认领"某个分区用于线程本地的内存分配,而不需要顾及分区是否连续。因此每个应用线程和GC线程都会独立的使用分区,进而减少同步时间,提升GC效率,这个分区称为本地分配缓冲区

其中,应用线程可以独占一个本地缓冲区来创建对象,而大部分都会落入Eden区(巨型对象或分配失败除外),因此TLAB的分区属于Eden空间;而每次垃圾收集时,每个GC线程同样可以独占一个本地缓冲区用来转移对象,每次回收会将对象复制到Survivor空间或老年代空间;对于从Eden/Survivor空间晋升到Survivor/老年代空间的对象,同样有GC独占本地缓存区进行操作,该部分称为晋升本地缓冲区(PLAB)

分区模型

在这里插入图片描述
G1对内存的使用以分区为单位,而对对象的分配则以卡片为单位。

巨型对象 Humongous Region

一个大小达到甚至超过分区大小一半的对象称为巨型对象,当线程为巨型对象分配空间时,不能简单地在TLAB进行分配,因为巨型对象的移动成本很高,而且有可能一个分区不能容纳巨型对象,因此,巨型对象会直接在老年代分配,所占用的连续空间称为巨型分区(Humongous Region),G1内部做了一个优化,一旦发现没有引用指向巨型对象,则可以直接在年轻代收集周期中被回收。
巨型对象会独占一个或多个连续分区,其中第一个分区被标记为开始巨型,相邻连续分区被标记为连续巨型,由于无法享受Lab带来的优化,并且确定一片连续空间需要进行整堆扫描,所以成本非常高,应该避免产生巨型对象。

已记忆集合 Remember Set
在串行和并行收集器中,GC通过整堆扫描来确定对象是否处于可达路径中,然而G1为了避免STW式的整堆扫描,在每个分区中记录了一个已记忆集合,内部类似一个反向指针,记录引用分区内对象的卡片索引。当要回收该分区时,通过扫描分区的Rset,来确定引用本分区的对象是否存活,进而确定本分区内的对象存活情况。
事实上并非所有的引用都需要记录在RSet中,如果一个分区确定需要扫描,那么无需RSet也可以无遗漏的得到引用关系。如果引用源自本分区的对象,当然不用落入RSet;同时 G1 GC每次都会对年轻代进行整体收集,因此引用源自年轻代的对象,也不需要在RSet中记录。最后只有老年代的分区可能会有RSet记录,这些分区称为拥有RSet分区

Per Region Table

RSet在内部使用Per Region Table记录分区的引用情况,由于RSet的记录要占用分区的空间,如果一个分区非常受欢迎,那么RSet占用的空间会上升,从而降低分区的可用空间,采用了改变RSet的密度的方式。

  • 稀少:直接记录引用对象的卡片索引
  • 细粒度:记录引用对象的分区索引
  • 粗粒度:只记录引用情况,每个分区对应一个比特位,只记录了引用的数量,需要通过整堆扫描才能找出所有引用。

收集集合CSet

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

候选老年代分区的CSet准入条件可以通过活跃度阈值-XX:G1MixedGCLiveThresholdPercent默认为85%进行设置(也就是如果一个分区中存活的对象超过85%则这个分区不做为候选分区,不考虑回收),从而拦截那些回收开销巨大的对象,同时,每次混合收集可以包含候选老年代分区,可根据CSet对堆的总大小占比-XX:G1OldCSetRegionThresholdPercent设置数量上限。

** 新生代收集集合 CSet of Young Collection**

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

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

混合收集集合 CSet of Mixed Collection

年轻代收集不断活动后,老年代的空间也会逐渐被填充。当老年代占用空间超过整堆IHOP(默认45%)时,G1就会启动一次混合垃圾收集周期。为了满足暂停目标,G1不可能一口气将所有候选分区收集掉,因此G1可能会产生连续多次混合收集与应用线程交替执行,每次STW的混合收集与年轻代收集的过程类似。
为了确定,包含到年轻代收集集合CSet的老年代分区,JVM通过参数混合周期的最大总次数XX:G1MixedGCCountTarget(默认8)、堆废物百分比-XX:G1HeapWastePercent(默认为5%)。通过候选老年代分区总数与混合周期最大总次数,确定每次包含到CSet的最小年轻代分区数量;根据堆废物百分比,当收集达到参数时,不再启动新的混合收集,而每次添加到CSet的分区,则通过计算得到的GC效率进行安排。

G1的活动周期

在这里插入图片描述

RSet的维护

由于不能整堆扫描,又需要分区确切的活跃度,因此G1需要一个增量式的完全标记并发算法,通过维护RSet,得到准确的分区引用信息。在G1中,RSet的维护主要来源两方面:写栅栏和并发优化线程;

栅栏
是指在原生代码片段中,当某些语句被执行时,栅栏代码也会被执行,而G1主要在赋值语句,使用写前栅栏和写后栅栏,事实上,写栅栏的指令序列开销非常昂贵,应用吞吐量也会根据栅栏复杂度降低。

写前栅栏Pre-Write Barrier
即将执行一段赋值语句时,等式左侧对象将修改引用到另一个对象,那么等式左侧对象原先引用的对象所在分区将因此丧失一个引用,那么JVM就需要在赋值语句生效前,记录丧失引用的对象,JVM并不会立即维护RSet,而是通过批量处理,在将来RSet更新。

写后栅栏 Post-Write Barrier
当执行一段赋值语句后,等式右侧对象获取了左侧对象的引用,那么等式右侧对象所在的分区的RSet也应该得到更新。同样为了降低开销,写后栅栏发生后,Rset也不会立即更新,同样只是记录此次更新日志,在将来批量处理。

起始快照算法 Snapshot at the beginning(SATB)
完全并发标记算法:起始快照算法,主要针对标记-清除垃圾收集器的并发标记阶段,非常适合G1的分区块的堆结构,同时解决了CMS的主要烦恼:重新标记暂停时间长带来的潜在风险。

SATB会创建一个对象图,相当于堆的逻辑快照,从而确保并发标记阶段所有垃圾对象都能通过快照被鉴别出来。当赋值语句发生时,应用将会改变它的对象图,那么JVM需要记录被覆盖的对象,因此写前栅栏会在引用变更前将值记录在SATB日志或缓冲区中。每个线程都独占一个SATB缓冲区,初始有256条记录空间,当空间用尽时,线程会分配新的SATB缓冲区继续使用,而原有的缓冲区则加入全部列表中。最终在并发标记阶段,并发标记线程在标记的同时,还会定期检查和处理全局缓冲区列表的记录,然后根据标记图分片的标记位扫描引用字段来更新RSet。此过程又称为并发标记/SATB写前栅栏

并发优化线程 Concurrentce Refinement Threads
当赋值语句发生后,写后栅栏会先通过G1的过滤技术判断是否是跨分区的引用更新并将跨分区更新对象的卡片加入缓冲区序列,即更新日志缓冲区或脏卡片队列。与SATB类似,一旦日志缓冲区用尽,则分配一个新的日志缓冲区,并将原来的缓冲区加入全部列表中。
并发线程优化只专注于日志缓冲区记录的卡片来维护更新RSet,线程最大数目可以通过-XX:G1ConcRefinementThreads(默认等于-XX:ParellelGCThreads)设置。并发优化线程永远是活跃的,一旦发现全局列表有记录存在,就开始并发处理。如果记录增长很快或者来不及处理,G1会用分层的方式调度,使更多的线程处理全局列表,如果并发优化线程也不能跟上缓冲区数量,则Java应用线程会挂起应用并被加进来帮助处理,直到全部处理完,因此,应该避免此类场景出现。

并发标记周期 Concurrent Marking Cycle

这个阶段将会为混合收集周期识别垃圾最多的老年代分区,整个周期完成根标记、识别所有可能存活对象,并及计算每个分区的活跃度,从而确定GC效率等级。
当达到IHOP(初始堆占用比,老年代占整堆比 默认为45%)阈值时便会触发并发标记周期。整个并发标记周期将由初始标记、根分区扫描、并发标记、重新标记、清除这几个阶段组成。其中初始标记、重新标记、清除是STW的,而并发标记如果来不及标记存活对象,则可能在并发标记过程中,G1又触发了几次年轻代收集。

并发标记线程 Concurrent Marking Threads
在这里插入图片描述
每个分区都需要创建位图来存储标记数据(标记存活的对象),来确定标记周期内被分配的对象。

  • Previous Bitmap:记录上一个并发标记周期的标记数据;
  • Next Bitmap:记录当前并发标记周期标记的数据;
  • Previous TAMS(PTAMS)、Next TAMS(NTAMS)记录已标记的范围;

每个并发标记周期,在初始标记STW的最后,G1会分配一个空的Next位图和一个指向分区顶部(Top)的NTAMS标记。在PTAMS与分区底部Bottom的范围内,所有存活对象都已被标记,PTAMS与Top之间都是隐式存活对象。在并发标记阶段,Next位图吸收了Previous位图的标记数据,同时每个分区都会有新的对象分配,则Top和NTAMS分离,前往更高的地址空间。在并发标记的一次标记中,并发标记线程将找出NTAMS与PTAMS之间的所有存活对象,将标记数据存储在Next位图中。同时,在NTAMS与Top之间的对象即将成为已标记对象。如此不断地更新Next位图信息,并在清除阶段与Previous位图交换角色。

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

根分区扫描 Root Region Scanning

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

并发标记 Concurrent Marking

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

存活对象数据数据计算 Live Data Accounting

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

重新标记 Remark
重新标记是最后一个标记阶段,在该阶段G1需要一个暂停的时间,去处理剩下的SATB日志缓冲区和所有更新,找出所有未被访问的存活对象,同时安全完成存活数据计算。这个阶段也是并行执行的,同时引用处理也是重新标记阶段的一部分,所有重度使用引用对象的应用都会在引用处理上产生开销。

清除 Clean Up
紧挨着重新标记阶段的清除阶段也是STW的,Previous/Next标记位图、以及PTAMS/NTAMS都会在清除阶段交换角色。

  1. RSet梳理,启发式算法会根据活跃度和RSet尺寸对分区定义不同等级,同时RSet梳理也有助于发现无用的引用;
  2. 整理堆分区,为混合收集周期识别回收收益的老年代分区集合;
  3. 识别所有空闲分区,即发现无存活对象的分区。该分区可在清除阶段直接回收,无需等下次收集周期;

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

转移的担保机制Full GC

转移失败是指G1无法在堆空间中申请新的分区时,G1便会触发担保机制,执行一次STW式、单线程的Full GC,会对整堆做标记清除和压缩,最后只包含纯粹的存活对象。

  • 在年轻代分区拷贝存活对象,无法找到可用空闲分区;
  • 在老年代分区转移存活对象,无法找到可用的空闲分区;
  • 分配巨型对象时在老年代无法找到足够的连续分区。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
JVM (Java Virtual Machine) G1 (Garbage-First) 垃圾收集器是一种用于 Java 应用程序的垃圾收集算法。它是自JDK 7u4版本后引入的一种全新的垃圾收集器G1垃圾收集器的设计目标是为了解决传统的分代垃圾收集器可能遇到的一些问题,如停顿时间长、内存碎片化等。它采用了一种基于区域的垃圾收集方式,可以将内存划分为多个大小相等的区域,每个区域可以是Eden、Survivor或Old区。 G1垃圾收集器的工作原理如下: 1. 初始标记(Initial Mark):标记所有从根对象直接可达的对象。 2. 并发标记(Concurrent Mark):在并发执行程序的同时,标记那些在初始标记阶段无法访问到的对象。 3. 最终标记(Final Mark):为并发标记阶段中发生改变的对象进行最终标记。 4. 筛选回收(Live Data Counting and Evacuation):根据各个区域的回收价值来优先回收价值低的区域。 G1垃圾收集器具有以下特点: - 并发执行:在执行垃圾收集过程时,尽可能减少应用程序的停顿时间。 - 分区回收:将整个堆划分为多个区域,可以根据需要优先回收垃圾较多的区域,从而避免全堆回收带来的长时间停顿。 - 内存整理:G1垃圾收集器会对内存进行整理,减少内存碎片化,提高内存利用率。 需要注意的是,G1垃圾收集器并不适用于所有情况。在特定的场景下,如大堆情况下的长时间运行、对延迟要求非常高的应用等,可能需要考虑其他垃圾收集器的使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值