深入理解JVM(③)ZGC收集器
前言
ZGC是一款在JDK11中新加入的具有实验性质的低延迟垃圾收集器,目前仅支持Linux/x86-64。ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。
ZGC布局
与Shenandoah和G1一样,ZGC也采取基于Region的堆内存布局,但与他们不同的是,ZGC的Region具有动态性(动态的创建和销毁,以及动态的区域容量大小)。
ZGC的Region可以分为三类:
- 小型Region:容量固定为2MB,用于放置小于256KB的小对象。
- 中型Region:容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。
- 大型Region:容量不固定,可以动态变化,但必须为2MB的整数倍,用于存放4MB或以上的大对象。并且每个大型Region只会存放一个对象。
ZGC内存布局图:
染色指针
HotSpot的垃圾收集器,有几种不同的标记实现方案。
- 把标记直接记录在对象头上(Serial 收集器)。
- 把标记记录在于对象相互独立的数据结构上(G1、Shenandoah使用了一种相当于堆内存的1/64大小的,称为BitMap的结构来记录标记信息)。
- ZGC染色指针直接把标记信息记载引用对象的指针上。
染色指针是一种直接将少量额外的信息存储在指针上的技术。
目前Linux下64位指针的高18位不能用来寻址,但剩余的46位指针所能支持的64TB内存仍然鞥呢够充分满足大型服务器的需要。鉴于此,ZGC将其高4位提取出来存储四个标志信息。
通过这些标志虚拟机就可以直接从指针中看到器引用对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集(是否被移动过——Remapped)、是否只能通过finalize()方法才能被访问到(Finalizable)。由于这些标志位进一步压缩了原本只有46位的地址空寂,导致ZGC能够管理的内存不可以超过4TB。
染色指针示意图:
染色指针的优势
- 染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指令向该Region的引用都被修正后才能清理。
- 染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是在写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作。
- 染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。
内存多重映射
Linux/x86-64平台上ZGC使用了多重映射(Multi-Mapping)将多个不同的虚拟内存地址映射到同一物理内存地址上,这是一种多对一映射,意味着ZGC在虚拟内存中看到的地址空寂要比实际的堆内存容量来的更大。把染色指针中的标志位看作是地址的分段符,那只要将这些不同的地址段都映射到同一物理内裤空间,经过多重映射转换后,就可以使用染色指针正常进行寻址了。
多重映射下的寻址:
ZGC的运作过程
ZGC的运作过程大致可划分为以下四个大的阶段。四个阶段都是可以并发执行的,仅是两个阶段中间会存在短暂的停顿小阶段。
运作过程如下:
- 并发标记(Concurrent Mark): 与G1、Shenandoah一样,并发标记是遍历对象图做可达性分析的阶段,前后也要经过类似于G1、Shenandoah的初始标记、最终标记的短暂停顿,而且这些停顿阶段所做的事情在目标上也是相类似的。
- 并发预备重分配( Concurrent Prepare for Relocate): 这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。
- 并发重分配(Concurrent Relocate): 重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。
- 并发重映射(Concurrent Remap): 重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,ZGC的并发映射并不是以一个必须要“迫切”去完成的任务。ZGC很巧妙地把并发重映射阶段要做的工作,合并到下一次垃圾收集循环中的并发标记阶段里去完成,反正他们都是要遍历所有对象的,这样合并节省了一次遍历的开销。
ZGC的优劣势
- 缺点:浮动垃圾
当ZGC准备要对一个很大的堆做一次完整的并发收集,驾驶其全过程要持续十分钟以上,由于应用的对象分配速率很高,将创造大量的新对象,这些新对象很难进入当次收集的标记范围,通常就只能全部作为存活对象来看待(尽管其中绝大部分对象都是朝生夕灭),这就产生了大量的浮动垃圾。
目前唯一的办法就是尽可能地去增加堆容量大小,获取更多喘息的时间。但若要从根本上解决,还是需要引入分代收集,让新生对象都在一个专门的区域中创建,然后针对这个区域进行更频繁、更快的收集。 - 优点:高吞吐量、低延迟。
ZGC是支持“NUMA-Aware”的内存分配。MUMA(Non-Uniform Memory Access,非统一内存访问架构)是一种多处理器或多核处理器计算机所设计的内存架构。
现在多CPU插槽的服务器都是Numa架构,比如两颗CPU插槽(24核),64G内存的服务器,那其中一颗CPU上的12个核,访问从属于它的32G本地内存,要比访问另外32G远端内存要快得多。
ZGC默认支持NUMA架构,在创建对象时,根据当前线程在哪个CPU执行,优先在靠近这个CPU的内存进行分配,这样可以显著的提高性能,在SPEC JBB 2005 基准测试里获得40%的提升。
作者:纪莫
欢迎任何形式的转载,但请务必注明出处。
限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。
欢迎扫描二维码关注公众号:Jimoer
文章会同步到公众号上面,大家一起成长,共同提升技术能力。
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。
您的鼓励是博主的最大动力!