JVM垃圾回收:G1回收器和ZGC回收器
G1回收器:区域化分代式
G1前置知识
Card Table(卡表,多种垃圾回收器均具备)
由于在进行YoungGC时,我们在进行对一个对象是否被引用的过程,需要扫描整个Old区,所以JVM设计了CardTable,将Old区分为一个一个Card,一个Card有多个对象;如果一个Card中的对象有引用指向Young区,则将其标记为Dirty Card,下次需要进行YoungGC时,只需要去扫描Dirty Card即可。即上文提到的卡表。
RSet(Remembered Set,记忆集)
是辅助GC过程的一种结构,典型的空间换时间工具,和Card Table有些类似。
后面说到的CSet(Collection Set)也是辅助GC的,它记录了GC要收集的Region集合,集合里的Region可以是任意年代的。
在GC的时候,对于old->young和old->old的跨代对象引用,不需要扫描整个堆找到谁引用了当前分区中的对象,只要扫描对应的CSet中的RSet即可。逻辑上说每个Region都有一个RSet,RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)。
而Card Table则是一种points-out(我引用了谁的对象)的结构,每个Card 覆盖一定范围的Heap(一般为512Bytes)。G1的RSet是在Card Table的基础上实现的:每个Region会记录下别的Region有指向自己的指针,并标记这些指针分别在哪些Card的范围内。这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index集合。每个Region中都有一个RSet,记录其他Region到本Region的引用信息;使得垃圾回收器不需要扫描整个堆找到谁引用当前分区中的对象,只需要扫描RSet即可。
对于年轻代的 Region,它的 RSet 只保存了来自老年代的引用,这是因为年轻代的回收是针对所有年轻代 Region 的,没必要画蛇添足。所以说年轻代 Region 的 RSet 有可能是空的。
而对于老年代的 Region 来说,它的 RSet 也只会保存老年代对它的引用。这是因为老年代回收之前,会先对年轻代进行回收。这时,Eden 区变空了,而在回收过程中会扫描 Survivor 分区,所以也没必要保存来自年轻代的引用。
RSet 通常会占用很大的空间,大约 5% 或者更高。不仅仅是空间方面,很多计算开销也是比较大的。
事实上,维系RSet中的引用关系靠post-write barrier和Concurrent refinement threads来维护。程序运行的过程中,写入某个字段就会产生一个 post-write barrier 。为了减少这个开销,将内容放入 RSet 的过程是异步的,而且经过了很多的优化:Write Barrier 把脏卡信息存放到本地缓冲区(local buffer),有专门的 GC 线程负责收集,并将相关信息传给被引用 Region 的 RSet。
参数 -XX:G1ConcRefinementThreads 或者 -XX:ParallelGCThreads 可以控制这个异步的过程。如果并发优化线程跟不上缓冲区的速度,就会在用户进程上完成。
伪代码:
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 }
post-write barrier记录了跨Region的引用更新,更新日志缓冲区则记录了那些包含更新引用的Cards。一旦缓冲区满了,Post-write barrier就停止服务了,会由Concurrent refinement threads处理这些缓冲区日志。
RSet究竟是怎么辅助GC的呢?在做YGC的时候,只需要选定young generation region的RSet作为根集,这些RSet记录了old->young的跨代引用,避免了扫描整个old generation。 而mixed gc的时候,old generation中记录了old->old的RSet,young->old的引用由扫描全部young generation region得到,这样也不用扫描全部old generation region。所以RSet的引入大大减少了GC的工作量。
CSet(Collection Set)
一组可被回收的分区Region的集合, 是多个对象的集合内存区域。
年轻代与老年代的比例
5% - 60%,一般不使用手工指定,因为这是G1预测停顿时间的基准,这地方简要说明一下,G1可以指定一个预期的停顿时间,然后G1会根据你设定的时间来动态调整年轻代的比例,例如时间长,就将年轻代比例调小,让YGC尽早行。
G1特点:
- 并行与并发
- 并行性:G1回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。
- 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行。因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况。
- 分代收集
- 从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region)。
- 将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代,每一个Region都可以根据需要,扮演年轻代的Eden空间、Survivor空间或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
- 和之前的各类回收器不同,它同时兼顾年轻代和老年代。而之前的回收器,要么负责年轻代,要么负责老年代,只能兼顾其中一个。它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于那个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。
- 空间整合
- CMS:“标记-清除”算法、内存碎片、若干次GC后进行一次碎片整理
- G1将内存划分为一个个的region。内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看做是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显。
- 停顿预测模型(即:软实时soft real-time)
- 这是G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒。这几乎已经是实时Java(RTSJ)的中软实时垃圾收集器特征了。
- 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
- G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先回收价值最大的那些Region。这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
- 相比于CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。
分区Region:
使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB,2MB,4MB,8MB,16MB,32MB。可以通过-XX:G1HeapRegionSize设定。所有的Region大小相同,且在JVM生命周期内不会被改变。G1虽然还保留有年轻代和老年代的概念,但年轻代和老年代不再是固定的,他们都是一系列Region(不需要连续)的集合。通过Region的动态分配方式实现逻辑上的连续。
一个region有可能属于Eden,Survivor或者Old/Tenured内存区域。但是一个region只可能属于一个角色。图中的E表示该region属于Eden内存区域,S表示属于Survivor内存区域,O表示属于Old内存区域。图中空白的表示未使用的内存空间。
G1垃圾收集器还增加了一种新的内存区域,叫做humongous 内存区域,如图中的H块。主要用于存储大对象(humongous object,H-obj),如果大小超过单个region 容量50%,就放到H。H-obj有如下几个特征:
- H-obj直接分配到了old gen,防止了反复拷贝移动。
- H-obj在global concurrent marking阶段的cleanup 和 full GC阶段回收。
- 在分配H-obj之前先检查是否超过 initiating heap occupancy percent和the marking threshold, 如果超过的话,就启动global concurrent marking,为的是提早回收,防止 evacuation failures 和 full GC。
为了减少连续H-objs分配对GC的影响,需要把大对象变为普通的对象,建议增大Region size。
单个Region采用指针碰撞和TLAB的方式分配内存。
一个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围从1M到32M,且是2的指数。如果不设定,那么G1会根据Heap大小自动决定。
设置H的原因:
对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个humongous区,它用来专门存放大对象。对于那些超过了整个Region容量的超级大对象,G1会直接在老年代寻找连续的H区来存储这些大对象。为了能找到连续的H区,有时候不得不启动Full GC。G1的大多数行为都把H区作为老年代的一部分来看待。
如果一个巨型对象跨越两个分区,开始的那个分区被称为“开始巨型”,后面的分区被称为“连续巨型”,这样最后一个分区的一部分空间是被浪费掉的,如果有很多巨型对象都刚好比分区大小多一点,就会造成很多空间的浪费,从而导致堆的碎片化。如果你发现有很多由于巨型对象分配引起的连续的并发周期,并且堆已经碎片化(明明空间够,但是触发了FULL GC),可以考虑调整-XX:G1HeapRegionSize
参数,减少或消除巨型对象的分配。
关于巨型对象的回收:在JDK8u40之前,巨型对象的回收只能在并发收集周期的清除阶段或FULL GC过程中过程中被回收,在JDK8u40(包括这个版本)之后,一旦没有任何其他对象引用巨型对象,那么巨型对象也可以在年轻代收集中被回收。
G1缺点:
相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占有(Footprint)还是程序运行时的额外执行负载都要比CMS要高。
就内存占用来说,虽然G1和CMS都使用卡表来处理跨代指针,但G1的卡表实现更为复杂,而且堆中每个Region,无论扮演的是年轻代还是老年代角色,都必须有一份卡表,这导致G1的记忆集(和其他内存消耗)可能会占整个堆容量的20%乃至更多的内存空间;相比起来CMS的卡表就相当简单,只有唯一一份,而且只需要处理老年代到年轻代的引用,反过来则不需要,由于年轻代的对象具有朝生夕灭的不稳定性,引用变化频繁,能省下这个区域的维护开销是很划算的。
在执行负载的角度上,同样由于两个收集器各自的细节实现特点导致了用户程序运行时的负载会有不同,譬如它们都使用到写屏障,CMS用写后屏障来更新维护卡表;而G1除了使用写后屏障来进行同样的(由于G1的卡表结构复杂,其实是更繁琐的)卡表维护操作外,为了实现原始快照搜索算法(SATB),还需要使用写前屏障来跟踪并发时的指针变化情况。相比起增量更新算法,原始快照搜索能够减少并发标记和重新标记阶段的消耗,避免CMS那样在最终标记阶段停顿时间过长的缺点,但是在用户程序运行过程中确实会产生由跟踪引用变化带来的额外负担。由于G1对写屏障的复杂操作要比CMS消耗更多的运算资源,所以CMS的写屏障实现是直接的同步操作,而G1就不得不将其实现为类似于消息队列的结构,把写前屏障和写后屏障中要做的事情都放到队列里,然后再异步处理。
从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8G之间。
G1收集器需要解决的问题
- 1.将Java堆分成多个独立的Region后,Region里面存在的跨Region引用对象如何解决?
- 可以使用记忆集避免全堆作为GC Roots扫描,但在G1收集器上记忆集的应用其实要复杂很多,它的每个Region都维护有自己的记忆集,这些记忆集会记录下别的Region指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。G1的记忆集在存储结构的本质上是一种哈希表,key是别的Region的起始地址,value是一个集合,里面存储的元素是卡表的索引号。这种“双向”的卡表结构(卡表是“我指向谁”,这种结构还记录了“谁指向我”)比原来的卡表实现起来更复杂,同时由于Region数量比传统收集器的分代数量明显更多,因此G1收集器要比其他的传统垃圾收集器有着更高的内存占用负担。根据经验,G1至少要耗费大约相当于Java堆容量10%至20%的额外内存来维持收集器工作。
- 具体流程:每次Reference类型数据写操作时,都会产生一个 写屏障(Write Barrier)暂时去终止操作;然后检查将要写入的引用 指向的对象是否和该Reference类型数据在不同的 Region(其他收集器:检查老年代对象是否引用了年轻代对象);如果不同,通过 卡表(Card Table)把相关引用信息记录到引用指向对象的所在Region对应的记忆集(Remembered Set) 中;当进行垃圾收集时,在GC Roots枚举范围加上记忆集;就可以保证不进行全局扫描了。
- 2.在并发标记阶段如何保证收集线程与用户线程互不干扰地运行?
- 这里首先要解决的是用户线程改变对象引用关系时,必须保证其不能打破原本的对象图结构,导致标记结果出现错误。CMS收集器采用增量更新算法解决该问题,而G1收集器则是通过原始快照(STAB)算法解决。此外,垃圾收集对用户线程的影响还体现在回收过程中新创建对象的内存分配上,程序要继续运行就肯定会持续有新对象被创建,G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针,吧Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。与CMS中的“Concurrent Mode Failure”失败会导致Full GC类似,如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结用户线程执行,导致Full GC而产生长时间STW。
- 3.如何建立可靠的停顿预测模型
- 用户通过-XX:MaxGCPauseMillis参数指定的停顿时间只意味着垃圾收集发生之前的期望值,但G1收集器要怎么做才能满足用户的期望呢?G1收集器的停顿预测模型是以衰减均值(Decaying Average)为理论基础实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。这里强调的衰减平均值是指它会比普通的平均值更容易受到新数据的影响,平均值代表整体平均状态,但衰减平均值更准确地代表“最近的”平均状态。换句话说,Region的统计状态越新越能决定其回收的价值。然后通过这些信息预测现在开始回收的话,由那些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。
// share/vm/gc_implementation/g1/g1CollectorPolicy.hpp double get_new_prediction(TruncatedSeq* seq) { return MAX2(seq->davg() + sigma() * seq->dsd(), seq->davg() * confidence_factor(seq->num())); }
在这个预测计算公式中:davg表示衰减均值,sigma()返回一个系数,表示信赖度,dsd表示衰减标准偏差,confidence_factor表示可信度相关系数。而方法的参数TruncateSeq,顾名思义,是一个截断的序列,它只跟踪了序列中的最新的n个元素。
在G1 GC过程中,每个可测量的步骤花费的时间都会记录到TruncateSeq(继承了AbsSeq)中,用来计算衰减均值、衰减变量,衰减标准偏差等:
// src/share/vm/utilities/numberSeq.cpp void AbsSeq::add(double val) { if (_num == 0) { // if the sequence is empty, the davg is the same as the value _davg = val; // and the variance is 0 _dvariance = 0.0; } else { // otherwise, calculate both _davg = (1.0 - _alpha) * val + _alpha * _davg; double diff = val - _davg; _dvariance = (1.0 - _alpha) * diff * diff + _alpha * _dvariance; } }
比如要预测一次GC过程中,RSet的更新时间,这个操作主要是将Dirty Card加入到RSet中,具体原理参考前面的RSet。每个Dirty Card的时间花费通过_cost_per_card_ms_seq来记录,具体预测代码如下:
// share/vm/gc_implementation/g1/g1CollectorPolicy.hpp double predict_rs_update_time_ms(size_t pending_cards) { return (double) pending_cards * predict_cost_per_card_ms(); } double predict_cost_per_card_ms() { return get_new_prediction(_cost_per_card_ms_seq); }
get_new_prediction就是我们开头说的方法
- 用户通过-XX:MaxGCPauseMillis参数指定的停顿时间只意味着垃圾收集发生之前的期望值,但G1收集器要怎么做才能满足用户的期望呢?G1收集器的停顿预测模型是以衰减均值(Decaying Average)为理论基础实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。这里强调的衰减平均值是指它会比普通的平均值更容易受到新数据的影响,平均值代表整体平均状态,但衰减平均值更准确地代表“最近的”平均状态。换句话说,Region的统计状态越新越能决定其回收的价值。然后通过这些信息预测现在开始回收的话,由那些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。
G1解决三色标记算法中漏标的办法:SATB
SATB(Snapshot At The Beginning), 在应对漏标问题时,G1使用了SATB方法来做,具体流程:
- 在开始标记的时候生成一个快照图标记存活对象
- 在一个引用断开后,要将此引用推到GC的堆栈里,保证白色对象(垃圾)还能被GC线程扫描到(在**write barrier(写屏障)**里把所有旧的引用所指向的对象都变成非白的)。
- 配合Rset,去扫描哪些Region引用到当前的白色对象,若没有引用到当前对象,则回收
SATB详细流程
- SATB是维持并发GC的一种手段。G1并发的基础就是SATB。SATB可以理解成在GC开始之前对堆内存里的对象做一次快照,此时活的对像就认为是活的,从而开成一个对象图。
- 在GC收集的时候,年轻代的对象也认为是活的对象,除此之外其他不可达的对象都认为是垃圾对象。
- 如何找到在GC过程中分配的对象呢?每个region记录着两个top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS。在TAMS以上的对象就是新分配的,因而被视为隐式marked。
- 通过这种方式我们就找到了在GC过程中新分配的对象,并把这些对象认为是活的对象。
- 解决了对象在GC过程中分配的问题,那么在GC过程中引用发生变化的问题怎么解决呢?
- G1给出的解决办法是通过Write Barrier。Write Barrier就是对引用字段进行赋值做了额外处理。通过Write Barrier就可以了解到哪些引用对象发生了什么样的变化。
- mark的过程就是遍历heap标记live object的过程,采用的是三色标记算法,这三种颜色为white(表示还未访问到)、gray(访问到但是它用到的引用还没有完全扫描)、back(访问到而且其用到的引用已经完全扫描完)。
- 整个三色标记算法就是从GC roots出发遍历heap,针对可达对象先标记white为gray,然后再标记gray为black;遍历完成之后所有可达对象都是balck的,所有white都是可以回收的。
- SATB仅仅对于在marking开始阶段进行“snapshot”(marked all reachable at mark start),但是concurrent的时候并发修改可能造成对象漏标记。
- 对black新引用了一个white对象,然后又从gray对象中删除了对该white对象的引用,这样会造成了该white对象漏标记。
- 对black新引用了一个white对象,然后从gray对象删了一个引用该white对象的white对象,这样也会造成了该white对象漏标记。
- 对black新引用了一个刚new出来的white对象,没有其他gray对象引用该white对象,这样也会造成了该white对象漏标记。
SATB效率高于增量更新的原因
因为SATB在重新标记环节只需要去重新扫描那些被推到堆栈中的引用,并配合Rset来判断当前对象是否被引用来进行回收;
并且在最后G1并不会选择回收所有垃圾对象,而是根据Region的垃圾多少来判断与预估回收价值(指回收的垃圾与回收的STW时间的一个预估值),将一个或者多个Region放到CSet中,最后将这些Region中的存活对象压缩并复制到新的Region中,清空原来的Region。
G1参数配置
-XX:+UseG1GC 手动指定使用G1收集器执行内存回收任务
-XX:G1HeapRegionSize 设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。
-XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms。一般来说,回收阶段占到几十到一百甚至接近两百毫秒都很正常,但如果我们把停顿时间调得非常低,譬如设置为二十毫秒,很可能出现的结果就是由于停顿目标时间太短,导致每次选出来的回收集只占堆内存很小的一部分,收集器收集的速度逐渐跟不上分配器分配的速度,导致垃圾慢慢堆积。很可能一开始收集器还能从空闲的堆内存中获得一些喘息的时间,但应用运行时间一长就不行了,最终占满堆引发Full GC反而降低性能,所以通常把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的。
-XX:ParallelGCThreads 设置STW期间,并行GC工作线程数。
-XX:ConcGCThreads 并发标记阶段,设置并发执行的线程数。为并行垃圾回收线程数(ParallelGCThreads)的1/4左右,四舍五入。
-XX:InitiatingHeapOccupancyPercent 设置触发标记周期的Java堆占用率阈值。超过此值,就触发GC。默认是45。这里的java堆占比指的是non_young_capacity_bytes,包括old+humongous
-XX:G1NewSizePercent年轻代最小值,默认值5%
-XX:G1MaxNewSizePercent年轻代最大值,默认值60%
G1中提供了两种垃圾回收模式:YoungGC和Mixed GC,在不同的条件下被触发。
- Young GC:选定所有年轻代里的Region。通过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销。
- Mixed GC:选定所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region。在用户指定的开销目标范围内尽可能选择收益高的老年代Region。
避免Full GC解决方案:
- 加大内存;
- 提高CPU性能,加快GC回收速度,而对象增加速度赶不上回收速度,则Full GC可以避免;
- 降低进行Mixed GC触发的阈值,让Mixed GC提早发生(默认45%)
逻辑上global concurrent marking与evacuation是相对独立的。但是global concurrent marking之中的initial marking要做的事情正好跟yong GC要做的事情有重叠(遍历根节点),所以在实现上把它们安排在一起做。
一个young GC可以顺带做initial marking,也可以不做;一个正常的initial marking(除非是System.gc()+-XX:+ExplicitGCInvokesConcurrent)总是搭在young GC上做。
一个假想的混合的STW时间线:
启动程序:
->young GC
->young GC
->young GC
->young GC + initial marking
(...concurrent marking ...)
->young GC (...concurrent marking ...)
(...concurrent marking ...)
->young GC (...concurrent marking ...)
->final marking
->clean up
->mixed GC
->mixed GC
->mixed GC
...
->mixed GC
->young GC + initial marking
(...concurrent marking ...)
...
G1收集器的收集活动主要有四种操作:
- 年轻代垃圾收集
- 后台收集、并发周期
- 混合式垃圾收集
- 必要时候的Full GC
一、年轻代垃圾收集的图例如下:
JVM 启动时,G1 会先准备好 Eden 区,程序在运行过程中不断创建对象到 Eden 区,当所有的 Eden 区都满了,G1 会启动一次年轻代垃圾回收过程。
年轻代的收集包括下面的回收阶段:
(1)扫描根
根,可以看作是我们前面介绍的 GC Roots,加上 RSet 记录的其他 Region 的外部引用。
(2)更新 RS
处理 dirty card queue 中的卡页,更新 RSet。此阶段完成后,RSet 可以准确的反映老年代对所在的内存分段中对象的引用。可以看作是第一步的补充。
(3)处理 RS
识别被老年代对象指向的 Eden 中的对象,这些被指向的 Eden 中的对象被认为是存活的对象。
(4)复制对象
收集算法依然使用的是 Copy 算法。在这个阶段,对象树被遍历,Eden 区内存段中存活的对象会被复制到 Survivor 区中空的 Region。这个过程和其他垃圾回收算法一样,包括对象的年龄和晋升。
(5)处理引用
处理 Soft、Weak、Phantom、Final、JNI Weak 等引用。结束收集。
特点:
- Eden区耗尽的时候就会触发年轻代收集,年轻代垃圾收集会对整个年轻代进行回收,会一次性回收掉年轻代的所有 Region,它的跨代引用使用 RSet 数据结构来追溯。
- 年轻代垃圾收集期间,整个应用STW
- 年轻代垃圾收集是由多线程并发执行的
- 年轻代收集结束后依然存活的对象,会被拷贝到一个新的Survivor分区,或者是老年代。
G1设计了一个标记阈值,它描述的是整个Java堆内存的使用比例,默认值是45,这个值可以通过命令-XX:InitiatingHeapOccupancyPercent(IHOP)来调整,一旦达到这个阈值就会触发一次并发收集周期。注意:这里的比例是针对整个堆大小的比例,而CMS中的CMSInitiatingOccupancyFraction命令选型是针对老年代的比例。
二、并发收集周期的图例如下:
在上图中有几个情况需要注意:
- 年轻代的空间占用情况发生了变化——在并发收集周期中,至少有一次(很可能是多次)年轻代垃圾收集;
- 注意到一些分区被标记为X,这些分区属于老年代,它们就是标记周期找出的包含最多垃圾的分区(注意:它们内部仍然保留着数据);
- 老年代的空间占用在标记周期结束后变得更多,这是因为在标记周期期间,年轻代的垃圾收集会晋升对象到老年代,而且标记周期中并不会释放老年代的任何对象。
![](https://img2020.cnblogs.com/blog/1708060/202201/1708060-20220113151718933-1572149117.png)
- 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。初始标记做的工作是设置两个TAMS变量(NTAMS和PTAMS)的值,所有在TAMS之上的对象在这个并发周期内会被识别为隐式存活对象。
- 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行,并且收集各个Region的存活对象信息。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。并发标记阶段是多线程的,我们可以通过
-XX:ConcGCThreads
来设置并发线程数,默认情况下,G1垃圾收集器会将这个线程总数设置为并行垃圾线程数(-XX:ParallelGCThreads
)的四分之一;并发标记会利用trace算法找到所有活着的对象,并记录在一个bitmap中,因为在TAMS之上的对象都被视为隐式存活,因此我们只需要遍历那些在TAMS之下的;记录在标记的时候发生的引用改变,SATB的思路是在开始的时候设置一个快照,然后假定这个快照不改变,根据这个快照去进行trace,这时候如果某个对象的引用发生变化,就需要通过pre-write barrier logs将该对象的旧的值记录在一个SATB缓冲区中,如果这个缓冲区满了,就把它加到一个全局的列表中——G1会有并发标记的线程定期去处理这个全局列表。 - 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB日志缓冲区和所有更新的引用,同时G1垃圾收集器还会找出所有未被标记的存活对象。这个阶段还会负责引用处理等工作。
- 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集线程并行完成。这个过程还会做几个事情:识别出所有空闲的分区、RSet梳理、将不用的类从metaspace中卸载、回收巨型对象等等。识别出每个分区里存活的对象有个好处是在遇到一个完全空闲的分区时,它的RSet可以立即被清理,同时这个分区可以立刻被回收并释放到空闲队列中,而不需要再放入CSet等待混合收集阶段回收;梳理RSet有助于发现无用的引用。
第一阶段initial mark是共用了Young GC的暂停,这是因为他们可以复用Root Region Scan操作,所以可以说global concurrent marking是伴随Young GC而发生的。
从上述阶段的描述可以看出,G1收集器除了并发标记外,其余阶段也是要完全暂停用户阶段的,换言之,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量。
三、混合收集的图例如下:
混合收集只会回收一部分老年代分区,上图是第一次混合收集前后的堆情况对比。
混合收集会执行多次,一直运行到(几乎)所有标记点老年代分区都被回收,在这之后就会恢复到常规的年轻代垃圾收集周期。当整个堆的使用率超过指定的百分比时,G1 GC会启动新一轮的并发标记周期。在混合收集周期中,对于要回收的分区,会将该分区中存活的数据拷贝到另一个分区,这也是为什么G1收集器最终出现碎片化的频率比CMS收集器小得多的原因——以这种方式回收对象,实际上伴随着针对当前分区的压缩。
Mixed GC发生的时机其实是由一些参数控制着的,另外也控制着哪些老年代Region会被选入CSet。
- G1HeapWastePercent:在global concurrent marking结束之后,我们可以知道old gen regions中有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC。因为这种情况下, GC 会花费很多的时间但是回收到的内存却很少。所以这个参数也是可以调整 Mixed GC 的频率的。
- G1MixedGCLiveThresholdPercent:old generation region中的存活对象的占比,只有在此参数之下,才会被选入CSet。
- G1MixedGCCountTarget:一次global concurrent marking之后,最多执行Mixed GC的次数。
- G1OldCSetRegionThresholdPercent:一次Mixed GC中能被选入CSet的最多old generation region数量。
G1日志
Young GC日志
我们先来看看Young GC的日志:
{Heap before GC invocations=12 (full 1): garbage-first heap total 3145728K, used 336645K [0x0000000700000000, 0x00000007c0000000, 0x00000007c0000000) region size 1024K, 172 young (176128K), 13 survivors (13312K) Metaspace used 29944K, capacity 30196K, committed 30464K, reserved 1077248K class space used 3391K, capacity 3480K, committed 3584K, reserved 1048576K 2014-11-14T17:57:23.654+0800: 27.884: [GC pause (G1 Evacuation Pause) (young) Desired survivor size 11534336 bytes, new threshold 15 (max 15) - age 1: 5011600 bytes, 5011600 total 27.884: [G1Ergonomics (CSet Construction) start choosing CSet, _pending_cards: 1461, predicted base time: 35.25 ms, remaining time: 64.75 ms, target pause time: 100.00 ms] 27.884: [G1Ergonomics (CSet Construction) add young regions to CSet, eden: 159 regions, survivors: 13 regions, predicted young region time: 44.09 ms] 27.884: [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 159 regions, survivors: 13 regions, old: 0 regions, predicted pause time: 79.34 ms, target pause time: 100.00 ms] , 0.0158389 secs] [Parallel Time: 8.1 ms, GC Workers: 4] [GC Worker Start (ms): Min: 27884.5, Avg: 27884.5, Max: 27884.5, Diff: 0.1] [Ext Root Scanning (ms): Min: 0.4, Avg: 0.8, Max: 1.2, Diff: 0.8, Sum: 3.1] [Update RS (ms): Min: 0.0, Avg: 0.3, Max: 0.6, Diff: 0.6, Sum: 1.4] [Processed Buffers: Min: 0, Avg: 2.8, Max: 5, Diff: 5, Sum: 11] [Scan RS (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.3] [Code Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.6] [Object Copy (ms): Min: 4.9, Avg: 5.1, Max: 5.2, Diff: 0.3, Sum: 20.4] [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [GC Worker Other (ms): Min: 0.0, Avg: 0.4, Max: 1.3, Diff: 1.3, Sum: 1.4] [GC Worker Total (ms): Min: 6.4, Avg: 6.8, Max: 7.8, Diff: 1.4, Sum: 27.2] [GC Worker End (ms): Min: 27891.0, Avg: 27891.3, Max: 27892.3, Diff: 1.3] [Code Root Fixup: 0.5 ms] [Code Root Migration: 1.3 ms] [Code Root Purge: 0.0 ms] [Clear CT: 0.2 ms] [Other: 5.8 ms] [Choose CSet: 0.0 ms] [Ref Proc: 5.0 ms] [Ref Enq: 0.1 ms] [Redirty Cards: 0.0 ms] [Free CSet: 0.2 ms] [Eden: 159.0M(159.0M)->0.0B(301.0M) Survivors: 13.0M->11.0M Heap: 328.8M(3072.0M)->167.3M(3072.0M)] Heap after GC invocations=13 (full 1): garbage-first heap total 3145728K, used 171269K [0x0000000700000000, 0x00000007c0000000, 0x00000007c0000000) region size 1024K, 11 young (11264K), 11 survivors (11264K) Metaspace used 29944K, capacity 30196K, committed 30464K, reserved 1077248K class space used 3391K, capacity 3480K, committed 3584K, reserved 1048576K } [Times: user=0.05 sys=0.01, real=0.02 secs]
每个过程的作用如下:
-
garbage-first heap total 3145728K, used 336645K [0x0000000700000000, 0x00000007c0000000, 0x00000007c0000000)
这行表示使用了G1垃圾收集器,total heap 3145728K,使用了336645K。
-
region size 1024K, 172 young (176128K), 13 survivors (13312K)
Region大小为1M,青年代占用了172个(共176128K),幸存区占用了13个(共13312K)。
-
Metaspace used 29944K, capacity 30196K, committed 30464K, reserved 1077248K
class space used 3391K, capacity 3480K, committed 3584K, reserved 1048576K
java 8的新特性,去掉永久区,添加了元数据区,这块不是本文重点,不再赘述。需要注意的是,之所以有committed和reserved,是因为没有设置MetaspaceSize=MaxMetaspaceSize。
-
[GC pause (G1 Evacuation Pause) (young)
GC原因,年轻代minor GC。
-
[G1Ergonomics (CSet Construction) start choosing CSet, _pending_cards: 1461, predicted base time: 35.25 ms, remaining time: 64.75 ms, target pause time: 100.00 ms]
发生minor GC和full GC时,所有相关region都是要回收的。而发生并发GC时,会根据目标停顿时间动态选择部分垃圾对并多的Region回收,这一步就是选择Region。_pending_cards是关于RSet的Card Table。predicted base time是预测的扫描card table时间。
-
[G1Ergonomics (CSet Construction) add young regions to CSet, eden: 159 regions, survivors: 13 regions, predicted young region time: 44.09 ms]
这一步是添加Region到collection set,Eden区一共159个Region,13个幸存区Region,这也和之前的(172 young (176128K), 13 survivors (13312K))吻合。预计收集时间是44.09 ms。
-
[G1Ergonomics (CSet Construction) finish choosing CSet, eden: 159 regions, survivors: 13 regions, old: 0 regions, predicted pause time: 79.34 ms, target pause time: 100.00 ms]
这一步是对上面两步的总结。预计总收集时间79.34ms。
-
[Parallel Time: 8.1 ms, GC Workers: 4]
由于收集过程是多线程并行(并发)进行,这里是4个线程,总共耗时8.1ms(wall clock time)
-
[GC Worker Start (ms): Min: 27884.5, Avg: 27884.5, Max: 27884.5, Diff: 0.1]
收集线程开始的时间,使用的是相对时间,Min是最早开始时间,Avg是平均开始时间,Max是最晚开始时间,Diff是Max-Min(此处的0.1貌似有问题)
-
[Ext Root Scanning (ms): Min: 0.4, Avg: 0.8, Max: 1.2, Diff: 0.8, Sum: 3.1]
扫描Roots花费的时间,Sum表示total cpu time,下同。
-
[Update RS (ms): Min: 0.0, Avg: 0.3, Max: 0.6, Diff: 0.6, Sum: 1.4] [Processed Buffers: Min: 0, Avg: 2.8, Max: 5, Diff: 5, Sum: 11]
Update RS (ms)是每个线程花费在更新Remembered Set上的时间。
-
[Scan RS (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.3]
扫描CS中的region对应的RSet,因为RSet是points-into,所以这样实现避免了扫描old generadion region,但是会产生float garbage。
-
[Code Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.6]
扫描code root耗时。code root指的是经过JIT编译后的代码里,引用了heap中的对象。引用关系保存在RSet中。
-
[Object Copy (ms): Min: 4.9, Avg: 5.1, Max: 5.2, Diff: 0.3, Sum: 20.4]
拷贝活的对象到新region的耗时。
-
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
线程结束,在结束前,它会检查其他线程是否还有未扫描完的引用,如果有,则"偷"过来,完成后再申请结束,这个时间是线程之前互相同步所花费的时间。
-
[GC Worker Other (ms): Min: 0.0, Avg: 0.4, Max: 1.3, Diff: 1.3, Sum: 1.4]
花费在其他工作上(未列出)的时间。
-
[GC Worker Total (ms): Min: 6.4, Avg: 6.8, Max: 7.8, Diff: 1.4, Sum: 27.2]
每个线程花费的时间和。
-
[GC Worker End (ms): Min: 27891.0, Avg: 27891.3, Max: 27892.3, Diff: 1.3]
每个线程结束的时间。
-
[Code Root Fixup: 0.5 ms]
用来将code root修正到正确的evacuate之后的对象位置所花费的时间。
-
[Code Root Migration: 1.3 ms]
更新code root 引用的耗时,code root中的引用因为对象的evacuation而需要更新。
-
[Code Root Purge: 0.0 ms]
清除code root的耗时,code root中的引用已经失效,不再指向Region中的对象,所以需要被清除。
-
[Clear CT: 0.2 ms]
清除card table的耗时。
-
[Other: 5.8 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 5.0 ms]
[Ref Enq: 0.1 ms]
[Redirty Cards: 0.0 ms]
[Free CSet: 0.2 ms]
其他事项共耗时5.8ms,其他事项包括选择CSet,处理已用对象,引用入ReferenceQueues,释放CSet中的region到free list。
-
[Eden: 159.0M(159.0M)->0.0B(301.0M) Survivors: 13.0M->11.0M Heap: 328.8M(3072.0M)->167.3M(3072.0M)]
年轻代清空了,下次扩容到301MB。
global concurrent marking 日志
对于global concurrent marking过程,它的日志如下所示:
66955.252: [G1Ergonomics (Concurrent Cycles) request concurrent cycle initiation, reason: occupancy higher than threshold, occupancy: 1449132032 bytes, allocation request: 579608 bytes, threshold: 1449 551430 bytes (45.00 %), source: concurrent humongous allocation] 2014-12-10T11:13:09.532+0800: 66955.252: Application time: 2.5750418 seconds 66955.259: [G1Ergonomics (Concurrent Cycles) request concurrent cycle initiation, reason: requested by GC cause, GC cause: G1 Humongous Allocation] {Heap before GC invocations=1874 (full 4): garbage-first heap total 3145728K, used 1281786K [0x0000000700000000, 0x00000007c0000000, 0x00000007c0000000) region size 1024K, 171 young (175104K), 27 survivors (27648K) Metaspace used 116681K, capacity 137645K, committed 137984K, reserved 1171456K class space used 13082K, capacity 16290K, committed 16384K, reserved 1048576K 66955.259: [G1Ergonomics (Concurrent Cycles) initiate concurrent cycle, reason: concurrent cycle initiation requested] 2014-12-10T11:13:09.539+0800: 66955.259: [GC pause (G1 Humongous Allocation) (young) (initial-mark) ……. 2014-12-10T11:13:09.597+0800: 66955.317: [GC concurrent-root-region-scan-start] 2014-12-10T11:13:09.597+0800: 66955.318: Total time for which application threads were stopped: 0.0655753 seconds 2014-12-10T11:13:09.610+0800: 66955.330: Application time: 0.0127071 seconds 2014-12-10T11:13:09.614+0800: 66955.335: Total time for which application threads were stopped: 0.0043882 seconds 2014-12-10T11:13:09.625+0800: 66955.346: [GC concurrent-root-region-scan-end, 0.0281351 secs] 2014-12-10T11:13:09.625+0800: 66955.346: [GC concurrent-mark-start] 2014-12-10T11:13:09.645+0800: 66955.365: Application time: 0.0306801 seconds 2014-12-10T11:13:09.651+0800: 66955.371: Total time for which application threads were stopped: 0.0061326 seconds 2014-12-10T11:13:10.212+0800: 66955.933: [GC concurrent-mark-end, 0.5871129 secs] 2014-12-10T11:13:10.212+0800: 66955.933: Application time: 0.5613792 seconds 2014-12-10T11:13:10.215+0800: 66955.935: [GC remark 66955.936: [GC ref-proc, 0.0235275 secs], 0.0320865 secs] [Times: user=0.05 sys=0.00, real=0.03 secs] 2014-12-10T11:13:10.247+0800: 66955.968: Total time for which application threads were stopped: 0.0350098 seconds 2014-12-10T11:13:10.248+0800: 66955.968: Application time: 0.0001691 seconds 2014-12-10T11:13:10.250+0800: 66955.970: [GC cleanup 1178M->632M(3072M), 0.0060632 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 2014-12-10T11:13:10.256+0800: 66955.977: Total time for which application threads were stopped: 0.0088462 seconds 2014-12-10T11:13:10.257+0800: 66955.977: [GC concurrent-cleanup-start] 2014-12-10T11:13:10.259+0800: 66955.979: [GC concurrent-cleanup-end, 0.0024743 secs
这次发生global concurrent marking的原因是:humongous allocation,上面提过在巨大对象分配之前,会检测到old generation 使用占比是否超过了 initiating heap occupancy percent(45%),因为
1449132032(used)+ 579608(allocation request:) > 1449551430(threshold),所以触发了本次global concurrent marking。对于具体执行过程,上面的表格已经详细讲解了。值得注意的是上文中所说的initial mark往往伴随着一次YGC,在日志中也有体现:GC pause (G1 Humongous Allocation) (young) (initial-mark)。
G1执行过程中的异常情况
并发标记周期开始后的FULL GC
G1启动了标记周期,但是在并发标记完成之前,就发生了Full GC,日志常常如下所示:
51.408: [GC concurrent-mark-start]
65.473: [Full GC 4095M->1395M(4096M), 6.1963770 secs]
[Times: user=7.87 sys=0.00, real=6.20 secs]
71.669: [GC concurrent-mark-abort]
GC concurrent-mark-start开始之后就发生了FULL GC,这说明针对老年代分区的回收速度比较慢,或者说对象过快得从年轻代晋升到老年代,或者说是有很多大对象直接在老年代分配。针对上述原因,我们可能需要做的调整有:调大整个堆的大小、更快得触发并发回收周期、让更多的回收线程参与到垃圾收集的动作中。
混合收集模式中的FULL GC
在GC日志中观察到,在一次混合收集之后跟着一条FULL GC,这意味着混合收集的速度太慢,在老年代释放出足够多的分区之前,应用程序就来请求比当前剩余可分配空间大的内存。针对这种情况我们可以做的调整:增加每次混合收集收集掉的老年代分区个数;增加并发标记的线程数;提高混合收集发生的频率。
疏散失败(转移失败)
在年轻代垃圾收集快结束时,找不到可用的分区接收存活下来的对象,常见如下的日志:
60.238: [GC pause (young) (to-space overflow), 0.41546900 secs]
这意味着整个堆的碎片化已经非常严重了,我们可以从以下几个方面调整:
-
增加整个堆的大小——通过增加-XX:G1ReservePercent选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量;
-
通过减少 -XX:InitiatingHeapOccupancyPercent提前启动标记周期;
-
你也可以通过增加-XX:ConcGCThreads选项的值来增加并发标记线程的数目;
巨型对象分配失败
如果在GC日志中看到莫名其妙的FULL GC日志,又对应不到上述讲过的几种情况,那么就可以怀疑是巨型对象分配导致的,这里我们可以考虑使用jmap命令进行堆dump,然后通过MAT对堆转储文件进行分析。
G1的调优
G1的调优目标主要是在避免FULL GC和疏散失败的前提下,尽量实现较短的停顿时间和较高的吞吐量。关于G1 GC的调优,需要记住以下几点:
-
不要自己显式设置年轻代的大小(用Xmn或-XX:NewRatio参数),如果显式设置年轻代的大小,会导致目标时间这个参数失效。
-
由于G1收集器自身已经有一套预测和调整机制了,因此我们首先的选择是相信它,即调整-XX:MaxGCPauseMillis=N参数,这也符合G1的目的——让GC调优尽量简单,这里有个取舍:如果减小这个参数的值,就意味着会调小年轻代的大小,也会导致年轻代GC发生得更频繁,同时,还会导致混合收集周期中回收的老年代分区减少,从而增加FULL GC的风险。这个时间设置得越短,应用的吞吐量也会受到影响。
-
针对混合垃圾收集的调优。如果调整这期望的最大暂停时间这个参数还是无法解决问题,即在日志中仍然可以看到FULL GC的现象,那么就需要自己手动做一些调整,可以做的调整包括:
-
调整G1垃圾收集的后台线程数,通过设置-XX:ConcGCThreads=n这个参数,可以增加后台标记线程的数量,帮G1赢得这场你追我赶的游戏;
-
调整G1垃圾收集器并发周期的频率,如果让G1更早得启动垃圾收集,也可以帮助G1赢得这场比赛,那么可以通过设置-XX:InitiatingHeapOccupancyPercent这个参数来实现这个目标,如果将这个参数调小,G1就会更早得触发并发垃圾收集周期。这个值需要谨慎设置:如果这个参数设置得太高,会导致FULL GC出现得频繁;如果这个值设置得过小,又会导致G1频繁得进行并发收集,白白浪费CPU资源。通过GC日志可以通过一个点来判断GC是否正常——在一轮并发周期结束后,需要确保堆剩下的空间小于InitiatingHeapOccupancyPercent的值。
-
调整G1垃圾收集器的混合收集的工作量,即在一次混合垃圾收集中尽量多处理一些分区,可以从另外一方面提高混合垃圾收集的频率。在一次混合收集中可以回收多少分区,取决于三个因素:(1)有多少个分区被认定为垃圾分区,-XX:G1MixedGCLiveThresholdPercent=n这个参数表示如果一个分区中的存活对象比例超过n,就不会被挑选为垃圾分区,因此可以通过这个参数控制每次混合收集的分区个数,这个参数的值越大,某个分区越容易被当做是垃圾分区;(2)G1在一个并发周期中,最多经历几次混合收集周期,这个可以通过-XX:G1MixedGCCountTarget=n设置,默认是8,如果减小这个值,可以增加每次混合收集收集的分区数,但是可能会导致停顿时间过长;(3)期望的GC停顿的最大值,由MaxGCPauseMillis参数确定,默认值是200ms,在混合收集周期内的停顿时间是向上规整的,如果实际运行时间比这个参数小,那么G1就能收集更多的分区。
G1的最佳实践
1. 关键参数项
-
-XX:+UseG1GC,告诉JVM使用G1垃圾收集器
-
-XX:MaxGCPauseMillis=200,设置GC暂停时间的目标最大值,这是个柔性的目标,JVM会尽力达到这个目标
-
-XX:INitiatingHeapOccupancyPercent=45,如果整个堆的使用率超过这个值,G1会触发一次并发周期。记住这里针对的是整个堆空间的比例,而不是某个分代的比例。
2. 最佳实践
不要设置年轻代的大小
通过-Xmn显式设置年轻代的大小,会干扰G1收集器的默认行为:
-
G1不再以设定的暂停时间为目标,换句话说,如果设置了年轻代的大小,就无法实现自适应的调整来达到指定的暂停时间这个目标
-
G1不能按需扩大或缩小年轻代的大小
响应时间度量
不要根据平均响应时间(ART)来设置-XX:MaxGCPauseMillis=n这个参数,应该设置希望90%的GC都可以达到的暂停时间。这意味着90%的用户请求不会超过这个响应时间,记住,这个值是一个目标,但是G1并不保证100%的GC暂停时间都可以达到这个目标
3. G1 GC的参数选项
格式:参数名含义默认值
-XX:+UseG1GC使用G1收集器JDK1.8中还需要显式指定
-XX:MaxGCPauseMillis=n设置一个期望的最大GC暂停时间,这是一个柔性的目标,JVM会尽力去达到这个目标200
-XX:InitiatingHeapOccupancyPercent=n当整个堆的空间使用百分比超过这个值时,就会触发一次并发收集周期,记住是整个堆45
-XX:NewRatio=n年轻代和老年代的比例2
-XX:SurvivorRatio=nEden空间和Survivor空间的比例8
-XX:MaxTenuringThreshold=n对象在年轻代中经历的最多的年轻代收集,或者说最大的岁数G1中是15
-XX:ParallelGCThreads=n设置垃圾收集器的并行阶段的垃圾收集线程数不同的平台有不同的值
-XX:ConcGCThreads=n设置垃圾收集器并发执行GC的线程数n一般是ParallelGCThreads的四分之一
-XX:G1ReservePercent=n设置作为空闲空间的预留内存百分比,以降低目标空间溢出(疏散失败)的风险。默认值是 10%。增加或减少这个值,请确保对总的 Java 堆调整相同的量10
-XX:G1HeapRegionSize=n分区的大小堆内存大小的1/2000,单位是MB,值是2的幂,范围是1MB到32MB之间
-XX:G1HeapWastePercent=n设置您愿意浪费的堆百分比。如果可回收百分比小于堆废物百分比,JavaHotSpotVM不会启动混合垃圾回收周期(注意,这个参数可以用于调整混合收集的频率)。JDK1.8是5
-XX:G1MixedGCCountTarget=8设置并发周期后需要执行多少次混合收集,如果混合收集中STW的时间过长,可以考虑增大这个参数。(注意:这个可以用来调整每次混合收集中回收掉老年代分区的多少,即调节混合收集的停顿时间)8
-XX:G1MixedGCLiveThresholdPercent=n一个分区是否会被放入mix GC的CSet的阈值。对于一个分区来说,它的存活对象率如果超过这个比例,则改分区不会被列入mixed gc的CSet中JDK1.6和1.7是65,JDK1.8是85
常见问题
-
Young GC、Mixed GC和Full GC的区别? 答:Young GC的CSet中只包括年轻代的分区,Mixed GC的CSet中除了包括年轻代分区,还包括老年代分区;Full GC会暂停整个引用,同时对年轻代和老年代进行收集和压缩。
-
ParallelGCThreads和ConcGCThreads的区别? 答:ParallelGCThreads指得是在STW阶段,并行执行垃圾收集动作的线程数,ParallelGCThreads的值一般等于逻辑CPU核数,如果CPU核数大于8,则设置为
5/8 * cpus
,在SPARC等大型机上这个系数是5/16。;ConcGCThreads指的是在并发标记阶段,并发执行标记的线程数,一般设置为ParallelGCThreads的四分之一。 -
write barrier在GC中的作用?如何理解G1 GC中write barrier的作用? 写屏障是一种内存管理机制,用在这样的场景——当代码尝试修改一个对象的引用时,在前面放上写屏障就意味着将这个对象放在了写屏障后面。write barrier在GC中的作用有点复杂,我们这里以trace GC算法为例讲下:trace GC有些算法是并发的,例如CMS和G1,即用户线程和垃圾收集线程可以同时运行,即mutator一边跑,collector一边收集。这里有一个限制是:黑色的对象不应该指向任何白色的对象。如果mutator视图让一个黑色的对象指向一个白色的对象,这个限制就会被打破,然后GC就会失败。针对这个问题有两种解决思路:(1)通过添加read barriers阻止mutator看到白色的对象;(2)通过write barrier阻止mutator修改一个黑色的对象,让它指向一个白色的对象。write barrier的解决方法就是讲黑色的对象放到写write barrier后面。如果真得发生了white-on-black这种写需求,一般也有多种修正方法:增量得将白色的对象变灰,将黑色的对象重新置灰等等。我理解,增量的变灰就是CMS和G1里并发标记的过程,将黑色的对象重新变灰就是利用卡表或SATB的缓冲区将黑色的对象重新置灰的过程,当然会在重新标记中将所有灰色的对象处理掉。
G1回收器的适用场景
-
面向服务端应用,针对具有大内存、多处理器的机器,需要较低延迟的场景。。
-
最主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案;如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;(G1通过每次只清理一部分而不是全部的Region的增量式清理来保证每次GC停顿时间不会过长)。
-
G1 作为 CMS 的代替者出现,解决了 CMS 中的各种疑难问题,包括暂停时间的可预测性,并终结了堆内存的碎片化。对单业务延迟非常敏感的系统来说,如果 CPU 资源不受限制,那么 G1 可以说是 HotSpot 中最好的选择,特别是在最新版本的 JVM 中。当然这种降低延迟的优化也不是没有代价的:由于额外的写屏障和守护线程,G1 的开销会更大。如果系统属于吞吐量优先型的,又或者 CPU 持续占用 100%,而又不在乎单次 GC 的暂停时间,那么 CMS 是更好的选择。在下面的情况时,使用G1可能比CMS好:HotSpot 垃圾收集器里,除了G1以外,其他的垃圾收集器使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用线程承担后台运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程。
- 超过50%的Java堆被活动数据占用;
- 对象分配频率或年代提升频率变化很大;
- GC停顿时间过长(长于0.5至1秒)
G1和CMS的区别
-
G1从整体上来看是 标记-整理 算法,但从局部(两个Region之间)是复制算法。而CMS是 标记-清除算法 所以说,G1不会产生内存碎片,而CMS会产生内存碎片
-
CMS使用了 写后屏障来维护卡表,而G1不仅使用了写后屏障来维护卡表,还是用了 写前屏障来跟踪并发时的指针变化情况(为了实现原始快照)。
-
CMS对Java堆内存使用的是传统的 年轻代和老年代划分方法,而G1使用的全新的划分方法。
-
CMS收集器只收集老年代,可以配合年轻代的Serial和ParNew收集器一起使用。G1收集器收集范围是老年代和年轻代。不需要结合其他收集器使用
-
CMS使用 增量更新解决并发标记下出现的错误标记问题,而G1使用原始快照解决
ZGC
G1 的预测模型也会有失效的时候,它并不是总如我们期望的那样运行,尤其是给它定下一个苛刻的目标之后。另外,如果应用的内存非常吃紧,对内存进行部分回收根本不够,始终要进行整个 Heap 的回收,那么 G1 要做的工作量就一点也不会比其他垃圾回收器少,而且因为本身算法复杂了,还可能比其他回收器要差。
所以垃圾回收器本身的优化和升级,从来都没有停止过。最新的 ZGC 垃圾回收器,就有 3 个令人振奋的 Flag:
- 停顿时间不会超过 10ms;
- 停顿时间不会随着堆的增大而增大(不管多大的堆都能保持在 10ms 以下);
- 可支持几百 M,甚至几 T 的堆大小(最大支持 4T)。
在 ZGC 中,连逻辑上的年轻代和老年代也去掉了,只分为一块块的 page,每次进行 GC 时,都会对 page 进行压缩操作,所以没有碎片问题。ZGC 还能感知 NUMA 架构,提高内存的访问速度。与传统的收集算法相比,ZGC 直接在对象的引用指针上做文章,用来标识对象的状态,所以它只能用在 64 位的机器上。
现在在线上使用 ZGC 的还非常少。即使是用,也只能在 Linux 平台上使用。等待它的普及,还需要一段时间。