系统学习请点击 jvm学习目录
ZGC总览
在《HotSpot Virtual Machine Garbage Collection Tuning Guide》中是这样介绍GC的:
Z 垃圾回收器 (ZGC) 是一个可扩展的低延迟垃圾回收器。ZGC 同时执行所有昂贵的工作,而不停止执行超过 10 毫秒的应用程序线程,这使得它适用于需要低延迟和/或使用非常大的堆(多 TB)的应用程序。
ZGC追求低延迟,无论引用与多大的堆内存,都志在将停顿时间限制在10ms以内,这与Shenandoah垃圾收集器很像,但是之前我们将Shenandoah时说到,这是要牺牲吞吐量的,同时10ms也不大能做到。而对于ZGC,它的性能表现相较Shenandoah有更优秀的表现,吞吐量甚至超过了G1,延迟也能降到10ms以内。
本博客的重点在于染色指针
本文介绍时,会将ZGC与Shenandoah进行对比,Shenandoah学习可以点击jvm学习 Shenandoah垃圾收集器(建议在学习ZGC之前先把CMS,G1和Shenandoah学习一遍,这样能更充分的理解)
ZGC特点
内存布局
首先来讲一下ZGC的内存布局。
ZGC的内存布局和Shenandoah很像,但又有区别。
ZGC同样采用Region,同样没有采用分代理论,这是相同点;不同点在于Shenandoah的Region是固定大小了,而ZGC的Region是动态的。
ZGC中的Region有着三种大小:
- 小型Region:2MB,存放小于256KB的小对象
- 中型Region:32MB,存放大于等于256KB且小于4MB的中型对象
- 大型Region:容量不固定,但为2MB的整数倍,存放有且只有一个大于或等于4MB的大型对象(大型Region不一定比中型Region大哦)
由于Region的大小是动态的,所以就肯定伴随着Region的创建和销毁。
具体会生成什么样的Region,那要取决于等待分配内存空间的对象的大小了。观察一下,发现所有的Region都是2MB,我们可以这样认为,ZGC将堆内部分成了一个个大小为2MB的单位,创建小对象时,就给一个单位给它;创建中对象时,就给16个连续的单位给它;要是大对象,就按照它的大小给一定数量的单位给它。
染色指针和转发表
由于要追求低延迟,那么在 将回收集中的复制到空白的Region中去,并回收沦为垃圾的Region 这一步骤中,要做到并发。那么就要考虑当引用尚未更新时,用户线程访问对象,是访问旧对象还是新对象呢?怎么访问到新对象呢?
在Shenandoah中我们说过,用Brooks Pointer来解决,相当于网络中的路由器桥接一样。使用Brooks Pointer能够很方便的解决这些问题,不过也带来了开销,首先是在对象头前面加了Brooks Pointer,使得每个对象占用的空间都变大,其次是访问对象时繁琐,每次都要先到Brooks Pointer,然后再到对象。
针对这些问题,ZGC采用不同的解决措施—染色指针(Colored Pointer)+转发表。
染色指针是基于这样的理论:当在进行GC的时候,我们根本不需要访问对象的具体情况(字段,方法什么的)对不对,我们只需要知道该对象的引用关系,所以我们根本不需要访问到对象。而在引用未更新之前,用户要访问新的对象,也不需要访问旧的对象。所以ZGC采用了一个很巧妙的方法,用引用指针来存储引用信息、标记信息。
对于linux 64位操作系统,其内存寻址是一个64位的二进制串,它可以支持2的64次方的内存,而linux下高18位是不能用的,所以就是2的46次方,也就是支持64TB的内存,这个支持的内存似乎有点过于大了,所以ZGC的开发这就灵机一动,能不能将其中几位来作为标志信息呢,反正支持内存小点也没事。于是,染色指针便出现了。如下图所示(图来自于Per Liden的一次演讲)
将可用的46位拿出高4位来表示信息(剩下42位依旧是内存地址,4TB)。
第一个是Finalizable,表示是否对象被判断为死亡,只能通过finalize()方法才能访问到并拯救它;
第二个是Remapped,表示本对象是否被移动过,意思是:在垃圾回收过程中,本对象存活,将本对象复制到空白的Region中,但引用尚未更新,此时要将Remapped打上标记,说明我已经被复制过了,我是旧对象了,别访问我,访问新对象去吧!
第三个和第四个是Marked1和Marked0,这俩就是用于并发可达性分析的三色标记的,具体可以看JVM学习 并发可达性分析详解。
并发重分配阶段,当将存活的对象复制到空白的Region中,但引用尚未更新,此时要将Remapped打上标记,然后呢,然后怎么访问新对象呢,染色指针上只是一个标记而已对吧,并没有说新对象在哪里呀!别急,慢慢道来。
ZGC为每个Region都建立了一个转发表,当发现该对象被复制过了,就不访问旧对象,而是去查转发表,在转发表中找到新对象的内存地址。从而去访问新对象。同时,此时就将引用更新(这也叫自愈,自己把引用更新)。
通过染色指针+转发表就可以解决并发复制过程中用户线程访问对象的问题。相比于Shenandoah的Brooks指针,其好处显而易见,节省了开销,同时访问对象也不繁琐,一次治愈,终身便利。同时,在对象被复制,染色指针的Remapped被标记之后,即使引用尚未更新,也可以马上将Region回收,因为从上面我们可知,用户访问对象,根本不用关心旧对象。不过,转发表一定要保存,等到该Region的引用全部更新之后才能删除,否则就找不到新对象了。
不过缺点也是显而易见的,染色指针导致ZGC支持的内存大小变成了1/16,这在以后内存很大的情况下会很麻烦,现在嘛,4TB已经很够用了,况且,linux还有高18位还没用呢。
运行过程
主要分为四个大的阶段,四个阶段都是可以并发执行的。
- 并发标记:这里与G1和Shenandoah是差不多的,做并发可达性分析,经历初始标记,重新标记等等,该暂停的得暂停,不同的只是进行三色标记是在染色指针上而不是对象上。
- 并发预备重分配:本阶段根据特定的查询条件统计出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation
Set)。在该阶段,每次回收都会扫描所有的Region,用更大范围的扫描成本换取省去为了解决跨Region引用的记忆集维护成本。 - 并发重分配:ZGC的核心阶段,将重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表,记录从旧对象到新对象的转向关系。由于染色指针的Remapped被置位,当用户线程并发访问了位于重分配集中的对象,此次访问被预置的内存屏障截获,然后根据Region上的转发表将访问转发到新复制的对象上,并同时修正更新该引用。
- 并发重映射:修正整个堆中所有旧对象的所有引用。这一阶段不是很迫切去完成,因为ZGC有自愈的能力,并且ZGC巧妙的将该阶段融合到了下一次垃圾回收运行过程的并发标记阶段,因为反正它们都是要遍历所有对象的。
总结
ZGC是一个追求无论堆内存大小都要10ms延迟的一款垃圾收集器,其采用了动态的内存分布和染色指针+转发表的骚操作。可以说它非常棒。
它可以追平G1的吞吐量,同时达到10ms停顿的目标。不过暂时还是在实验中,为了更完美。
由于核心在于染色指针+转发表,它也具有一定的确定,使得支持内存变小。但这其实也不是问题。
其每次需要扫描所有Region,虽然解决了跨Region引用,但也更慢了。
ZGC重点就在于染色指针,本篇博客的重点也放在了那里,进行了详细讲解,运行过程部分更多的借鉴了《深入理解JVM》
参考
- 深入理解JVM 周志明