垃圾收集器G1和ZGC详解
1、G1(Garbage-First)
开启参数:-XX:+UseG1GC
G1是面向服务器的一款垃圾收集器,主要针对于多核处理器的大内存机器,可以满足gc的停顿时间且保证吞吐量,一般8g以上推荐使用G1,G1抛弃了之前堆中严格的分代内存划分,如下图:
G1对堆模型的处理转换成了如下图方式,将整个堆内存划分成一个个小的独立区域(Region),JVM最多可以有2048个Region,也可以用参数-XX:G1HeapRegionSize指定Region的大小,一般不推荐指定,虽然G1依然有分代内存划分,但抛弃了连续的分代,他们可以是一些不连续的Region集合,正因为这样,每一个Region区域的功能会发生变化,比如一个Region之前按是年轻代,在做完垃圾回收之后又变成了老年代。
1.1、分代特性
年轻代:默认占整个堆的5%,可以通过设置-XX:G1NewSizePercent参数调整年轻代的初始占比,在运行过程中,JVM会动态的调整年轻代的占比,但最多不会超过整个堆的60%,最大占比也可以通过-XX:G1MaxNewSizePercent进行调整,年轻代的Eden区和Survivor区比例也是8:1:1。
Humongous:专门放大对象的区域,当一个对象的大小超过了一个Region区域的50%,则为大对象,直接放到Humongous区,在MixedGC或Full gc的时候会回收。
1.2、G1回收流程
由下图可以看出在初始标记,最终标记以及筛选回收都会STW,而且它没有并发重置。
初始标记:先STW,并记录下gc roots直接能引用的对象,速度很快,如果不做STW,gc root会非常多。
并发标记:根据初始标记的结果,做整个的一个可达性分析,找出所有的被引用的对象,这个过程耗时比较长(大约占整个收集过程的80%左右),但是这个过程和用户线程并发执行,所以用户无感知,但是因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变(就比如在并发标记前是非垃圾,标记之后是垃圾或者并发标记前是垃圾,并发标记后变非垃圾),详情这篇文章中的三色标记处理。
最终标记:先STW,同时修复在并发标记里面出现状态变换的对象,主要用到三色标记里的原始快照算法(见下面详解)做重新标记,详情这篇文章中的三色标记。
筛选回收:会根据用户所指定的STW参数-XX:MaxGCPauseMillis(最大垃圾回收停顿时间)来制定回收计划,判断这轮回收需要回收多少个,所以这个阶段不一定会将所有的垃圾都回收掉,它在回收之前对堆有一个区域的回收时间估算,如果回收1/2就达到了用户指定的最大停顿时间,那么就只会回收1/2,但是这1/2如何去选具体是哪一块区域,有一个筛选算法在下面详解,剩下的在下一次的垃圾回收去回收,这也就是为什么没有重置标记,其实筛选回收是可以与用户线程并发执行的,但是由于我们指定了最大停顿时间,所以,在保证时间的情况下为了提高吞吐量,我们进行了STW,回收完成之后将旧的地址转换成新的地址。(注意:其实这个阶段可以与用户线程并发执行,但是由于G1内部算法过于复杂,没有实现并发执行,不过到了Shenandoah就实现了并发收集,Shenandoah可以看成是G1的升级版本。)
1.3、G1回收主要算法
1.3.1、G1垃圾回收算法
G1回收无论是年轻代还是老年代主要的回收算法是复制算法,它会将要回收Region的存活对象挪到相邻的空的Region,然后清空之前的Region,这样就保证了内存碎片的减少。
1.3.2、G1筛选回收筛选算法
在G1收集器后台维护了一个优先列表,每次根据允许的收集时间,选择回收价值最大的Region,比如在同等大小的两个Region下,回收一个需要100ms,一个需要50ms,那么G1肯定会优先回收那个50ms的,这样就保证了在有效时间内能回收更多的堆空间,回收时间就是复制的时间,要复制的存活对象越多,回收时间就越长,回收的效益就越低,被回收的优先级就越低。
1.4、G1垃圾收集器的特点
1、并行与并发:G1能充分利用CPU、多核的环境优势来缩短STW停顿时间,部分其他收集器需要STW的区域,G1也可以并发执行。
2、分代收集:虽然G1去掉了连续内存空间分代的概念,也不需要其他收集器配合使用,但分代收集依然存在。
3、空间整合:整体上看,G1采用了标记–整理的算法,局部看是标记–复制算法。
4、可预测停顿时间:用户可通过-XX:MaxGCPauseMillis参数设置最大停顿时间,拥有良好的用户体验,但是这个参数不可随意设置,不能设置的太小,否则每一次minor gc时间过短,收集的垃圾太少,容易触发full gc。
1.5、G1垃圾收集分类
minor gc:Eden区的默认大小为5%,当Eden区放满的时候,G1不会马上做minor gc,它会先判断一下,触发一次minor gc的时间与我们用户设置的最大停顿时间的差距,如果大于或者接近最大停顿时间,则立即触发minor gc,如果回收时间比最大停顿时间小很多的话,将会扩大Eden区,继续放对象,过一段时间再次判断,依次重复,直到做了minor gc。
mixed gc:不是full gc,我们有一个参数-XX:InitiatingHeapOccupancyPercent可设定当老年代的占比达到一定的大小之后触发mixed gc,mixed gc主要回收所有年轻代和部分的老年代以及大对象区域,由于底层是复制算法,对剩余空间大小的要求比较高,所以,触发mixed gc的占比必须要调整到合适大小,如果没有足够的region去复制存活的对象,将会触发full gc。
full gc:先STW,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,非常耗时。在后续有一个收集器版本Shenandoah就优化成多线程收集了。
1.6、G1垃圾收集器优化建议
G1的调优主要就是针对于-XX:MaxGCPauseMills参数去调优的,这个参数不能太大也不能太小。
太大:如果这个参数过于大,minor gc将很少发生,在它发生时有极大可能会有大量的存活对象进入Survivor区,如果Survivor放不下,就会进入老年代,就很容易触发mixed gc,不建议。
太小:在触发minor gc时很难回收到垃圾,最后导致垃圾太多,空间被占,也很容易触发mixed gc。
1.7、G1的适合场景
1、50%以上的堆被存活对象占用:当大多数对象都存活的时候,说明老年代被占用的比例也会很大,这个时候就会触发full gc,full gc是很慢的,如果我们使用G1,那么G1就会触发mixed gc,而且mixed gc的GC最大停顿时间还是可控的。
2、对象分配和晋升的速度变化非常大:说明了对象往老年代挪动的频率很频繁,一样的,可以减少full gc的发生。
3、垃圾回收时间特别长,超过1秒:可以设置停顿时间,提升用户体验。
4、8GB以上的堆内存(建议值):内存如果在8G以下,收集的垃圾不是很多,而G1的算法相对于CMS较为复杂,还很有可能效率不如CMS,但是对于大内存,STW时间比较长,所以,在可控停顿时间这里,G1比较合适。
5、停顿时间是500ms以内:停顿时间可由用户控制。
2、ZGC(Z Garbage Collector)
2.1、版本支持
ZGC从jdk11开始支持,目前还是一个实验性版本,ZGC可以说源自于是Azul System公司开发的C4(Concurrent Continuously Compacting Collector) 收集器,主要支持平台如下:
2.2、ZGC目标
支持TB级别:根据官方文档来看,在Jdk11时ZGC可支持的最大内存为4TB,在jdk13可以支持16TB。
最大停顿时间不超过10ms:之所以能控制在10ms以下,是因为它的停顿时间主要跟Root扫描有关,而跟root数量和堆的大小没有关系。
奠定未来GC特性的基础。
最坏的情况下吞吐量会下降15%。
2.3、暂时不分代
分代是因为很多对象的生命周期不同而产生的。ZGC暂时不分代,是因为ZGC的底层算法比较复杂,暂时还没有写分代,后续会加入。
2.4、ZGC内存布局
ZGC内部是以Region的方式进行内存布局的,暂时没有设置分代,使用读屏障、颜色指针等技术来实现可并发的标记–整理算法的,且低延迟,Region分为大中小三种类型,详情如下:
小型Region(small Region):容量为2mb,用于放置小于256kb的对象。
中型Region(medium Region):容量为4mb,用于放置容量大于等于256kb但小于4mb的对象。
大型Region(large Region):容量不固定,大小可变,但必须是2的整数倍,且大小大于等于4mb的对象。
注意:由于大型Region的容量大小可变,所以,它的容量有可能小于中型Region,容量最低可达4mb,因为大型Region只放4mb以上的对象(包括4mb),且每个大型Region只存放一个大对象,所以大型对象不会被ZGC重分配(重分配是ZGC的一种处理动作,用于复制对象的收集器阶段),第一,大对象复制的代价比较高,第二,一个Region只会分配一个大对象,所以,当成为垃圾时直接销毁即可。
2.5、NUMA-aware
说到NUMA就有一个UMA,NUMA是在UMA上面改进的,ZGC能自动感知NUMA架构并充分利用NUMA架构特性的,下面具体描述:
UMA:全称Uniform Memory Access Architecture(统一内存访问体系结构),看到下图我们可以看出,UMA是多核CPU同时访问一块内存,这样的话就有内存的竞争,就会有锁,对应的就会有效率的降低,当CPU的核数越多,竞争就越激烈。
NUMA:全称Non Uniform Memory AccessArchitecture(非统一内存访问体系结构),如图所示,它会将内存划分为相等大小的区域,每一块CPU会优先访问某一块内存,这样冲突就降低了很多,当自己优先的这块内存已被用完,它可以去访问其它内存块。
2.6、ZGC的运作过程
ZGC的一个大致的运作过程如下图所示:
ZGC大体分为4个阶段,每个小阶段下面又分一些,详情如下:
1、并发标记(Concurrent Mark):
初始标记(Mark Start):先STW,并记录下gc roots直接引用的对象。
并发标记(Concurrent Mark):根据初始标记的结果,基于gc roots可达性分析算法找出所有被引用的对象,在G1中使用三色标记对对象的状态做维护,ZGC使用颜色指针做标记(颜色指针详情在博客后面的颜色指针)。
最终标记(Mark End):先STW,然后修复一些在并发标记过程中垃圾状态出现变化的对象。
2、并发预备重分配(Concurrent Prepare for Relocate):这个阶段ZGC会根据特定的查询条件扫描一下所有的Region并得出本次收集过程中需要清理哪些Region,将它们重新组成重分配集(Relocation Set),用范围更大的扫描成本换取省去G1中记忆集的维护成本。
3、并发重分配(Concurrent Relocate):
初始重分配(Relocate Start):做一些并发重分配的初始化动作。
并发重分配(Concurrent Relocate):这个阶段需要将并发预备重分配阶段计算出来的重分配集中的Region复制到新的Region并为每一个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系,从转发表ZGC就可以明确的知道哪些对象是否处于重分配集之中,在这个阶段时,如果有用户线程访问这个对象,这次访问将会被预置的内存屏障(读屏障)所截获,然后根据Region的转发表找出新的地址并访问,如果有更新再更新地址上的值,并使其指向新对象(这样子只有第一次访问时会变慢,后面的就可以不通过读屏障和转发表直接访问),ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。注意:一旦一个Region中的对象全部复制完成,旧的Region就可以清理释放掉了,但是转发表不能立即释放,因为可能还有访问在使用这个转发表,因为对象的旧地址转新地址是对象在被引用之后才会进行的操作。
4、并发重映射(Concurrent Remap):重映射其实就是将旧的地址转换为新的地址,由于ZGC中对象引用存在“自愈”功能,所以这个阶段其实不做也是可以的,ZGC很巧妙的将这一阶段合并到了下一次的并发标记阶段,反正他们都是要遍历所有对象的,这样也就减少了一次遍历对象的开销,一个Region的所有对象都被修改后,那么这个Region对应的转发表就会被销毁掉。
2.7、ZGC的读屏障
在并发重分配的时候,每进行一个对象的复制移动会对其颜色指针的Remapped标识赋值,标识这个指针被gc过,并且还会为其加一个读屏障,使得用户线程访问这个对象时可以知道这个对象的地址被改变了,程序就应该暂停一下,先更新一下地址,再进行访问值的操作,正是因为Load Barriers的存在,所以会导致配置ZGC的应用的吞吐量会变低。官方的测试数据是需要多出额外4%的开销。
2.8、ZGC的触发机制
1、定时触发:默认关闭,可通过ZCollectionInterval参数配置。
2、预热触发:最多三次,在堆内存达到10%、20%、30%时触发,主要是统计GC时间,为其他GC机制使用。
3、分配速率:基于正态分布统计,计算内存99.9%可能的最大分配速率,以及此速率下内存将要耗尽的时间点,在耗尽之前触发GC(耗尽时间 - 一次GC最大持续时间 - 一次GC检测周期时间)。
4、主动触发:(默认开启,可通过ZProactive参数配置) 距上次GC堆内存增长10%,或超过5分钟时,对比距上次GC的间隔时间跟(49 * 一次GC的最大持续时间),超过则触发。
10、辅助知识
10.1、G1收集器参数
参数 | 含义 |
---|---|
-XX:+UseG1GC | 启用G1垃圾收集器 |
-XX:ParallelGCThreads | 指定GC工作的线程数量 |
-XX:G1HeapRegionSize | 指定region的大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个region |
-XX:MaxGCPauseMillis | 最大GC暂停时间(默认为200ms) |
-XX:G1NewSizePercent | 年轻代内存初始空间(默认整堆5%) |
-XX:G1MaxNewSizePercent | 年轻代内存最大空间占比(默认整堆60%) |
-XX:TargetSurvivorRatio | survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代 |
-XX:MaxTenuringThreshold | 对象在年轻代的最大存活年龄(默认为15) |
-XX:InitiatingHeapOccupancyPercent | mixed gc发生的老年代和整个堆的占比阈值(默认为45%) |
-XX:G1MixedGCLiveThresholdPercent | region中存活的对象占比低于这个值才回收此region,否则回收的效益比不大(默认比例为85%) |
-XX:G1MixedGCCountTarget | 在一次回收阶段做几次筛选回收(默认为8次),意思是在最后一个筛选回收阶段可以分次回收,以至于一次的停顿时间不会过长,提高用户体验 |
-XX:G1HeapWastePercent | gc过程中空出来的region是否充足阈值(默认整堆5%) ,在执行mixed时用的是复制算法,会将旧region的存活对象放到新region,这样旧region就空闲出来,如果空闲出来的region区域达到整堆的5%,这次的mixed gc就结束了 |
10.2、每秒几十万并发的系统使用G1优化JVM
高并发的消息中间件比如Kafka,一般每秒要处理几万甚至几十万的消息,所以Kafka部署的时候需要大内存(一般都是64G),但是这种消息一般都是朝生夕舍的,不会存活太长时间,一般这种对象最好让它进行minor gc是最好的,所以我们在64G中可以为年轻代分配30–50个G,但是问题又来了,当内存这么大的时候minor gc会很慢,这个时候我们就可以使用G1垃圾收集器对minor gc进行最大停顿时间控制,让它边回收程序边运行(因为总内存大,所以就算设置100ms,也会回收好几个G,也可以空出一点空间继续执行程序)。
10.3、为什么CMS用增量更新,G1用SATB
我的理解:
- 因为增量更新之后会重新深度扫描,G1是以region的方式存储对象,而CMS是以一个连续的老年代存储对象,G1会涉及到跨代扫描,G1的代价相对于CMS要高。
- 而且G1较CMS更强调用户体验,重新深度扫描会加大STW时间,所以G1选择原始快照。
10.4、颜色指针(Colored Pointers)
同这一篇博客的三色标记,三色标记可能是将是否为垃圾标志放在了对象头中的gc标记,颜色指针是将对象是否为垃圾标志放在了指向对象地址的指针上面,例如下图所示:
- 18位:预留给后续使用
- 1位:Finalizable标识,此位与并发引用处理有关,它表示这个对象只能通过finalizer才能访问。
- 1位:Remapped标识,设置此位的值后,对象未指向relocation set中(relocation set表示需要GC的Region集合),有值就说明对象被gc复制过。
- 1位:Marked1标识。
- 1位:Marked0标识,和上面的Marked1都是标记对象用于辅助GC。
- 42位:对象的地址(在java11版本,ZGC支持4TB,2^42=4T)
10.4.1、为什么会有2个mark标记
两个标记是每一个gc周期时他都会去轮换使用,使上次GC周期中修正的已标记状态失效,所有引用都变成未标记。
- 当使用mark0时,周期结束后所有引用mark标记的都会成为01。
- 当使用mark1时,周期结束后所有引用mark标记的都会成为10。
10.4.2、ZGC不能做指针压缩
指针压缩指的是压缩为32位,寻址位数不能超过35,也就是JVM内存最大为32G(2^35=32GB),这里的寻址位数已经达到了42位。
10.4.3、颜色指针的优势
- 在一个Region中的所有存活对象都被移走后(复制走后),这个Region就可以被立即释放掉,因为它还有转发表记录着原始地址和新地址,这样的话,理论上,只要还有一个Region对象空闲,ZGC就能完成垃圾收集。
- 颜色指针有指针的“自愈”(Self-Healing)能力,这样子就减少了写屏障(例如三色标记中的增量更新或原始快照),只需要一个读屏障就可以解决问题,减少了内存屏障的使用数量。
- 颜色指针有这极大的扩展性,因为还有18位未使用,这样更有利于后续功能的扩展。
10.5、如何选择垃圾收集器
JDK 1.8默认使用 Parallel(年轻代和老年代都是)
JDK 1.9默认使用 G1
- 优先调整堆的大小让服务器自己来选择。
- 如果内存小于100M,使用串行收集器。
- 如果是单核,并且没有停顿时间的要求,串行或JVM自己选择。
- 如果允许停顿时间超过1秒,选择并行或者JVM自己选。
- 如果响应时间最重要,并且不能超过1秒,使用并发收集器。
- 4G以下可以用parallel,4-8G可以用ParNew+CMS,8G以上可以用G1,几百G以上用ZGC。
10.6、安全点和安全区域
由来:首先我们系统在执行gc的时候并不是说刚需要执行gc就马上STW执行gc,因为我们java中有很多原子性的代码,例如i++这种内部可能有很多代码执行,而且我们执行完成这一行代码还需要更新程序计数器保存一下当前的行号,像一些核心的代码是需要等待执行完成的,但是又不能等待过长的时间,于是就产生了安全点和安全区域,当程序执行到这些地方时,用户线程会轮询去访问系统是否需要gc的那个标志,如果需要,在这个地方用户线程可以停止,当所有的用户线程都被挂起时,可促使系统触发gc。
注意:在我们的G1收集器中就有安全点。
10.6.1、安全点设置区域点
安全点不能每一行代码都设置,这样的话效率太低,也不能设置太少,使gc等待过长时间,一般安全点会设置在如下几个区域:
- 方法返回之前。
- 调用某个方法之后
- 抛出异常的位置
- 循环的末尾
10.6.2、安全点区域
安全区域是指在某一段代码时,引用关系不会发生变化,例如像我们执行Sleep休眠的时候,在这个区域中任意地方开始gc都是安全的。
参考书籍《深入理解Java虚拟机》