JVM ZGC

JVM ZGC

小龙 813 2020-05-15

一、概述

Java之中的GC算法,经过了长期的发展之后,随着硬件水平的提升,实际上所有的算法都会受到活多或少的限制,尤其是Java所提供的G1收集器已经过去了多年了,当年服务器的硬件普及程度和当前的是没办法比较的,所以对于G1收集器也出现了所谓的性能瓶颈(内存已经上T了,CPU已经是多颗了),那么在这种的情况下就需要有一种新型的GC算法开提升服务器的性能,所有在JDK1.11(2018年9月25日正式发布)正式推出了江湖上传闻已久的ZGC垃圾收集器。

ZGC是由Oracle为OpenJDK开源的全新的垃圾收集器。它是由Per Liden编写的,借鉴了AzulC4垃圾收集器,专注于减少暂停时间的同时任然压缩堆(执行速度历史最快)

G1算法通过回收部分的Region,避免了全堆扫描,改善了大堆下的停顿时间,但在普通大小的堆里却表现平平
ZGC启动参数:-XX:+UseZGC

二、ZGC简介

ZGC全称Z Garbage Collector,是一款可伸缩(scalable)、低延迟(low latency garbage)、并发(concurrent)垃圾回收器,ZGC是一款基于Region内存布局的、不分代的设计,使用了读屏障、染色指针和内存多重映射等技术实现可并发标记-整理算法,旨在实现以下几个目标:

  • 停顿时间不超过10ms(不管现在操作的内存有多么的庞大,都维持在10ms以内的收集)
  • 停顿时间不随heap(堆内存)大小或存活对象大小增大而增大(垃圾随便多,停顿时间都在10ms内)
  • 可以处理从兆(M)几T大小的内存空间(完全符合当前以及未来的硬件发展水平)

ZGC1.png

2.1 NUMA架构支持

  • ZGC默认支持NUMA架构,在创建对象时,根据当前线程在那个CPU执行,优先在靠近这个CPU的内存进行分配,这样可以显著提高性能(基础测试提升40%)

ZGC2.png

2.2 ZGC回收算法处理机制

ZGC采用的是并行的处理机制,ZGC只有短暂的STW(Stop It World),大部分的过程都是和应用线程并发执行,最耗时的是并发标记和并发移动过程。

  • Marking(标记)
  • Relocation(移动)/Compaction(压缩)
  • Relocation Set Selection(移动选定集合)
  • Reference Processing(引用处理)
  • JNI WeakRefs Cleaning(JNI弱引用清除)
    |- StringTable/SymbolTable Cleaning
    |- Class Unloading
    整体的ZGC处理的时候考虑到内存分配的性能问题,所以所有清除后的垃圾空间,都需要进行散碎内存区域的移动,这样就可以实现内存区域的压缩。释放出完整的内存空间,有了完整的呢女空间,才能提升内存的处理性能。

2.3 ZGC的堆内存布局

  • 与G1一样,ZGC也采用基于Region的堆内存布局
  • ZGC的Region具有动态性。
  • 动态创建和销毁
  • 动态的区域容量大小

2.3.1 Region-Based

  • ZGC中没有新生代和老年代的概念,只有一块内存区域page,以page为单位进行对象的分配和回收。
  • ZGC-Page大小:2MB、32MB、N x 2MB;
    |- 动态地创建和销毁Region
    |- 动态的决定Region的大小
  • 存储处理:
    |- 小型Region(Small Region):固定容量为2MB,用于放置小于256KB的对象
    |- 中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但是小于4MB的对象。
    |- 大型Region(Large Region):容量不固定,可以动态变化,但是必须为2MB的整数倍数,用于放置4MB或以上的大对象。每个大型Region中只会存放一个大对象所以实际容量可能小于中型Region,最小容量可低至4MB。大型Region在ZGC的实现中是不会被重分配的,因为复制一个大对象的代价非常昂贵。

2.4 染色指针(Colored Pointers)

2.4.1 HotSpot虚拟机的标记实现方案有如下几种:

  • 把标记直接记录在对象头上(如Serial收集器);
  • 把标记记录在与对象相互独立的数据结构上(如G1使用了一种相当于堆内存1/64大小的,称为BitMap的结果来记录标记信息);
  • 直接把标记信息记录在对象的指针上(ZGC)

为什么会放在指针上呢?

追踪式收集算法的标记阶段就是看有没有引用,所以可以只和指针打交道而不管指针所引用的对象本身。
例如对象标记过程就是打个三色标记,这些标记本质上只和对象引用有关,和对象本身对象无关。某个对象只有它的引用关系才能决定它的存活。

2.4.2 染色指针解释

  • 染色指针是一种直接将少量额外的信息存储在指针上的技术。目前Linux下64位的操作系统中高18位是不能用来寻址的,但是剩余的46位却可以支持64T的空间,到目前为止几乎是使用不到这么大的内存的,于是ZGC将46位中的高4位取出,用来存储4个标志位,剩余的42位可以支持到4TB(2的42次幂),这也直接导致ZGC可以管理的内存不能超过4TB

ZGC4.png

Finalizable(可终结标记)
Remapped(重新映射)
Marked0、Marked1(标记位)

  • 限制:只有在64位系统上,因为ZGC设置就是使用的42-46位,32位明显不够。而且不支持压缩指针

2.4.3 堆内存多重映射(Multi-Mapping)

  • ZGC使用了多重映射技术,将多个不同的虚拟内存地址映射到同一个物理内存地址上,这是一种多对一映射
  • 因为染色指针只是重新定义内存中某些指针的其中几位,OS又不支持,OS只会把整个指针当做一个内存地址来对待,只是它自己瞎想,为了解决这个问题,使用了现代处理器的虚拟内存映射技术
  • Linux/x86-64平台上的ZGC使用了多重映射将多个不同的虚拟内存地址映射到同一个物理内存地址上,多对一映射。意味着ZGC在虚拟内存空间看到的地址空间比实际的堆内存容量更加大

ZGC5.png

2.4.4 染色指针的作用

  • 一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正之后才能清理,这使得理论上只要还有一个空闲Region,ZGC就能够完成收集。
  • 染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,ZGC只使用了读屏障。因为信息直接维护在指针中。
  • 染色指针具备强大的扩展性,它可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。

2.5 读屏障(Load Barrier)

  • 读屏障是每当应用程序从堆加载引用时运行的代码片段(既访问当前调用对象的字段,而不是当前对象的原生字段,non-primitive field)
void printName(Person person){
	String name = persion.name;  //这里触发读屏障,因为需要从heap读取引用
	System.out.println(name);  //这里不会直接触发读屏障
}
  • 读屏障主要在于对引用状态的检查,ZGC会在引用返回之前去检查着色指针的集合状态位:
    |- 如果检查通过(状态符合要求),则引用正常使用
    |- 如果检查不通过,那么就会在应用返回之前,ZGC会根据不同的状态来执行一些额外动作
  • ZGC中,当读取处于重新分配集的对象时,会被读屏障拦截,通过转发表记录将访问转发到新复制的对象上,并同时修正更新该引用值,使其直接指向新对象。ZGC将这种行为叫做指针的“自愈能力”。其好处是第一次访问旧对象访问会变慢,但也只会有一次变慢,当治愈完成后,后续访问就不会变慢了。ZGC的执行负载低。
    |- 因为在标记和移动的过程中,GC和应用线程是并发执行的,所以存在这种情况:对象A内存的引用所指的对象B在标记或者移动状态,为了保证应用线程拿到的B对象是正确的,那么在读取B的指针会经过一个“load barriers”读屏障,这个屏障可以保证正在执行GC时,数据读取的正确性。

测试


# JVM