目录
学习前言
一、低延迟垃圾收集器
1. Shenandoah收集器
二、ZGC
1. 内存布局
2. 更巧妙的并发整理
三、其他垃圾收集器
学习前言
除了之前我们所学习的经典垃圾收集器除外,我们还有一些低延迟垃圾收集等!
之前所学经典垃圾收集器,请参考本篇文章:垃圾收集器与内存分配机制(二)
一、低延迟垃圾收集器
衡量一款垃圾收集器有三项最重要的指标:
- 内存占用(Footprint)
- 吞吐量(throughput)
- 延迟(Latency)
这三者大约是一个不可能三角,一款优秀的收集器通常可以在其中一到两项上做到很好。而随着硬件的发展,这
三者中,延迟的重要性越来越凸显出来。原因有三:一是随着内存越来越大还越来越便宜,我们越来越能容忍收
集器占用多一点的内存;二是硬件性能的增长,比如更快的CPU处理速度,这对软件系统的处理能力是有直接提
升的,它有助于降低收集器运行时对应用程序的影响,换句话说,JVM吞吐量会更高;三,与前两者相反,有些
硬件提升,特别是内存容量的提升,并不会直接降低延迟,相反,它会带来负面效果,更多的内存回收必然使得
回收耗时更长。
因此,在当下,垃圾收集器的主要目标就是低延迟。
对于HotSpot而言,目前有两款转正不久的低延迟垃圾收集器:
Shenandoah与ZGC。它们在低延迟方面与之前梳理过得经典垃圾收集器比较如下:
从上图可以看出垃圾回收器的发展趋势:
- 尽量增加并发,以减少STW。G1目标是200ms,但实际只能做到四五百ms上下,Shenandoah目前可以做到几十ms以内,ZGC就牛逼了,10ms以内。
- 尽量减少空间碎片,以保证吞吐量。少用标记清除算法,事实上除了CMS也没谁用。
除了Parallel,从CMS到G1,再到Shenandoah与ZGC,都是在想办法并发地完成标记与回收,以达到降低延迟的
目的。
同时为了尽可能保证吞吐量,在回收阶段也尽量使用整理算法而不是清除算法。
继G1之后,Shenandoah与ZGC的目标就是,在尽可能对吞吐量影响不太大的前提下,实现任意堆内存大小下都
可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。即:大内存,低延迟。
Shenandoah与ZGC在Java15中都已经成为正式特性(但默认GC还是G1),下面简单梳理一下Shenandoah与ZGC
的特点,它们的关键都在于,如何实现并发整理。
1. Shenandoah收集器
Shenandoah收集器在技术上可以认为是G1的下一代继承者。但它不是由Oracle主推发展的收集器,它是由
RedHat公司主推的,属于OpenJDK的特性,而非OralceJDK的特性。它在OpenJDK12中引入成为实验特性,在
OpenJDK15中成为正式特性。
Shenandoah与G1一样,使用基于Region的堆内存布局,有用于巨型对象存储的 Humongous Region ,有基于
回收价值的回收策略,在初始标记、并发标记等阶段的处理思路上高度一致,甚至直接共享了一部分实现代码。
这使得G1的一些改善会同时反映到Shenandoah上,Shenandoah的一些新特性也会出现在G1中。例如G1的收集
担保 Full GC ,以前是单线程的 MSC,就是由于合并了Shenandoah的代码,才变为并行的多线程 MSC 。
Shenandoah相比G1,主要有以下改进:
- 回收阶段支持并发整理算法;
- 不支持分代收集理论,不再将Region区分为新生代和老年代;
- 不使用RSet记忆集,改为全局的连接矩阵,连接矩阵就是一个二维表,RegionN有对象引用ReginM的对
象,就在二维表的N行M列上打钩。
在大的流程上,因为不再基于分代收集理论,Shenandoah并没有所谓YoungGC和OldGC或MixedGC。
它的主要流程类似G1的并发标记周期和混合收集周期:
- 初始标记:标记与GCRoots直接关联的对象,短暂STW。
- 并发标记:与G1类似,并发执行,标记可达对象。
- 重新标记:使用SATB原始快照算法重新标记,并统计出各个Region的回收价值,将最高的Regions组成一个
CSet,短暂STW。
- 并发清理:将没有任何存活对象的Region直接清空。
- 并发回收:从这里开始,就是Shenandoah和G1的关键差异,Shenandoah先把回收集里面的存活对象先复
制一份到其他未被使用的Region中,并利用转发指针、CAS与读写屏障等技术来保证并发运行的用户线程能
同时保持对移动对象的访问(※1)。
- 初始引用更新:并发回收阶段复制对象结束后,需要把堆中所有指向旧对象的引用修正到复制后的新地址,
这个操作称为引用更新。初始引用更新这个阶段实际上并未做具体的更新处理,而是建立一个线程集合的时
间点,确保所有并发回收阶段中的GC线程都已完成分配给它们的对象复制任务而已。初始引用更新时间很短,
会产生一个非常短暂的STW。
- 并发引用更新:开始并发执行引用更新,找到对象引用链中的引用所在,将旧的引用地址改为新的引用地
址。
- 最终引用更新:更新GCRoots中的引用。
- 并发清理:回收CSet中的Regions。
※1 实现并发整理的关键技术:转发指针、CAS与读写屏障
转发指针,Brooks Pointer,Brooks是一个人的名字,转发指针技术由其提出。该技术在原有对象布局结构的最
前面统一增加一个新的引用字段,在正常不处于并发移动的情况下,该引用指向对象自己;当对象被复制,有了一份
新的副本时,只需要修改旧对象上转发指针的引用位置,使其指向新对象,便可将所有对该对象的访问转发到新
的副本上。这样只要旧对象的内存仍然存在,未被清理掉,虚拟机内存中所有通过旧引用地址访问的代码便仍然
可用,都会被自动转发到新对象上继续工作。而当引用地址全部被更新之后,旧对象就不会再被访问到,转发指
针不再由永无之地,随着旧对象一起被释放。
当然,使用转发指针会有线程安全问题。
比如GC线程复制对象和用户线程对旧对象执行写操作这两个动作如果同时发生,就有可能出现线程安全问题。
Shenandoah在这里是采用Compare And SwapCAS技术来保证线程安全的。
CAS是一种乐观锁,比较并替换。
另外,在对象被访问时触发转发指针动作,需要使用读写屏障技术,在对象的各种读写操作(包括读,写,比较,
hash,加锁等等)上做一个拦截,类似AOP。
注意,这里的读写屏障并不是JMM中的内存屏障。
内存屏障类似同步锁,是多线程环境下工作线程与主存之间保证共享变量的线程安全的技术。
事实上,在之前介绍的其他收集器中,已经利用到了写屏障技术,比如CMS与G1中的并发标记等。
Shenandoah不仅要用写屏障,还要用读屏障,这是它之前的性能瓶颈之一,但在Java13中得到了改善,改为使
用Load Reference Barrier,引用访问屏障,只拦截对象中数据类型为引用类型的读写操作,而不去管原生数据
类型等其他非引用字段的读写,这能够省去大量对原生类型、对象比较、对象加锁等场景中设置屏障所带来的消
耗。
目前,Shenandoah在停顿时间上与G1等经典收集器相比有了质的飞跃,已经能够做到几十毫秒;
但一方面还没有达到预期的10ms以内,另一方面却引起了吞吐量的明显下降,尤其是和Parallel Scavenge相
比。
二、ZGC
Z Garbage Collector,简称ZGC。ZGC是Oracle主推的下一代低延迟垃圾收集器,它在Java11中加入JVM作为实
验特性,在Java15中成为正式特性。目前还不是默认GC,但很有可能成为继G1之后的HotSpot默认垃圾回收器。
ZGC虽然目标和Shenandoah一样,都是把停顿时间控制在10ms以内,但它们的思路却完全不一样。就目前而
言,ZGC基本已经做到了,而Shenandoah还需要继续改善。
ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术
来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。其主要特点如下:
- 动态的Region布局
- 回收阶段支持更巧妙的并发整理
1. 内存布局
ZGC的Region是动态创建和销毁的,且大小不是全部相同的。
在X64平台下,ZGC的Region有大、中、小三个容量:
- 小型Region,容量固定为2M,用于存放小于256K的小对象。
- 中型Region,容量固定为32M,用于存放[256K~4M)的对象。
- 大型Region,容量不固定,但必须是2M的整数倍,用于存放4M及以上的大对象。每个大型Region只会存放
一个大对象,这意味着会有小于中型Region的大型Region,比如最小的大型Region只有4M,就比32M的中
型Region小。大型Region在ZGC中是不会被移动的。
2. 更巧妙的并发整理
ZGC的运行阶段大致如下(初始标记什么的就不写了):
- 并发标记:对所有Regions做并发的可达性分析,但标记的结果记在引用地址的固定位置上。这个技术叫染色
指针技术(※1)。
- 并发预备重分配:根据标记结果将所有存活对象所属的Region计入重分配集Relocation Set。
- 并发重分配:并发地将重分配集中的存活对象复制到空闲Region中,并未重分配集中的每一个Region维护一
个转发表Forward Table,记录旧对象到新对象的转发关系。由于染色指针技术的使用,ZGC仅仅从引用上就
能获知该对象是否在重分配集中(存活对象),通过预置好的读写屏障,如果用户线程并发访问了该对象,就会
被截获并根据转发表转发到复制的新对象上,同时自动修正引用地址到新对象地址,这种自动修改引用地址
的行为被称为自愈。ZGC这种设计的好处是,转发只会发生一次,并发期间用户线程再次访问该对象就没有
转发的损耗了,不像Shenandoah的转发指针技术,在并发期间用户线程每次访问该对象都要转发一下。另外
的好处是,由于染色指针技术,重分配集中的存活对象只要被复制完,就可以立即清空这个Region,只要保
留其对应转发表即可。
- 并发重映射:修正整个堆中对重分配集中旧对象的所有引用。这个阶段并不是一个迫切要完成的阶段,因为
在ZGC的设立里,引用可以"自愈"。但这么做还是有好处:释放转发表。因此ZGC选择将并发重映射这一步骤
放到下一次GC的并发标记阶段去完成,这样省下了一次遍历引用链的过程。
※1 染色指针技术
染色指针技术Colored Pointer是ZGC的标志性设计。一般收集器在可达性分析标记对象的三色状态时,都是标记
在对象或与对象相关的数据结构上。而染色指针技术是将三色状态直接标记到引用这个"指针",或者说内存地址
上。以64位的linux为例,它支持的内存地址空间去掉保留的高位18位不能使用,还剩下46位。ZGC将这剩下的46
位中的高4位拿出来存储4个标志信息,包括三色状态,对象是否已经被复制,是否只能通过finalize方法访问。
染色指针技术要直接修改操作系统的内存地址,这是需要操作系统和CPU的支持的。
在x86-64平台上,就需要利用到虚拟内存多重映射技术了。
染色指针技术带来的收益:
- 一旦重分配集中的某个Region的存活对象全部复制结束后,该Region能够立即清空,马上就可以拿来分配新
对象。这使得ZGC可以在空闲Region极少的极端情况下依然保证能够完成回收。
- 大幅减少在对象上设置读写屏障导致的性能损耗。因为可以直接从指针读到三色标记,是否已被复制等信
息。这使得GC对用户线程的性能影响减低,即,减少了ZGC对吞吐量的影响。
- 染色指针是一种可以扩展的技术,比如现在不能使用的高位18位,如果开发了这18位,ZGC就不必侵占目前
的46位,从而扩大支持的堆内存容量,也可以记录一些其他的标志信息。
染色指针技术的劣势:
- 不支持32位操作系统,因为没有地址空间不够。
- 由于把内存地址的高4位拿来做染色指针的存储了,所以导致能够管理的内存不能超过2的42次幂,即4TB。
目前来说,倒是完全够用。大内存的Java应用有上百G就不得了了。与这点限制相比,它能带来的收益要大得
多。
目前ZGC的优势还是很明显的,停顿时间方面,已经做到了10ms以内,而在"弱势"的吞吐量方面,居然也已经基
本追平以吞吐量为目标的Parallel Scavenge。基本上可以认为是完全超越G1的。但ZGC也有需要权衡的地方,
ZGC没有分代,不能针对新生代那种"朝生夕灭"的对象做针对性的优化,这导致ZGC能够承受的内存分配速度不会
太高。即,在一个会连续的高速的大量的分配内存的场景下,ZGC每次收集周期都会产生大量浮动垃圾,当回收
速度跟不上浮动垃圾产生的速度时,堆中的剩余空间会越来越少,最后可能导致收集失败。
目前只能通过加大堆内存的方式缓解这个问题。
其实从CMS到G1,以及Shenandoah都有浮动垃圾的问题。但前两者的分代设计基本保证浮动垃圾不会太多,
Shenandoah其实也有类似 YoungGC 的阶段设计去处理大量的新生对象。
三、其他垃圾收集器
除了以上梳理的7种经典垃圾收集器和两种低延迟垃圾收集器,还有一些其他的GC:
- Java11增加了一个叫 Epsilon 的收集器。它的特点就是对内存的管理是只分配,不回收。用途之一是用于需要剥离垃圾收集器影响的性能与压力测试;另外的用途是那些运行负载极小不需要任何回收的小应用,比如Function服务,几秒就执行完的脚本,特点就是跑完就关闭JVM。
- Azul的 Pauseless GC,简称 PGC,和 Concurrent Continuously Compacting Collector ,简称 C4。它们大约相当于ZGC的同胞前辈,但它们都是商用VM的GC,早就做到了标记和整理的全程并发。PGC运行在 Azul VM 上,C4运行在 Zing VM 上,且C4支持分代。从技术上讲,PGC、C4、ZGC一脉相承。目前ZGC相当于PGC,由于技术复杂性的原因,还没有支持分代。但未来应该也会考虑做到C4那样支持分代。
- OpenJDK现在除了HotSpot虚拟机,还支持OpenJ9虚拟机,它有自己的垃圾收集器如 Scavenger,Concurrent Mark,Incremental Generational 等等,不了解。