Java 11 诸多新特性中,最重要的可能就是引入一个新的GC回收器:ZGC(The Z Garbage Collector)。这个GC,是Oracle为了低延迟(暂停时间),大内存的场景而开发的一个新的垃圾回收器。java
在Java10 这几年通过几个版本的改进,调优已经接近极限。从时间上来看,Hotspot最近一次发布新的GC,是在2006年发布的G1。当时,AWS最大的实例只有1核CPU, 1.7G内存。而现在,AWS上已经能够买到128核,3904G内存的服务器实例了。 因此,ZGC的设计目标就是为了适用于这些场景:大内存低延迟(10ms之内)而且对应用的性能影响低(对应用吞吐量的影响在15%之内)。算法
之后也能够在这个基础之上,提供更多功能特性,如:多层heap结构(例如使用内存+固态磁盘组成一个混合堆区)服务器
在继续介绍以前,有必要说明几个GC中常见的基础概念。多线程
GC对于性能,须要作出一些取舍。 例如:并行GC能够利用多线程回收内存,但这也会带来更多CPU线程的开销(诸如上下文切换); 相似的,并发GC不会暂停应用的运行,可是这也会显著的须要更多的内存,同时因为GC线程和应用线程同时运行,这也增长了调度上的复杂度。并发
有了上述几个基本概念,接下来,再来看看ZGC是如何工做的。 为了达到ZGC的设计目标,其使用了Hotspot VM 的两个新特性:app
这个技术就是,在指针上存放一些信息。(Java中的引用)ide
在64位平台上(ZGC只支持64位),一个指针的寻址范围会很是大,这样,就能够利用指针的某些位,来表示一些状态位。 ZGC规定最大使用的堆,大小为4TB,这样的话,对于64位指针只须要使用42位就能够寻址4TB的空间,那么多余出来的22位就能够用于存放一些其余状态信息。 (指针长度一般是一个字(WORLD)的长度,而在64位平台,一个字长64位)性能
目前,使用了22位中的4位长度,分别用来表示:是否已经finalize,重映射(remap),mark0,mark1。测试
指针着色,也会带来一个问题。当解引用的时候,须要对其中的信息进行遮掩。(相似ip地址中的掩码) 这个工做,在某些平台是原生就支持的。如:SPARC; 而在x86平台上则不支持。 为了解决跨平台的兼容性,ZGC团队使用了multi-mapping技术来解决兼容问题。spa
要想理解multi-mapping,须要先来了解虚拟内存和物理内存的区别。
操做系统经过如下几个技术来管理和维护虚拟内存和物理内存的映射关系:
multi-mapping就是把不一样区域的虚拟内存映射到同一块物理内存的技术。 经过remap, mark0, mark1 三个状态字来控制映射过程。
图示以下:
// // Page Allocation Tiers // --------------------- // // Page Type Page Size Object Size Limit Object Alignment // ------------------------------------------------------------------ // Small 2M <= 265K <MinObjAlignmentInBytes> // Medium 32M <= 4M 4K // Large X*M > 4M 2M // ------------------------------------------------------------------ // // // Address Space & Pointer Layout // ------------------------------ // // +--------------------------------+ 0x00007FFFFFFFFFFF (127TB) // . . // . . // . . // +--------------------------------+ 0x0000140000000000 (20TB) // | Remapped View | // +--------------------------------+ 0x0000100000000000 (16TB) // | (Reserved, but unused) | // +--------------------------------+ 0x00000c0000000000 (12TB) // | Marked1 View | // +--------------------------------+ 0x0000080000000000 (8TB) // | Marked0 View | // +--------------------------------+ 0x0000040000000000 (4TB) // . . // +--------------------------------+ 0x0000000000000000 // // // 6 4 4 4 4 4 0 // 3 7 6 5 2 1 0 // +-------------------+-+----+-----------------------------------------------+ // |00000000 00000000 0|0|1111|11 11111111 11111111 11111111 11111111 11111111| // +-------------------+-+----+-----------------------------------------------+ // | | | | // | | | * 41-0 Object Offset (42-bits, 4TB address space) // | | | // | | * 45-42 Metadata Bits (4-bits) 0001 = Marked0 (Address view 4-8TB) // | | 0010 = Marked1 (Address view 8-12TB) // | | 0100 = Remapped (Address view 16-20TB) // | | 1000 = Finalizable (Address view N/A) // | | // | * 46-46 Unused (1-bit, always zero) // | // * 63-47 Fixed (17-bits, always zero) //
在G1回收器中,使用写屏障(write-barriers); 而ZGC中大量使用读屏障(Load barriers),相似于:
void printName( Person person ) { String name = person.name; // 读屏障,从堆中加载引用 System.out.println(name); // 不在使用读屏障,直接使用,此时的name不在堆中加载(局部变量) }
在ZGC进行GC的处理周期中,第一步就是标记。这一步,就是为了从堆中找出全部可到达的对象并在这些对象上打上标记。(也就是,找出全部不是垃圾的对象)
而ZGC的标记阶段,又分为三个步骤:
当标记阶段完成,接下来就是重定位阶段。重定位,就是把GC存活对象从新安放,从而释放堆区空间。
那么,为何要移动这些对象,而不是简单的直接填充而后直接释放呢?
这个阶段,ZGC会进行几个步骤:
ZGC在实际中的性能还不能彻底说就必定好,不过从这个版原本看,几回STW都是只对ROOT对象集合,而且这个集合大小和堆大小无关。在标记阶段最后一个STW,是某些条件下才出发的,而且是只须要处理增量数据,而不是全量。并且,并发执行的标记阶段也能够在超过预期中的时间,也能够返回,下一次再处理。
有关ZGC的性能,Stefan Karlsson 和 Per Liden 给出了一些数据,能够在youtube找到。经过SPECjbb 2015的数据,能够和Parallel GC大概有个比较,平均暂停时间基本在1~4ms。而G1 和 Parallel 则大体须要200ms级别。
不过,垃圾回收是一个十分复杂的工做。基准测试的数据不能彻底表明实际状况。因此,ZGC的性能还须要进一步在实际线上系统中验证。
指针着色和读屏障为之后提供了一些改进的可能:
随着闪存以及一些非易失存储(non-volatile memory)的发展,能够在JVM中构建一个多层的堆结构。 指针着色中目前只是用了4位,还有不少的空间能够存放一些元数据。这样,之后就有可能在读屏障时去其余存储中查找对象。
因为读取引用是能够经过读屏障来操做,那么理论上就能够在度屏障时,对数据进行压缩或者解压。
目前ZGC还处于一个实验性的阶段,还须要一段时间的验证。G1从发布到实际可用,也是通过了三年的时间。
服务器如今动辄上百G,甚至TB级别的内存,同时,Java对于堆的使用效率愈来愈重要。ZGC就是为了超大堆、低延迟的场景而设计的。其经过指针着色,以及读屏障等技术,使得ZGC为Hotspot的GC提供了一些可能。