G1(Garbage-First Collector)是一种垃圾回收算法,最早在JDK 6 Update 14中作为实验性功能加入,并在JDK 7 Update 4正式JDK,之后在JDK 9 中成为默认垃圾回收算法,在JDK 10中优化了Full GC性能。
G1是一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求。它是专门针对以下应用场景设计的:
G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色:
G1同之前的垃圾收集器一样实现了逻辑上的分代模型,不同的是G1在物理上是不分代的。G1的各代存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region,每个Region占有一块连续的虚拟内存地址。如下图所示:
G1把堆内存分为大小相等的内存分段,默认情况下会把内存分为2048个内存分段,可以用-XX:G1HeapRegionSize调整内存分段的个数。比如32G堆内存,2048个内存分段每段的大小为16M。这相当于把内存化整为零。内存分段是物理概念,代表实际的物理内存空间。
每个内存分段都可以被标记为Eden区,Survivor区,Old区,或者Humongous区。这样属于不同代,不同区的内存分段就可以不必是连续内存空间了。
在每个分区内部又被分成了若干个大小为512 Byte 卡片(Card),标识堆内存最小可用粒度。所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见 RSet)。每次对内存的回收,都是对指定分区的卡片进行处理。
另外与之前垃圾回收器不同的是增加了一个Humongous区,表示这些Region存储的是巨大对象(humongous object,H-obj),即大小大于等于region一半的对象。H-obj有如下几个特征:
此类对象直接被分配到老年代中的“巨型区域”,这些巨型区域是一个连续的区域集。StartsHumongous 标记该连续集的开始,ContinuesHumongous 标记它的延续。由于每个 StartsHumongous 和 ContinuesHumongous 区域集只包含一个巨型对象,所以没有使用巨型对象的终点与上个区域的终点之间的空间(即巨型对象所跨的空间)。如果对象只是略大于Region大小的倍数,则此类未使用的空间可能会导致堆碎片化。
为了减少连续H-objs分配对GC的影响,需要把大对象变为普通的对象,建议增大Region size。这样一来,之前的巨型对象就不再是巨型对象了,而是采用常规的分配路径。一个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围从1M到32M,且是2的指数。如果不设定,那么G1会根据Heap大小自动决定。
在串行和并行收集器中,GC 通过整堆扫描,来确定对象是否处于可达路径中。然而 G1 为了避免 STW 式的整堆扫描,在每个Region记录了一个RSet(Remembered Set)),内部类似一个反向指针,记录引用分区内对象的Card索引。当要回收该分区时,通过扫描分区的 RSet,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况。我们知道每个Region默认按照512Kb划分成多个Card,所以RSet需要记录的东西应该是 xx Region的 xx Card。
通常的,有两种记录引用关系的方式,PointOut和PointIn。如果obj1.field1=obj2,如果是PointOut方式,则在obj1所在region的RSet记录obj2的位置;如果是PointIn方式,则在obj2所在region记录obj1的位置。G1采用的是PointIn方式。
G1中一共有五种分区间的引用关系:
YGC时,GC Root主要是两类:栈空间和老年代分区到新生代分区的引用关系。
Mixed GC时,由于仅回收部分老年代分区,老年代分区之间的引用关系也将被使用。
因为年轻代回收是针对全部年轻代的对象的,反正所有年轻代内部的对象引用关系都会被扫描,所以RS不需要保存来自年轻代内部的引用。对于属于老年代分段的RSet来说,也只会保存来自老年代的引用,这是因为老年代的回收之前会先进行年轻代的回收,年轻代回收后Eden区变空了,G1会在老年代回收过程中扫描Survivor区到老年代的引用。因此,我们仅需要记录两种引用关系:老年代分区引用新生代分区,老年代分区之间的引用。
RSet 在内部使用 Per Region Table(PRT)记录分区的引用情况。由于 RSet 的记录要占用分区的空间,如果一个分区非常"受欢迎",那么 RSet 占用的空间会上升,从而降低分区的可用空间。G1 应对这个问题采用了改变 RSet 的密度的方式,在 PRT 中将会以三种模式记录引用:
由上可知,粗粒度的 PRT 只是记录了引用数量,需要通过整堆扫描才能找出所有引用,因此扫描速度也是最慢的。
为了维护这些RSet,如果每次给引用类型的字段赋值都要更新RSet,这带来的额外开销实在太大,G1中采用写栅栏(Write Barrier)和并发优化线程(Concurrence Refinement Threads)实现了RSet的更新。
void oop_field_store(oop* field, oop new_value) {
pre_write_barrier(field); // pre-write barrier: for maintaining SATB invariant
*field = new_value; // the actual store
post_write_barrier(field, new_value); // post-write barrier: for tracking cross-region reference
}
在执行引用赋值语句前后JVM会添加pre-write barrier和post-write barrier,post-write barrier主要做了以下事情:
赋值动作到此结束,接下来的RSet更新操作交由多个ConcurrentG1RefineThread并发完成,每当全局队列集合超过一定阈值后,ConcurrentG1RefineThread会取出若干个队列,遍历每个队列中记录的Card,并进行处理,大概实现逻辑如下:
Refinement threads线程数量可以通过-XX:G1ConcRefinementThreads(默认等于 -XX:ParellelGCThreads)参数设置。如果记录增长很快或者来不及处理,那么通过阈值 -X:G1ConcRefinementGreenZone/-XX:G1ConcRefinementYellowZone/-XX:G1ConcRefinementRedZone,G1 会用分层的方式调度,使更多的线程处理全局队列。如果并发优化线程也不能跟上缓冲区数量,则 Mutator 线程(Java 应用线程)会挂起应用并被加进来帮助处理,直到全部处理完。因此,必须避免此类场景出现。
CSet(Collection Set)代表每次 GC 暂停时回收的一系列目标分区。在任意一次收集暂停中,CSet 所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中来实现压缩,从而减少堆内存碎片。在混合垃圾回收期间会通过并发标记阶段,在老年代候选回收分区中筛选出回收收益最高的分区添加到 CSet 中。
候选老年代分区的 CSet 准入条件,可以通过活跃度阈值 -XX:G1MixedGCLiveThresholdPercent(默认65%)进行设置,从而拦截那些回收开销巨大的对象;每次混合垃圾回收包含候选老年代分区,可根据 CSet 对堆的总大小占比 -XX:G1OldCSetRegionThresholdPercent(默认10%)设置数量上限。
全称是Snapshot-At-The-Beginning,由字面理解,是GC开始时活着的对象的一个快照。它是通过Root Tracing得到的,作用是维持并发GC的正确性。 那么它是怎么维持并发GC的正确性的呢?根据三色标记算法,我们知道对象存在三种状态:
由于并发阶段的存在,Mutator和Garbage Collector线程同时对对象进行修改,就会出现白对象漏标的情况,这种情况发生的前提是:
对于第一个条件,在并发标记阶段,如果该白对象是new出来的,并没有被灰对象持有,那么它会不会被漏标呢?Region中有两个top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS。在TAMS以上的对象是新分配的,这是一种隐式的标记。对于在GC时已经存在的白对象,如果它是活着的,它必然会被另一个对象引用,即条件二中的灰对象。如果灰对象到白对象的直接引用或者间接引用被替换了或者删除了,白对象就会被漏标,从而导致被回收掉,这是非常严重的错误。SATB关注的是第二个条件的打破,即引用关系的删除。SATB利用pre write barrier将所有即将被删除的引用关系的旧引用记录下来,在重新标记(Remark)阶段以这些旧引用为根STW的重新扫描一遍RSet即可避免漏标问题。
由于堆内存是应用程序共享的,应用程序的多个线程在分配内存的时候需要加锁以进行同步。为了避免加锁,提高性能每一个应用程序的线程会被分配一个TLAB。TLAB中的内存来自于G1年轻代中的内存分段。当对象不是Humongous对象,TLAB也能装的下的时候,对象会被优先分配于创建此对象的线程的TLAB中。这样分配会很快,因为TLAB隶属于线程,所以不需要加锁。
与TLAB类似,G1会在年轻代回收过程中把Eden区中的对象复制(“提升”)到Survivor区中,Survivor区中的对象复制到Old区中。G1的回收过程是多线程执行的,为了避免多个线程往同一个内存分段进行复制,那么复制的过程也需要加锁。为了避免加锁,G1的每个线程都关联了一个PLAB,这样就不需要进行加锁了。
Pause Prediction Model 即停顿预测模型。G1 GC是一个响应时间优先的GC算法,它与CMS最大的不同是,用户可以设定整个GC过程的期望停顿时间,参数-XX:MaxGCPauseMillis指定一个G1收集过程目标停顿时间,默认值200ms,不过它不是硬性条件,只是期望值。那么G1怎么满足用户的期望呢?G1不是实时收集器,它很有可能达到设定的暂停时间目标,但并非绝对确定。G1根据先前收集的数据,估算在用户指定的目标时间内可以收集多少个区域。因此,收集器具有收集区域成本的合理准确的模型,并且收集器使用此模型来确定要收集哪些和多少个区域,同时保持在暂停时间目标之内。
JVM启动时,G1先准备好Eden区,程序在运行过程中不断创建对象到Eden区,当所有的Eden区都满了,G1会启动一次年轻代垃圾回收过程。年轻代只会回收Eden区和Survivor区。首先G1停止应用程序的执行(Stop-The-World),G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和Survivor区所有的内存分段。
当整个堆内存(包括老年代和新生代)被占满一定大小的时候(默认是45%,可以通过-XX:InitiatingHeapOccupancyPercent进行设置),Mixed GC(混合回收)就会被启动。具体检测堆内存使用情况的时机是年轻代回收之后或者Houmongous对象分配之后。
Mixed GC主要可以分为两个阶段
包含以下几个阶段:
将Region里的活对象拷贝到空Region里去(并行拷贝),然后回收原本的Region的空间。
为了满足停顿预测模型即暂停时间,G1 可能不能一口气将所有的Region都收集掉,因此 G1 可能会产生连续多次的混合收集与应用线程交替执行,每次 STW 的混合收集与年轻代收集过程相类似。由于老年代中的内存分段默认分8次(可以通过-XX:G1MixedGCCountTarget设置)回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收。并且有一个阈值会决定内存分段是否被回收,-XX:G1MixedGCLiveThresholdPercent,默认为65%,意思是垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间。
混合回收并不一定要进行8次。有一个阈值-XX:G1HeapWastePercent,默认值为10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。因为GC会花费很多的时间但是回收到的内存却很少。G1 GC 回收了足够的旧区域后(经过多次混合垃圾回收),G1 将恢复执行年轻代垃圾回收,直到下一个标记周期完成。
转移失败(Evacuation Failure)是指当 G1 无法在堆空间中申请新的分区时,G1 便会触发担保机制,执行一次 STW 式的、单线程的 Full GC。Full GC 会对整堆做标记清除和压缩,最后将只包含纯粹的存活对象。参数 -XX:G1ReservePercent(默认10%)可以保留空间,来应对晋升模式下的异常情况,最大占用整堆50%,更大也无意义。
G1 的Full GC算法就是单线程执行的 Serial Old GC,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免Full GC。
参数 | 含义 |
---|---|
-XX:G1HeapRegionSize=n | 设置Region大小,并非最终值 |
-XX:MaxGCPauseMillis | 设置G1收集过程目标时间,默认值200ms,不是硬性条件 |
-XX:G1NewSizePercent | 新生代最小值,默认值5% |
-XX:G1MaxNewSizePercent | 新生代最大值,默认值60% |
-XX:ParallelGCThreads | STW期间,并行GC线程数 |
-XX:ConcGCThreads=n | 并发标记阶段,并行执行的线程数,将 n 设置为并行垃圾回收线程数 (ParallelGCThreads) 的 1/4 左右。 |
-XX:InitiatingHeapOccupancyPercent | 设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。 |
-XX:G1MixedGCLiveThresholdPercent=65 | 为混合垃圾回收周期中要包括的旧区域设置占用率阈值。默认占用率为 65%。 |
-XX:G1HeapWastePercent=10 | 设置您愿意浪费的堆百分比。如果可回收百分比小于堆废物百分比,Java HotSpot VM 不会启动混合垃圾回收周期。默认值是 10% |
-XX:G1MixedGCCountTarget=8 | 执行混合垃圾回收的目标次数,默认值是 8 次混合垃圾回收,混合回收的目标是要控制在此目标次数以内。 |
-XX:G1OldCSetRegionThresholdPercent=10 | 设置混合垃圾回收期间要回收的最大旧区域数。默认值是 Java 堆的 10%。 |
-XX:G1ReservePercent=10 | 设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险。默认值是 10%。增加或减少百分比时,请确保对总的 Java 堆调整相同的量。 |
微调 G1 GC 时,请记住以下建议:
年轻代大小:避免使用 -Xmn 选项或 -XX:NewRatio 等其他相关选项显式设置年轻代大小。固定年轻代的大小会覆盖暂停时间目标。
暂停时间目标:每当对垃圾回收进行评估或调优时,都会涉及到延迟与吞吐量的权衡。G1 GC 是增量垃圾回收器,暂停统一,同时应用程序线程的开销也更多。G1 GC 的吞吐量目标是 90% 的应用程序时间和 10%的垃圾回收时间。如果将其与 Java HotSpot VM 的吞吐量回收器相比较,目标则是 99% 的应用程序时间和 1% 的垃圾回收时间。因此,当您评估 G1 GC 的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示您愿意承受更多的垃圾回收开销,而这会直接影响到吞吐量。当您评估 G1 GC 的延迟时,请设置所需的(软)实时目标,G1 GC 会尽量满足。副作用是,吞吐量可能会受到影响。
掌握混合垃圾回收:当您调优混合垃圾回收时,请尝试以下选项
-XX:InitiatingHeapOccupancyPercent:堆内存比例,达到后触发 Mixed GC
-XX:G1MixedGCLiveThresholdPercent 和 -XX:G1HeapWastePercent:Region的存活比例和允许堆的浪费比例
-XX:G1MixedGCCountTarget 和 -XX:G1OldCSetRegionThresholdPercent:执行混合垃圾回收的目标次数和设置混合垃圾回收期间要回收的最大旧区域数
G1的首要重点是为运行需要大堆且GC延迟有限的应用程序的用户提供解决方案。这意味着堆大小约为6GB或更大,并且稳定且可预测的暂停时间低于0.5秒。
如果当前具有CMS或ParallelOld垃圾收集器的应用程序具有以下一个或多个特征,则将其切换到G1很有用。
http://www.linkedkeeper.com/1511.html
https://www.jianshu.com/p/870abddaba41
https://blog.csdn.net/a860mhz/category_9293742.html
https://tech.meituan.com/2016/09/23/g1.html
https://www.cnblogs.com/yufengzhang/p/10571081.html
https://www.oracle.com/technetwork/cn/articles/java/g1gc-1984535-zhs.html
https://www.cnblogs.com/yufengzhang/p/10571081.html