详解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小时内删除。
留言与评论(共有 0 条评论) |