garbage

更新时间:2022-12-26 17:31:55 阅读: 评论:0


2022年12月26日发(作者:uniqueness)

详解JVMGarbageFirst(G1)垃圾收集器

前⾔

GarbageFirst(G1)是垃圾收集领域的最新成果,同时也是HotSpot在JVM上⼒推的垃圾收集器,并赋予取代CMS的使命。如果使⽤

Java8/9,那么有很⼤可能希望对G1收集器进⾏评估。本⽂详细⾸先对JVM其他的垃圾收集器进⾏总结,并与G1进⾏了简单的对

⽐;然后通过G1的内存模型、G1的活动周期,对G1的⼯作机制进⾏了介绍;同时还在介绍过程中,描述了可能需要引起注意的优化

点。笔者希望通过本⽂,让有⼀定JVM基础的读者能尽快掌握G1的知识点。

第⼀章概述

G1(GarbageFirst)垃圾收集器是当今垃圾回收技术最前沿的成果之⼀。早在JDK7就已加⼊JVM的收集器⼤家庭中,成为HotSpot重点发

展的垃圾回收技术。同优秀的CMS垃圾回收器⼀样,G1也是关注最⼩时延的垃圾回收器,也同样适合⼤尺⼨堆内存的垃圾收集,官⽅也推

荐使⽤G1来代替选择CMS。G1最⼤的特点是引⼊分区的思路,弱化了分代的概念,合理利⽤垃圾收集各个周期的资源,解决了其他收集器

甚⾄CMS的众多缺陷。

第⼆章JVMGC收集器的回顾与⽐较

从JDK3(1.3)开始,HotSpot团队⼀直努⼒朝着⾼效收集、减少停顿(STW:StopTheWorld)的⽅向努⼒,也贡献了从串⾏到CMS乃⾄最

新的G1在内的⼀系列优秀的垃圾收集器。上图展⽰了JDK的垃圾回收⼤家庭,以及相互之间的组合关系,下⾯就⼏种典型的组合应⽤进⾏

简单的介绍。

串⾏收集器

串⾏收集器组合Serial+SerialOld

开启选项:-XX:+SerialGC

串⾏收集器是最基本、发展时间最长、久经考验的垃圾收集器,也是client模式下的默认收集器配置。

串⾏收集器采⽤单线程stop-the-world的⽅式进⾏收集。当内存不⾜时,串⾏GC设置停顿标识,待所有线程都进⼊安全点(Safepoint)时,

应⽤线程暂停,串⾏GC开始⼯作,采⽤单线程⽅式回收空间并整理内存。单线程也意味着复杂度更低、占⽤内存更少,但同时也意味着不

能有效利⽤多核优势。事实上,串⾏收集器特别适合堆内存不⾼、单核甚⾄双核CPU的场合。

并⾏收集器

并⾏收集器组合ParallelScavenge+ParallelOld

开启选项:-XX:+UParallelGC或-XX:+UParallelOldGC(可互相激活)

并⾏收集器是以关注吞吐量为⽬标的垃圾收集器,也是rver模式下的默认收集器配置,对吞吐量的关注主要体现在年轻代Parallel

Scavenge收集器上。

并⾏收集器与串⾏收集器⼯作模式相似,都是stop-the-world⽅式,只是暂停时并⾏地进⾏垃圾收集。年轻代采⽤复制算法,⽼年代采⽤标

记-整理,在回收的同时还会对内存进⾏压缩。关注吞吐量主要指年轻代的ParallelScavenge收集器,通过两个⽬标参数-

XX:MaxGCPauMills和-XX:GCTimeRatio,调整新⽣代空间⼤⼩,来降低GC触发的频率。并⾏收集器适合对吞吐量要求远远⾼于延迟要求的

场景,并且在满⾜最差延时的情况下,并⾏收集器将提供最佳的吞吐量。

并发标记清除收集器

并发标记清除收集器组合ParNew+CMS+SerialOld

开启选项:-XX:+UConcMarkSweepGC

并发标记清除(CMS)是以关注延迟为⽬标、⼗分优秀的垃圾回收算法,开启后,年轻代使⽤STW式的并⾏收集,⽼年代回收采⽤CMS进⾏

垃圾回收,对延迟的关注也主要体现在⽼年代CMS上。

年轻代ParNew与并⾏收集器类似,⽽⽼年代CMS每个收集周期都要经历:初始标记、并发标记、重新标记、并发清除。其中,初始标记以

STW的⽅式标记所有的根对象;并发标记则同应⽤线程⼀起并⾏,标记出根对象的可达路径;在进⾏垃圾回收前,CMS再以⼀个STW进⾏

重新标记,标记那些由mutator线程(指引起数据变化的线程,即应⽤线程)修改⽽可能错过的可达对象;最后得到的不可达对象将在并发清

除阶段进⾏回收。值得注意的是,初始标记和重新标记都已优化为多线程执⾏。CMS⾮常适合堆内存⼤、CPU核数多的服务器端应⽤,也

是G1出现之前⼤型应⽤的⾸选收集器。

但是CMS并不完美,它有以下缺点:

1.由于并发进⾏,CMS在收集与应⽤线程会同时会增加对堆内存的占⽤,也就是说,CMS必须要在⽼年代堆内存⽤尽之前完成垃圾回

收,否则CMS回收失败时,将触发担保机制,串⾏⽼年代收集器将会以STW的⽅式进⾏⼀次GC,从⽽造成较⼤停顿时间;

2.标记清除算法⽆法整理空间碎⽚,⽼年代空间会随着应⽤时长被逐步耗尽,最后将不得不通过担保机制对堆内存进⾏压缩。CMS也提

供了参数-XX:CMSFullGCsBeForeCompaction(默认0,即每次都进⾏内存整理)来指定多少次CMS收集之后,进⾏⼀次压缩的FullGC。

GarbageFirst

GarbageFirst(G1)

开启选项:-XX:+UG1GC

之前介绍的⼏组垃圾收集器组合,都有⼏个共同点:

1.年轻代、⽼年代是独⽴且连续的内存块;

2.年轻代收集使⽤单eden、双survivor进⾏复制算法;

3.⽼年代收集必须扫描整个⽼年代区域;

4.都是以尽可能少⽽块地执⾏GC为设计原则。

G1垃圾收集器也是以关注延迟为⽬标、服务器端应⽤的垃圾收集器,被HotSpot团队寄予取代CMS的使命,也是⼀个⾮常具有调优潜⼒的

垃圾收集器。虽然G1也有类似CMS的收集动作:初始标记、并发标记、重新标记、清除、转移回收,并且也以⼀个串⾏收集器做担保机

制,但单纯地以类似前三种的过程描述显得并不是很妥当。事实上,G1收集与以上三组收集器有很⼤不同:

1.G1的设计原则是"⾸先收集尽可能多的垃圾(GarbageFirst)"。因此,G1并不会等内存耗尽(串⾏、并⾏)或者快耗尽(CMS)的时候开

始垃圾收集,⽽是在内部采⽤了启发式算法,在⽼年代找出具有⾼收集收益的分区进⾏收集。同时G1可以根据⽤户设置的暂停时间⽬

标⾃动调整年轻代和总堆⼤⼩,暂停⽬标越短年轻代空间越⼩、总空间就越⼤;

2.G1采⽤内存分区(Region)的思路,将内存划分为⼀个个相等⼤⼩的内存分区,回收时则以分区为单位进⾏回收,存活的对象复制到另

⼀个空闲分区中。由于都是以相等⼤⼩的分区为单位进⾏操作,因此G1天然就是⼀种压缩⽅案(局部压缩);

3.G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与⽼年代的区别,也不需要完全独⽴的survivor(tospace)堆做复制

准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运⾏在不同代之间前后切换;

4.G1的收集都是STW的,但年轻代和⽼年代的收集界限⽐较模糊,采⽤了混合(mixed)收集的⽅式。即每次收集既可能只收集年轻代分

区(年轻代收集),也可能在收集年轻代的同时,包含部分⽼年代分区(混合收集),这样即使堆内存很⼤时,也可以限制收集范围,从⽽

降低停顿。

第三章G1的内存模型

分区概念

分区

分区Region

G1采⽤了分区(Region)的思路,将整个堆空间分成若⼲个⼤⼩相等的内存区域,每次分配对象空间将逐段地使⽤内存。因此,在堆的使⽤

上,G1并不要求对象的存储⼀定是物理上连续的,只要逻辑上连续即可;每个分区也不会确定地为某个代服务,可以按需在年轻代和⽼年

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

区。

卡⽚

卡⽚Card

在每个分区内部⼜被分成了若⼲个⼤⼩为512Byte卡⽚(Card),标识堆内存最⼩可⽤粒度所有分区的卡⽚将会记录在全局卡⽚表(Global

CardTable)中,分配的对象会占⽤物理上连续的若⼲个卡⽚,当查找对分区内对象的引⽤时便可通过记录卡⽚来查找该引⽤对象(见

RSet)。每次对内存的回收,都是对指定分区的卡⽚进⾏处理。

堆Heap

G1同样可以通过-Xms/-Xmx来指定堆空间⼤⼩。当发⽣年轻代收集或混合收集时,通过计算GC与应⽤的耗费时间⽐,⾃动调整堆空间⼤

⼩。如果GC频率太⾼,则通过增加堆尺⼨,来减少GC频率,相应地GC占⽤的时间也随之降低;⽬标参数-XX:GCTimeRatio即为GC与应⽤

的耗费时间⽐,G1默认为9,⽽CMS默认为99,因为CMS的设计原则是耗费在GC上的时间尽可能的少。另外,当空间不⾜,如对象空间

分配或转移失败时,G1会⾸先尝试增加堆空间,如果扩容失败,则发起担保的FullGC。FullGC后,堆尺⼨计算结果也会调整堆空间。

分代模型

分代

分代Generation

分代垃圾收集可以将关注点集中在最近被分配的对象上,⽽⽆需整堆扫描,避免长命对象的拷贝,同时独⽴收集有助于降低响应时间。虽然

分区使得内存分配不再要求紧凑的内存空间,但G1依然使⽤了分代的思想。与其他垃圾收集器类似,G1将内存在逻辑上划分为年轻代和⽼

年代,其中年轻代⼜划分为Eden空间和Survivor空间。但年轻代空间并不是固定不变的,当现有年轻代分区占满时,JVM会分配新的空闲

分区加⼊到年轻代空间。

整个年轻代内存会在初始空间-XX:G1NewSizePercent(默认整堆5%)与最⼤空间-XX:G1MaxNewSizePercent(默认60%)之间动态变化,且由参

数⽬标暂停时间-XX:MaxGCPauMillis(默认200ms)、需要扩缩容的⼤⼩以及分区的已记忆集合(RSet)计算得到。当然,G1依然可以设置

固定的年轻代⼤⼩(参数-XX:NewRatio、-Xmn),但同时暂停⽬标将失去意义。

本地分配缓冲

本地分配缓冲Localallocationbuffer(Lab)

值得注意的是,由于分区的思想,每个线程均可以"认领"某个分区⽤于线程本地的内存分配,⽽不需要顾及分区是否连续。因此,每个应⽤

线程和GC线程都会独⽴的使⽤分区,进⽽减少同步时间,提升GC效率,这个分区称为本地分配缓冲区(Lab)。

其中,应⽤线程可以独占⼀个本地缓冲区(TLAB)来创建的对象,⽽⼤部分都会落⼊Eden区域(巨型对象或分配失败除外),因此TLAB的分

区属于Eden空间;⽽每次垃圾收集时,每个GC线程同样可以独占⼀个本地缓冲区(GCLAB)⽤来转移对象,每次回收会将对象复制到

Suvivor空间或⽼年代空间;对于从Eden/Survivor空间晋升(Promotion)到Survivor/⽼年代空间的对象,同样有GC独占的本地缓冲区进

⾏操作,该部分称为晋升本地缓冲区(PLAB)。

分区模型

G1对内存的使⽤以分区(Region)为单位,⽽对对象的分配则以卡⽚(Card)为单位。

巨型对象

巨型对象HumongousRegion

⼀个⼤⼩达到甚⾄超过分区⼤⼩⼀半的对象称为巨型对象(HumongousObject)。当线程为巨型分配空间时,不能简单在TLAB进⾏分配,

因为巨型对象的移动成本很⾼,⽽且有可能⼀个分区不能容纳巨型对象。因此,巨型对象会直接在⽼年代分配,所占⽤的连续空间称为巨型

分区(HumongousRegion)。G1内部做了⼀个优化,⼀旦发现没有引⽤指向巨型对象,则可直接在年轻代收集周期中被回收。

巨型对象会独占⼀个、或多个连续分区,其中第⼀个分区被标记为开始巨型(StartsHumongous),相邻连续分区被标记为连续巨型

(ContinuesHumongous)。由于⽆法享受Lab带来的优化,并且确定⼀⽚连续的内存空间需要扫描整堆,因此确定巨型对象开始位置的成

本⾮常⾼,如果可以,应⽤程序应避免⽣成巨型对象。

已记忆集合

已记忆集合RememberSet(RSet)

在串⾏和并⾏收集器中,GC通过整堆扫描,来确定对象是否处于可达路径中。然⽽G1为了避免STW式的整堆扫描,在每个分区记录了⼀个

已记忆集合(RSet),内部类似⼀个反向指针,记录引⽤分区内对象的卡⽚索引。当要回收该分区时,通过扫描分区的RSet,来确定引⽤本

分区内的对象是否存活,进⽽确定本分区内的对象存活情况。

事实上,并⾮所有的引⽤都需要记录在RSet中,如果⼀个分区确定需要扫描,那么⽆需RSet也可以⽆遗漏的得到引⽤关系。那么引⽤源⾃

本分区的对象,当然不⽤落⼊RSet中;同时,G1GC每次都会对年轻代进⾏整体收集,因此引⽤源⾃年轻代的对象,也不需要在RSet中记

录。最后只有⽼年代的分区可能会有RSet记录,这些分区称为拥有RSet分区(anRSet’sowningregion)。

PerRegionTable

PerRegionTable(PRT)

RSet在内部使⽤PerRegionTable(PRT)记录分区的引⽤情况。由于RSet的记录要占⽤分区的空间,如果⼀个分区⾮常"受欢迎",那么

RSet占⽤的空间会上升,从⽽降低分区的可⽤空间。G1应对这个问题采⽤了改变RSet的密度的⽅式,在PRT中将会以三种模式记录引⽤:

稀少:直接记录引⽤对象的卡⽚索引

细粒度:记录引⽤对象的分区索引

粗粒度:只记录引⽤情况,每个分区对应⼀个⽐特位

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

收集集合(CSet)

收集集合CSet

收集集合(CSet)代表每次GC暂停时回收的⼀系列⽬标分区。在任意⼀次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转

移到分配的空闲分区中。因此⽆论是年轻代收集,还是混合收集,⼯作的机制都是⼀致的。年轻代收集CSet只容纳年轻代分区,⽽混合收

集会通过启发式算法,在⽼年代候选回收分区中,筛选出回收收益最⾼的分区添加到CSet中。

候选⽼年代分区的CSet准⼊条件,可以通过活跃度阈值-XX:G1MixedGCLiveThresholdPercent(默认85%)进⾏设置,从⽽拦截那些回收开销巨

⼤的对象;同时,每次混合收集可以包含候选⽼年代分区,可根据CSet对堆的总⼤⼩占⽐-XX:G1OldCSetRegionThresholdPercent(默认10%)

设置数量上限。

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

年轻代收集集合

年轻代收集集合CSetofYoungCollection

应⽤线程不断活动后,年轻代空间会被逐渐填满。当JVM分配对象到Eden区域失败(Eden区已满)时,便会触发⼀次STW式的年轻代收集。

在年轻代收集中,Eden分区存活的对象将被拷贝到Survivor分区;原有Survivor分区存活的对象,将根据任期阈值(tenuringthreshold)

分别晋升到PLAB中,新的survivor分区和⽼年代分区。⽽原有的年轻代分区将被整体回收掉。

同时,年轻代收集还负责维护对象的年龄(存活次数),辅助判断⽼化(tenuring)对象晋升的时候是到Survivor分区还是到⽼年代分区。年轻

代收集⾸先先将晋升对象尺⼨总和、对象年龄信息维护到年龄表中,再根据年龄表、Survivor尺⼨、Survivor填充容量-

XX:TargetSurvivorRatio(默认50%)、最⼤任期阈值-XX:MaxTenuringThreshold(默认15),计算出⼀个恰当的任期阈值,凡是超过任期阈值的对

象都会被晋升到⽼年代。

混合收集集合

混合收集集合CSetofMixedCollection

年轻代收集不断活动后,⽼年代的空间也会被逐渐填充。当⽼年代占⽤空间超过整堆⽐IHOP阈值-XX:InitiatingHeapOccupancyPercent(默认

45%)时,G1就会启动⼀次混合垃圾收集周期。为了满⾜暂停⽬标,G1可能不能⼀⼝⽓将所有的候选分区收集掉,因此G1可能会产⽣连续

多次的混合收集与应⽤线程交替执⾏,每次STW的混合收集与年轻代收集过程相类似。

为了确定包含到年轻代收集集合CSet的⽼年代分区,JVM通过参数混合周期的最⼤总次数-XX:G1MixedGCCountTarget(默认8)、堆废物百分

⽐-XX:G1HeapWastePercent(默认5%)。通过候选⽼年代分区总数与混合周期最⼤总次数,确定每次包含到CSet的最⼩分区数量;根据堆废

物百分⽐,当收集达到参数时,不再启动新的混合收集。⽽每次添加到CSet的分区,则通过计算得到的GC效率进⾏安排。

第四章G1的活动周期

G1垃圾收集活动汇总

祭出⼀张总图

RSet的维护

由于不能整堆扫描,⼜需要计算分区确切的活跃度,因此,G1需要⼀个增量式的完全标记并发算法,通过维护RSet,得到准确的分区引⽤

信息。在G1中,RSet的维护主要来源两个⽅⾯:写栅栏(WriteBarrier)和并发优化线程(ConcurrenceRefinementThreads)

栅栏

栅栏Barrier

我们⾸先介绍⼀下栅栏(Barrier)的概念。栅栏是指在原⽣代码⽚段中,当某些语句被执⾏时,栅栏代码也会被执⾏。⽽G1主要在赋值语句

中,使⽤写前栅栏(Pre-WriteBarrrier)和写后栅栏(Post-WriteBarrrier)。事实上,写栅栏的指令序列开销⾮常昂贵,应⽤吞吐量也会根

据栅栏复杂度⽽降低。

写前栅栏Pre-WriteBarrrier

即将执⾏⼀段赋值语句时,等式左侧对象将修改引⽤到另⼀个对象,那么等式左侧对象原先引⽤的对象所在分区将因此丧失⼀个引⽤,那么

JVM就需要在赋值语句⽣效之前,记录丧失引⽤的对象。JVM并不会⽴即维护RSet,⽽是通过批量处理,在将来RSet更新(见SATB)。

写后栅栏Post-WriteBarrrier

当执⾏⼀段赋值语句后,等式右侧对象获取了左侧对象的引⽤,那么等式右侧对象所在分区的RSet也应该得到更新。同样为了降低开销,

写后栅栏发⽣后,RSet也不会⽴即更新,同样只是记录此次更新⽇志,在将来批量处理(见ConcurrenceRefinementThreads)。

起始快照算法

起始快照算法Snapshotatthebeginning(SATB)

TaiichiTuasa贡献的增量式完全并发标记算法起始快照算法(SATB),主要针对标记-清除垃圾收集器的并发标记阶段,⾮常适合G1的分区

块的堆结构,同时解决了CMS的主要烦恼:重新标记暂停时间长带来的潜在风险。

SATB会创建⼀个对象图,相当于堆的逻辑快照,从⽽确保并发标记阶段所有的垃圾对象都能通过快照被鉴别出来。当赋值语句发⽣时,应

⽤将会改变了它的对象图,那么JVM需要记录被覆盖的对象。因此写前栅栏会在引⽤变更前,将值记录在SATB⽇志或缓冲区中。每个线程

都会独占⼀个SATB缓冲区,初始有256条记录空间。当空间⽤尽时,线程会分配新的SATB缓冲区继续使⽤,⽽原有的缓冲去则加⼊全局

列表中。最终在并发标记阶段,并发标记线程(ConcurrentMarkingThreads)在标记的同时,还会定期检查和处理全局缓冲区列表的记

录,然后根据标记位图分⽚的标记位,扫描引⽤字段来更新RSet。此过程⼜称为并发标记/SATB写前栅栏。

并发优化线程

并发优化线程ConcurrenceRefinementThreads

G1中使⽤基于UrsHölzle的快速写栅栏,将栅栏开销缩减到2个额外的指令。栅栏将会更新⼀个cardtabletype的结构来跟踪代间引⽤。

当赋值语句发⽣后,写后栅栏会先通过G1的过滤技术判断是否是跨分区的引⽤更新,并将跨分区更新对象的卡⽚加⼊缓冲区序列,即更新

⽇志缓冲区或脏卡⽚队列。与SATB类似,⼀旦⽇志缓冲区⽤尽,则分配⼀个新的⽇志缓冲区,并将原来的缓冲区加⼊全局列表中。

并发优化线程(ConcurrenceRefinementThreads),只专注扫描⽇志缓冲区记录的卡⽚来维护更新RSet,线程最⼤数⽬可通过-

XX:G1ConcRefinementThreads(默认等于-XX:ParellelGCThreads)设置。并发优化线程永远是活跃的,⼀旦发现全局列表有记录存在,就开始并

发处理。如果记录增长很快或者来不及处理,那么通过阈值-X:G1ConcRefinementGreenZone/-XX:G1ConcRefinementYellowZone/-

XX:G1ConcRefinementRedZone,G1会⽤分层的⽅式调度,使更多的线程处理全局列表。如果并发优化线程也不能跟上缓冲区数量,则

Mutator线程(Java应⽤线程)会挂起应⽤并被加进来帮助处理,直到全部处理完。因此,必须避免此类场景出现。

并发标记周期

并发标记周期ConcurrentMarkingCycle

并发标记周期是G1中⾮常重要的阶段,这个阶段将会为混合收集周期识别垃圾最多的⽼年代分区。整个周期完成根标记、识别所有(可能)存

活对象,并计算每个分区的活跃度,从⽽确定GC效率等级。

当达到IHOP阈值-XX:InitiatingHeapOccupancyPercent(⽼年代占整堆⽐,默认45%)时,便会触发并发标记周期。整个并发标记周期将由初始

标记(InitialMark)、根分区扫描(RootRegionScanning)、并发标记(ConcurrentMarking)、重新标记(Remark)、清除(Cleanup)⼏个

阶段组成。其中,初始标记(随年轻代收集⼀起活动)、重新标记、清除是STW的,⽽并发标记如果来不及标记存活对象,则可能在并发标记

过程中,G1⼜触发了⼏次年轻代收集。

并发标记线程

并发标记线程ConcurrentMarkingThreads

要标记存活的对象,每个分区都需要创建位图(Bitmap)信息来存储标记数据,来确定标记周期内被分配的对象。G1采⽤了两个位图

PreviousBitmap、NextBitmap,来存储标记数据,Previous位图存储上次的标记数据,Next位图在标记周期内不断变化更新,同时

Previous位图的标记数据也越来越过时,当标记周期结束后Next位图便替换Previous位图,成为上次标记的位图。同时,每个分区通过顶

部开始标记(TAMS),来记录已标记过的内存范围。同样的,G1使⽤了两个顶部开始标记PreviousTAMS(PTAMS)、Next

TAMS(NTAMS),记录已标记的范围。

在并发标记阶段,G1会根据参数-XX:ConcGCThreads(默认GC线程数的1/4,即-XX:ParallelGCThreads/4),分配并发标记线程

(ConcurrentMarkingThreads),进⾏标记活动。每个并发线程⼀次只扫描⼀个分区,并通过"⼿指"指针的⽅式优化获取分区。并发标记

线程是爆发式的,在给定的时间段拼命⼲活,然后休息⼀段时间,再拼命⼲活。

每个并发标记周期,在初始标记STW的最后,G1会分配⼀个空的Next位图和⼀个指向分区顶部(Top)的NTAMS标记。Previous位图记录

的上次标记数据,上次的标记位置,即PTAMS,在PTAMS与分区底部(Bottom)的范围内,所有的存活对象都已被标记。那么,在

PTAMS与Top之间的对象都将是隐式存活(ImplicitlyLive)对象。在并发标记阶段,Next位图吸收了Previous位图的标记数据,同时每个

分区都会有新的对象分配,则Top与NTAMS分离,前往更⾼的地址空间。在并发标记的⼀次标记中,并发标记线程将找出NTAMS与

PTAMS之间的所有存活对象,将标记数据存储在Next位图中。同时,在NTAMS与Top之间的对象即成为已标记对象。如此不断地更新

Next位图信息,并在清除阶段与Previous位图交换⾓⾊。

初始标记

初始标记InitialMark

初始标记(InitialMark)负责标记所有能被直接可达的根对象(原⽣栈对象、全局对象、JNI对象),根是对象图的起点,因此初始标记需要将

Mutator线程(Java应⽤线程)暂停掉,也就是需要⼀个STW的时间段。事实上,当达到IHOP阈值时,G1并不会⽴即发起并发标记周期,⽽

是等待下⼀次年轻代收集,利⽤年轻代收集的STW时间段,完成初始标记,这种⽅式称为借道(Piggybacking)。在初始标记暂停中,分区

的NTAMS都被设置到分区顶部Top,初始标记是并发执⾏,直到所有的分区处理完。

根分区扫描

根分区扫描RootRegionScanning

在初始标记暂停结束后,年轻代收集也完成的对象复制到Survivor的⼯作,应⽤线程开始活跃起来。此时为了保证标记算法的正确性,所有

新复制到Survivor分区的对象,都需要被扫描并标记成根,这个过程称为根分区扫描(RootRegionScanning),同时扫描的Suvivor分区

也被称为根分区(RootRegion)。根分区扫描必须在下⼀次年轻代垃圾收集启动前完成(并发标记的过程中,可能会被若⼲次年轻代垃圾收集

打断),因为每次GC会产⽣新的存活对象集合。

并发标记

并发标记ConcurrentMarking

和应⽤线程并发执⾏,并发标记线程在并发标记阶段启动,由参数-XX:ConcGCThreads(默认GC线程数的1/4,即-XX:ParallelGCThreads/4)

控制启动数量,每个线程每次只扫描⼀个分区,从⽽标记出存活对象图。在这⼀阶段会处理Previous/Next标记位图,扫描标记对象的引⽤

字段。同时,并发标记线程还会定期检查和处理STAB全局缓冲区列表的记录,更新对象引⽤信息。参数-

XX:+ClassUnloadingWithConcurrentMark会开启⼀个优化,如果⼀个类不可达(不是对象不可达),则在重新标记阶段,这个类就会被直接卸载。

所有的标记任务必须在堆满前就完成扫描,如果并发标记耗时很长,那么有可能在并发标记过程中,⼜经历了⼏次年轻代收集。如果堆满前

没有完成标记任务,则会触发担保机制,经历⼀次长时间的串⾏FullGC。

存活数据计算

存活数据计算LiveDataAccounting

存活数据计算(LiveDataAccounting)是标记操作的附加产物,只要⼀个对象被标记,同时会被计算字节数,并计⼊分区空间。只有

NTAMS以下的对象会被标记和计算,在标记周期的最后,Next位图将被清空,等待下次标记周期。

重新标记

重新标记Remark

重新标记(Remark)是最后⼀个标记阶段。在该阶段中,G1需要⼀个暂停的时间,去处理剩下的SATB⽇志缓冲区和所有更新,找出所有未

被访问的存活对象,同时安全完成存活数据计算。这个阶段也是并⾏执⾏的,通过参数-XX:ParallelGCThread可设置GC暂停时可⽤的GC线

程数。同时,引⽤处理也是重新标记阶段的⼀部分,所有重度使⽤引⽤对象(弱引⽤、软引⽤、虚引⽤、最终引⽤)的应⽤都会在引⽤处理上

产⽣开销。

清除

清除Cleanup

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

阶段主要执⾏以下操作:

梳理,启发式算法会根据活跃度和RSet尺⼨对分区定义不同等级,同时RSet数理也有助于发现⽆⽤的引⽤。参数-

XX:+PrintAdaptiveSizePolicy可以开启打印启发式算法决策细节;

2.整理堆分区,为混合收集周期识别回收收益⾼(基于释放空间和暂停⽬标)的⽼年代分区集合;

3.识别所有空闲分区,即发现⽆存活对象的分区。该分区可在清除阶段直接回收,⽆需等待下次收集周期。

年轻代收集/混合收集周期

年轻代收集和混合收集周期,是G1回收空间的主要活动。当应⽤运⾏开始时,堆内存可⽤空间还⽐较⼤,只会在年轻代满时,触发年轻代

收集;随着⽼年代内存增长,当到达IHOP阈值-XX:InitiatingHeapOccupancyPercent(⽼年代占整堆⽐,默认45%)时,G1开始着⼿准备收集⽼

年代空间。⾸先经历并发标记周期,识别出⾼收益的⽼年代分区,前⽂已述。但随后G1并不会马上开始⼀次混合收集,⽽是让应⽤线程先

运⾏⼀段时间,等待触发⼀次年轻代收集。在这次STW中,G1将保准整理混合收集周期。接着再次让应⽤线程运⾏,当接下来的⼏次年轻

代收集时,将会有⽼年代分区加⼊到CSet中,即触发混合收集,这些连续多次的混合收集称为混合收集周期(MixedCollectionCycle)。

GC⼯作线程数

GC⼯作线程数-XX:ParallelGCThreads

JVM可以通过参数-XX:ParallelGCThreads进⾏指定GC⼯作的线程数量。参数-XX:ParallelGCThreads默认值并不是固定的,⽽是根据当前的

CPU资源进⾏计算。如果⽤户没有指定,且CPU⼩于等于8,则默认与CPU核数相等;若CPU⼤于8,则默认JVM会经过计算得到⼀个⼩

于CPU核数的线程数;当然也可以⼈⼯指定与CPU核数相等。

年轻代收集

年轻代收集YoungCollection

每次收集过程中,既有并⾏执⾏的活动,也有串⾏执⾏的活动,但都可以是多线程的。在并⾏执⾏的任务中,如果某个任务过重,会导致其

他线程在等待某项任务的处理,需要对这些地⽅进⾏优化。

并⾏活动

外部根分区扫描ExtRootScanning:此活动对堆外的根(JVM系统⽬录、VM数据结构、JNI线程句柄、硬件寄存器、全局变量、线程对栈

根)进⾏扫描,发现那些没有加⼊到暂停收集集合CSet中的对象。如果系统⽬录(单根)拥有⼤量加载的类,最终可能其他并⾏活动结束后,

该活动依然没有结束⽽带来的等待时间。

更新已记忆集合UpdateRS:并发优化线程会对脏卡⽚的分区进⾏扫描更新⽇志缓冲区来更新RSet,但只会处理全局缓冲列表。作为补

充,所有被记录但是还没有被优化线程处理的剩余缓冲区,会在该阶段处理,变成已处理缓冲区(ProcesdBuffers)。为了限制花在更新

RSet的时间,可以设置暂停占⽤百分⽐-XX:G1RSetUpdatingPauTimePercent(默认10%,即-XX:MaxGCPauMills/10)。值得注意的是,如

果更新⽇志缓冲区更新的任务不降低,单纯地减少RSet的更新时间,会导致暂停中被处理的缓冲区减少,将⽇志缓冲区更新⼯作推到并发

优化线程上,从⽽增加对Java应⽤线程资源的争夺。

RSet扫描ScanRS:在收集当前CSet之前,考虑到分区外的引⽤,必须扫描CSet分区的RSet。如果RSet发⽣粗化,则会增加RSet的扫

描时间。开启诊断模式-XX:UnlockDiagnosticVMOptions后,通过参数-XX:+G1SummarizeRSetStats可以确定并发优化线程是否能够及时处理更

新⽇志缓冲区,并提供更多的信息,来帮助为RSet粗化总数提供窗⼝。参数-XX:G1SummarizeRSetStatsPeriod=n可设置RSet的统计周期,

即经历多少此GC后进⾏⼀次统计

代码根扫描CodeRootScanning:对代码根集合进⾏扫描,扫描JVM编译后代码NativeMethod的引⽤信息(nmethod扫描),进⾏RSet

扫描。事实上,只有CSet分区中的RSet有强代码根时,才会做nmethod扫描,查找对CSet的引⽤。

转移和回收ObjectCopy:通过选定的CSet以及CSet分区完整的引⽤集,将执⾏暂停时间的主要部分:CSet分区存活对象的转移、CSet

分区空间的回收。通过⼯作窃取机制来负载均衡地选定复制对象的线程,并且复制和扫描对象被转移的存活对象将拷贝到每个GC线程分配

缓冲区GCLAB。G1会通过计算,预测分区复制所花费的时间,从⽽调整年轻代的尺⼨。

终⽌Termination:完成上述任务后,如果任务队列已空,则⼯作线程会发起终⽌要求。如果还有其他线程继续⼯作,空闲的线程会通过⼯

作窃取机制尝试帮助其他线程处理。⽽单独执⾏根分区扫描的线程,如果任务过重,最终会晚于终⽌。

GC外部的并⾏活动GCWorkerOther:该部分并⾮GC的活动,⽽是JVM的活动导致占⽤了GC暂停时间(例如JNI编译)。

串⾏活动

代码根更新CodeRootFixup:根据转移对象更新代码根。

代码根清理CodeRootPurge:清理代码根集合表。

清除全局卡⽚标记ClearCT:在任意收集周期会扫描CSet与RSet记录的PRT,扫描时会在全局卡⽚表中进⾏标记,防⽌重复扫描。在收

集周期的最后将会清除全局卡⽚表中的已扫描标志。

选择下次收集集合ChooCSet:该部分主要⽤于并发标记周期后的年轻代收集、以及混合收集中,在这些收集过程中,由于有⽼年代候

选分区的加⼊,往往需要对下次收集的范围做出界定;但单纯的年轻代收集中,所有收集的分区都会被收集,不存在选择。

引⽤处理RefProc:主要针对软引⽤、弱引⽤、虚引⽤、final引⽤、JNI引⽤。当RefProc占⽤时间过多时,可选择使⽤参数-

XX:ParallelRefProcEnabled激活多线程引⽤处理。G1希望应⽤能⼩⼼使⽤软引⽤,因为软引⽤会⼀直占据内存空间直到空间耗尽时被FullGC

回收掉;即使未发⽣FullGC,软引⽤对内存的占⽤,也会导致GC次数的增加。

引⽤排队RefEnq:此项活动可能会导致RSet的更新,此时会通过记录⽇志,将关联的卡⽚标记为脏卡⽚。

卡⽚重新脏化RedirtyCards:重新脏化卡⽚。

回收空闲巨型分区HumongousReclaim:G1做了⼀个优化:通过查看所有根对象以及年轻代分区的RSet,如果确定RSet中巨型对象没

有任何引⽤,则说明G1发现了⼀个不可达的巨型对象,该对象分区会被回收。

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

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

并发标记周期后的年轻代收集

并发标记周期后的年轻代收集YoungCollectionFollowingConcurrentMarkingCycle

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

CSet(ChooCSet)。

混合收集周期

混合收集周期MixedCollectionCycle

单次的混合收集与年轻代收集并⽆⼆致。根据暂停⽬标,⽼年代的分区可能不能⼀次暂停收集中被处理完,G1会发起连续多次的混合收

集,称为混合收集周期(MixedCollectionCycle)。G1会计算每次加⼊到CSet中的分区数量、混合收集进⾏次数,并且在上次的年轻代收

集、以及接下来的混合收集中,G1会确定下次加⼊CSet的分区集(ChooCSet),并且确定是否结束混合收集周期。

转移失败的担保机制FullGC

转移失败的担保机制FullGC

转移失败(EvacuationFailure)是指当G1⽆法在堆空间中申请新的分区时,G1便会触发担保机制,执⾏⼀次STW式的、单线程的Full

GC。FullGC会对整堆做标记清除和压缩,最后将只包含纯粹的存活对象。参数-XX:G1RervePercent(默认10%)可以保留空间,来应对晋

升模式下的异常情况,最⼤占⽤整堆50%,更⼤也⽆意义。

G1在以下场景中会触发FullGC,同时会在⽇志中记录to-space-exhausted以及EvacuationFailure:

1.从年轻代分区拷贝存活对象时,⽆法找到可⽤的空闲分区

2.从⽼年代分区转移存活对象时,⽆法找到可⽤的空闲分区

3.分配巨型对象时在⽼年代⽆法找到⾜够的连续分区

由于G1的应⽤场合往往堆内存都⽐较⼤,所以FullGC的收集代价⾮常昂贵,应该避免FullGC的发⽣。

第五章总结

G1是⼀款⾮常优秀的垃圾收集器,不仅适合堆内存⼤的应⽤,同时也简化了调优的⼯作。通过主要的参数初始和最⼤堆空间、以及最⼤容

忍的GC暂停⽬标,就能得到不错的性能;同时,我们也看到G1对内存空间的浪费较⾼,但通过**⾸先收集尽可能多的垃圾(Garbage

First)**的设计原则,可以及时发现过期对象,从⽽让内存占⽤处于合理的⽔平。

参考资料

[1]CharlieH,MonicaB,PoonamP,rformanceCompanion

[2]周志明.深⼊理解JVM虚拟机

本文发布于:2022-12-26 17:31:55,感谢您对本站的认可!

本文链接:http://www.wtabcd.cn/fanwen/fan/90/35042.html

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

上一篇:mansion
下一篇:pants
标签:garbage
相关文章
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图