The Z Garbage Collector (ZGC) 【2】

Java 11 诸多新特性中,最重要的可能就是引入一个新的GC回收器:ZGC(The Z Garbage Collector)。这个GC,是Oracle为了低延迟(暂停时间),大内存的场景而开发的一个新的垃圾回收器。

为啥需要这个新回收器

在Java10 这几年经过几个版本的改进,调优已经接近极限。从时间上来看,Hotspot最近一次发布新的GC,是在2006年发布的G1。当时,AWS最大的实例只有1核CPU, 1.7G内存。而如今,AWS上已经可以买到128核,3904G内存的服务器实例了。 所以,ZGC的设计目标就是为了适用于这些场景:大内存低延迟(10ms以内)并且对应用的性能影响低(对应用吞吐量的影响在15%以内)

以后也可以在这个基础之上,提供更多功能特性,如:多层heap结构(例如使用内存+固态磁盘组成一个混合堆区)

几个概念

在继续介绍之前,有必要说明几个GC中常见的基础概念。

  • 并行(Parallel)
    • 有多个GC线程
    • 应用线程执行,不确定
  • 串行(Serial)
    • 只有单个GC线程
    • 应用线程执行,不确定
  • 暂停时间(Stop The World)
    • 停止应用线程的执行
    • 期间执行GC
  • 并发(Concurrent)
    • GC线程和应用线程同时都在运行
    • 也即是:内存分配器和垃圾回收器同时处于工作状态
  • 增量(Incremental)
    • 也即是由于某个条件触发的,额外(更高级别)的一些GC过程

取舍

GC对于性能,需要做出一些取舍。 例如:并行GC可以利用多线程回收内存,但这也会带来更多CPU线程的开销(诸如上下文切换); 类似的,并发GC不会暂停应用的运行,但是这也会显著的需要更多的内存,同时由于GC线程和应用线程同时运行,这也增加了调度上的复杂度。

ZGC

有了上述几个基本概念,接下来,再来看看ZGC是如何工作的。 为了达到ZGC的设计目标,其使用了Hotspot VM 的两个新特性:

  • 指针着色
  • 读屏障

指针着色

这个技术就是,在指针上存放一些信息。(Java中的引用)

在64位平台上(ZGC只支持64位),一个指针的寻址范围会非常大,这样,就可以利用指针的某些位,来表示一些状态位。 ZGC规定最大使用的堆,大小为4TB,这样的话,对于64位指针只需要使用42位就可以寻址4TB的空间,那么多余出来的22位就可以用于存放一些其他状态信息。 (指针长度通常是一个字(WORLD)的长度,而在64位平台,一个字长64位)

目前,使用了22位中的4位长度,分别用来表示:是否已经finalize,重映射(remap),mark0,mark1。

指针着色,也会带来一个问题。当解引用的时候,需要对其中的信息进行遮掩。(类似ip地址中的掩码) 这个工作,在某些平台是原生就支持的。如:SPARC; 而在x86平台上则不支持。 为了解决跨平台的兼容性,ZGC团队使用了multi-mapping技术来解决兼容问题。

multi-mapping

要想理解multi-mapping,需要先来了解虚拟内存和物理内存的区别。

  • 物理内存
    • 真实的ram存储介质
  • 虚拟内存
    • 抽象:在应用看来自己能够使用的内存

操作系统通过以下几个技术来管理和维护虚拟内存和物理内存的映射关系:

  • 页表(page table)
  • 进程内存管理单元(Memory Management Unit (MMU) )
  • 页表缓冲(Translation Lookaside Buffer (TLB) )
    • 对应用中的内存地址进行转换

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就是利用这个动作,在引用返回之前去检查着色指针的几个状态位
      • 如果检查通过(状态符合要求),则引用正常使用
      • 如果检查不通过,那么就会在应用返回之前,ZGC就会根据不同状态来执行一些额外动作

标记

在ZGC进行GC的处理周期中,第一步就是标记。这一步,就是为了从堆中找出所有可到达的对象并在这些对象上打上标记。(也就是,找出所有不是垃圾的对象)

而ZGC的标记阶段,又分为三个步骤:

1. STW(Stop The World),将ROOT对象标记为存活

image

  • ROOT就类似于局部变量,从这些对象上,可以引用堆中的对象
  • 如果某个对象在对象引用图中不可达,也就是从任何ROOT都无法引用到,那么就认为这个对象是一个垃圾
  • 而在引用图中可到达的对象集合,则被认为是存活对象集
  • 这个阶段只是找出ROOT对象,并标记这些ROOT对象存活
    • 所以很快,STW时间很短
2. 并发扫描对象引用图,标记所有可达对象
  • 在这个步骤中,读屏障检查所有引用
    • 通过掩码获取引用中的状态信息,判断这个引用是否已经标记过
      • 如果还未标记,则放入标记队列中
3. STW,处理一些状态以及某些边界值处理
  • 这一阶段完成后,整个标记阶段完成

重定位

标记阶段完成,接下来就是重定位阶段。重定位,就是把GC存活对象重新安放,从而释放堆区空间。

那么,为什么要移动这些对象,而不是简单的直接填充然后直接释放呢?

  • 有些GC算法就是如此,直接释放
  • 但这样会导致,接下来再次申请内存的时候,就要花费更大的代价
    • 内存碎片

这个阶段,ZGC会进行几个步骤:

  1. 找出需要重定位的内存页
    • ZGC将整个堆分为几个页来管理,在重定位这个阶段开始时:
      • 并发的找出几个需要重定位的内存页
  2. 当重定位的页已经找好了,那么就会触发一次STW暂停
    • 从ROOT对象开始,对所有对象进行重定位
    • 对这些对象的引用,重新映射到新的地址
    • 类似上一个阶段,这一次STW也只是对ROOT对象进行
      • 这个对象集很小,所以STW时间很短
      • 而且这个集合大小和堆大小无关,所以基本来说这一次的STW是一个可预估的时长
  3. 当ROOT对象移动完成之后,接下来会并发执行重定位
    • 这个步骤,会把内存页中所有对象进行重定位并进行移动
    • 这个过程不影响应用使用对象引用
      • 利用读屏障,具体如下图: image
      • 通过这个机制保障了应用中所有对象引用的可见性,并且也保障了重定位操作可以于应用并发执行。
  4. 由于对象引用发生迁移,重定位到了新的位置,而为了保障其他对象还能通过老的引用能够访问对象,就需要将引用的映射关系保存起来(REMAP)
    • 而这个REMAP,则会在下一次标记阶段清理,从而不会让这个REMAP膨胀过大

小结

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提供了一些可能。

原文: Java's new Z Garbage Collector (ZGC) is very exciting

转载于:https://my.oschina.net/roccn/blog/2253435

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值