随着Java虚拟机技术的不断发展,垃圾收集器也进行了大量迭代。前面介绍了7种经典的垃圾收集器,各有优缺点。Serial GC虽然是最古老的垃圾收集器,但由于设计简单,未必就是过时的收集器。CMS收集器由于自身有些算法缺陷,在JDK9中被标记为废弃,在最新版的JDK14中,CMS已经被彻底移除,进入了历史垃圾堆。我们可以发现G1收集器在JDK的各个版本中仍然在不断改进,并且成为了JDK9到JDK13的默认垃圾收集器。今天,我们要介绍的三种垃圾收集器算是垃圾收集器的前沿成果。
Epsilon收集器
我们看一下JEP 318中对于Epsilon收集器的介绍:
Develop a GC that handles memory allocation but does not implement any actual memory reclamation mechanism. Once the available Java heap is exhausted, the JVM will shut down.
好吧,翻译成中文就是说这款垃圾收集器可以分配内存但是不实现垃圾回收机制。一旦可用的Java堆内存耗尽,JVM就会关闭。
纳尼?垃圾收集器没有垃圾收集功能叫什么垃圾收集器?实际上Epsilon收集器提供有限分配限制和尽可能低的延迟开销的消极GC实现,尽管会以占用一定内存和牺牲一定内存吞吐量为代价。如果只是需要运行几分钟甚至几秒的小应用,那么JVM负责给对象分配内存,程序在堆内存消耗殆尽之前退出,这种场景下Epsilon收集器是很不错的选择。
Shenandoah收集器
Shenandoah收集器算是最特别的一款垃圾收集器,它是由Red Hat研发的低延迟垃圾收集器,之所以说它特别,是因为研发团队将它引入OpenJDK 12的时候,Oracle官方却不愿意将它加入HotSpot中,导致了开源版比商用版功能更多的尴尬。
Red Hat团队设计这款收集器的最初目的是为了将垃圾收集的延迟降低到10毫秒以内,不管是200MB还是200GB的垃圾都要达到这样的目标,但Shenandoah开发团队的实际测试似乎并没有达到最初的目标,尽管停顿时间确实是大大降低了。停顿时间的降低必然会造成别的参数性能下降,在吞吐量方面,Shenandoah收集器比起G1、CMS等收集器还是有一定差距的。
工作过程
Shenandoah收集器非常像是G1的改进版本,它的工作过程大致可以分为以下阶段:
-
初始标记。标记和GC Roots直接关联的对象,这个过程需要STW(Stop the world),该过程停顿时间只与GC Roots的数量相关。
-
并发标记。与用户线程并发执行,遍历对象图,标记所有可达对象。
-
最终标记。同样需要STW,并扫描剩余STAB,统计最有回收价值的Region,并将这些Region组成回收集。
-
并发清理。清理没有存活对象的Region。
-
并发回收。将回收集中的存活对象复制到空白Region中,并且这个阶段是与用户线程并发执行的,用户线程有可能继续访问原来的内存地址,所以利用读屏障和Brooks Pointers转发指针。
-
初始引用更新。需要STW,建立线程集合点,确保并发回收阶段的存活对象已经移到其他Region中。
-
并发引用更新。按物理内存顺序,线性搜索引用类型,将旧值改成新值。
-
最终引用更新。短暂的STW,修正GC Roots的引用。
-
并发清理。将回收集中的内存全部回收。
Brooks Pointers
现在解释一下并发回收里的Brooks Pointers,我们知道,在并发回收的过程中可能有用户程序继续访问旧地址,但是访问旧地址势必会出错,因此提出了转发指针的概念(其实Brooks是个人名)。
转发指针是设置在原有对象前的一个引用字段,正常情况下指向对象自身。并发回收阶段把旧对象的转发指针指向新对象的地址。
Shenandoah相比G1的改进
Shenandoah收集器其实可以看做是G1的继承版本,这两款收集器的工作步骤很多都是一样的,甚至还共享了代码。但是Shenandoah收集器其实还是有很多改进的地方的,以下介绍一下改进之处:
-
Shenandoah收集器支持并发整理而G1回收内存不能与用户线程并发执行。
-
Shenandoah收集器默认不使用分代收集。
-
Shenandoah收集器不再使用记忆集记录跨Region的引用而是改用连接矩阵。
简单介绍一下连接矩阵,连接矩阵可理解为一个二维表格,如果Region M指向Region N则在表格的M行N列做一个标记。
ZGC收集器
和Shenandoah一样,ZGC也希望能将延迟时间压缩到10毫秒以内。它是JDK11引入的实验性质的低延迟收集器,这可是根红苗正的Oracle亲儿子。
内存布局
ZGC是暂时不使用分代机制的,它的内存布局主要有小型Region、中型Region和大型Region。
-
小型Region:容量为2MB,存放小于256KB的对象。
-
中型Region:容量为32MB,存放小于4MB但大于256KB的对象。
-
大型Region:容量不固定,但必须是2MB的整数倍,用于存放4MB及以上大小的对象,也就是说最小容量只有4MB,比中型Region小,但是大型Region只能存放一个对象。
染色指针
染色指针是ZGC与其他垃圾收集器的区别之一,它将少量对象信息直接存储在指针上。以64位Linux为例,它的高18位不能用来寻址,剩下的46位中4位是标志位,用来表示引用对象的状态,剩下42位用来寻址,最大支持4TB内存。
上面的四位标志位中Marked0和Marked1表示引用对象的三色标记状态,Remapped表示是否被移动过,Finalizable表示是否只能通过finalize()方法访问到。染色指针指向的存活对象被移走后,对象所在的Region立马就可以被释放。
工作过程
-
并发标记。并发标记前面已经提过很多次了,遍历对象图,需要STW,但是ZGC的标记是标记在染色指针上的。也就是染色指针的Marked0和Marked1两个标志位,用来标识对象状态。
-
并发预备重分配。ZGC将所有要收集的Region组成重分配集,ZGC会扫描所有的Region来换维护记忆集的成本。
-
并发重分配。该过程将重分配集中的存活对象复制到其他Region中,并为重分配集维护一个转发表记录旧对象到新对象的迁移。从染色指针的状态位可以知道对象是否还在重分配集中,如果在的话,会被内存屏障拦截并转发到新对象,然后更新引用使其指向新对象。
-
并发重映射。该过程所做的工作就是修正所有的Region中指向重分配集的引用,但这并不是紧迫的任务,因为大不了就是第一次使用多一次转发。一旦所有引用都被修正,原来的转发表就可以释放了。
NUMA
ZGC支持NUMA的内存架构,NUMA的中文是非统一内存访问,介绍它之前首先介绍一下UMA(统一内存访问)。
在以前的计算机发展中,这样的总线模型保证了所有CPU对内存的访问地址还是一致的。但随着CPU核心数的增加,这样的架构显然无法适应现状。于是发展出了NUMA内存架构。在NUMA架构中,CPU和内存被划分成多个不同的节点,不同节点之间可以通过QPI(Quick Path Interconnect)进行访问。ZGC会优先在当前线程所属的CPU的本地内存中进行访问,这样可以大大加快CPU访问内存的速度。
写在最后的话
去年我的一个学长分享了某互联网公司内部员工培训的JVM课件,已经将ZGC加入到培训内容中。可以预见的是,ZGC由于是Oracle官方推出的根红苗正的垃圾收集器,有很大概率会出现在今后甚至是今年即将到来的秋招面试大礼包中。