垃圾收集器G1与ZGC

  • Post author:
  • Post category:其他




0. 前言

JVM有很多版本,最广为运用的为HotSpot,本文以

HotSpot

为基础。



1. JVM简介



1.1. HotSpot Architecture

在这里插入图片描述

  • 类加载器
  • 运行时数据区:堆、方法区、线程栈、程序计数器、本地线程栈
  • 执行引擎:解释器、JIT编译器(C1、C2)、垃圾收集器

    JVM调优往往在上述深颜色的这三个方面:堆(大小)、垃圾收集器、JIT编译器,其中JIT编译器我们一般不会涉及,大多数调整选项都与堆大小和所选择的垃圾收集器有关。



1.2. 调优目标


内存占用(Footprint)、吞吐量(Throughput)和延迟(Latency)

,三者共同构成了一个“不可能三角”。三者总体的表现会随技术进步而越来越好,但延迟的重要性日益凸显,因为在硬件设施发展的今天,内存可以很大,吞吐量也会更高,但这些对延迟反而起到了反面效果,吞吐量、内存和延迟貌似是两个相斥的指标。

简要介绍几个概念:


  • 响应性

    (短时间做出反应,没有大暂停时间)

  • 吞吐量

    (追求cpu在应用程序的时间比例)

  • Paraller

    (并行):垃圾回收线程为多线程

  • Concurrent

    (并发):应用线程与垃圾线程一起执行

附:吞吐量 = CPU在用户应用程序运行的时间 / (CPU在用户应用程序运行的时间 + CPU垃圾回收的时间)

大致看下G1之前的处理器

在这里插入图片描述

Serial、SerialOld:垃圾回收线程为单线程,且会阻塞应用线程,会STW

Ps+Po:并行处理,圾回收线程为多线程

CMS:并发处理,垃圾回收线程与应用线程同时运行



2. G1

G1(Garbage First Collector 垃圾优先的收集器):是一种服务器式垃圾收集器,针对具有大内存的多处理器机器。它以高概率满足垃圾收集 (GC) 暂停时间目标,同时实现高吞吐量。

目标:


吞吐量和低延迟


默认暂停在200ms

  1. 更高的暂停目标,高吞吐,高延时
  2. 更低的暂停目标,低吞吐,低延时

    在这里插入图片描述
    在这里插入图片描述



2.1. G1的Heap

G1将新生代,老年代的物理空间划分取消了,但是新生代和老年代的逻辑划分并没有取消(物理不分区,逻辑分区)。

G1用了

Regin

(1M~32M),1024个左右,每个Region默认按照512Kb划分成多个Card。每个Regin被化成Eden区、Survivor区、Old区。但是不固定(每次垃圾回收,区域都有可能更改)。

G1从多个region中复制存活的对象,然后集中放入一个region中,同时整理、清除内存(copying收集算法)。

在这里插入图片描述



2.2. 与CMS的垃圾回收阶段对比

先看下CMS的垃圾回收,一般使用

ParNew + CMS

的组合(CMS 收集器在 JDK 9 中已被弃用,并在 JDK 14 中被删除)



2.2.1. ParNew的youngGC

在这里插入图片描述

在这里插入图片描述



2.2.2. CMS的OldGC

在这里插入图片描述


缺点

  • CMS无法处理浮动垃圾,并发标记和并发清理时,还会产生垃圾,拖到下次回收
  • 标记-清除,有大量碎片
  • Remark阶段时间停顿过长



2.2.3. G1的youngGC

在这里插入图片描述

  • 年轻代垃圾收集或年轻 GC 是停止世界事件。停止所有应用程序线程以进行操作。
  • 活动对象被复制到新的幸存者或老年代区域。
  • 年轻 GC 使用多个线程并行完成。



2.2.4. G1的MixedGC

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述



2.3. 跨代引用以及部分区域回收

概念了解


RSet

(RememberedSets)用来解决跨代引用以及部分区域回收问题。(三个假说引起)

储着其他分区中的对象对本分区对象的引用。

G1和CMS都使用卡表(CardTable)来处理跨代指针。

Card->CardPage


如何解决跨代引用?

  • CMS:CMS中有且只有一个RSet(数组),在老年代,只需要处理老年代到新生代的引用,是一种point-out,反过来则不需要,由于新生代的对象具有朝生夕灭的不稳定性,引用变化频繁,能省下这个区域的维护开销是很划算的。

    在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。
  • G1:G1 中每个分区(Regin)有且只有一个RSet,这个Rset其实是一个hash table,key是别的region的起始地址,value是一个集合,里面的元素是card table的index。举例来说,如果region A的RSet里有一项的key是region B,value里有index为1234的card,它的意思就是region B的一个card里有引用指向region A。所以对region A来说,该RSet记录的是points-into的关系;而card table仍然记录了points-out的关系。

    每个Region默认按照512Kb划分成多个Card,所以RSet需要记录的东西应该是 xx Region的 xx Card。

在这里插入图片描述

附:point-in的意思是哪些分区引用了当前分区中的对象。poin-out相反。



2.4. 三色标记算法的发展



2.4.1. 三色标记理论

在这里插入图片描述


三种颜色对象:

  1. 白色:没有访问过。
  2. 黑色:对象已经访问过,而且本对象的 引用的其他对象也被访问过了。
  3. 灰色:对象已经访问过,本对象的 引用的其他对象 没有全部被访问过。


步骤:

  1. 所有对象都属于白色对象
  2. GCRoot直接引用的对象先变为灰色
  3. .GCRoot直接引用的对象先变为黑色,其引用对象为灰色
  4. .直到所有灰色对象变为黑色为止。剩余白色对象将被回收



2.4.2. 三色标记引发的问题

当 STW时,对象间的引用 是不会发生变化的,可以轻松完成标记。

需要支持并发标记时,即标记期间应用线程还在继续跑,

对象间的引用可能发生变化


问题就会出现:

多标和漏标

1.

多标


C被标记了(黑),D(灰),开始标记时,CD之间引用没了,D被当做存活对象,没有被回收,产生浮动垃圾(本应回收却没回收的)

并发标记后的产生的新对象,也被当做黑色对象,也有可能是浮动垃圾。

并不影响正确性,只是会等到下一轮回收中才被清楚。

2 .

漏标问题


情景一:

C被T1(标记线程)标记了(黑),D(灰),开始标记时,DE之间引用断掉了没了,E成为白的(应该被回收)

但因为时多线程并发标记,

另外一个T2(标记线程),标记从H开始的线,H已经变为黑的,但此时业务线程(T3)建立了HE的引用,理论上E应该变为灰的,但是因为H已经标记为黑色,结束了对子属性对象的标记,E则被当成垃圾回收了

初步解决方案(未完全解决):可以在建立引用时,M2把H对象从黑的标记为灰的,这样又要再次标记引用对象E,E就不会被回收。

并发标记的时候,依旧产生漏标,

增量更新(Incremental Update)


情景二:

如I有两个属性,T1(垃圾回收线程)已完成属性1(引用F),正在标属性2(引用J),T2(业务线程)把属性1指向了E(E在另一个垃圾回收线程中断开了DE引用,E变为白色),就算此时I对象被重新标记为灰色,也不会对属性1重新标记,因为属于T1对I的标记还未完成(本身就属于灰色)则E还是白的,会被漏标。

在这里插入图片描述


漏标条件

  • 赋值器插入了一条或多条从黑色对象到白色对象的新引用(新的引用)
  • 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。(引用变化)



2.4.3. 漏标解决思路

打破上述两个条件之一即可


  • Incremental Update 增量更新

    ,关注引用的增加(把黑 色的标位灰色的,下次重新扫描)

    SATB snapshot at the beginning 快照,关注引用的删除(把引用推到GC的堆栈,保证还能被GC扫描到)

    CMS用的时增量更新;

  • G1之写屏障和SATB(Snapshot at the beginning)


    在起始时做一个快照,当引用消失时,要把这个引用推到GC的堆栈,保证还能被GC扫描到,配合RSet,只用扫描哪些Regin引用到

如上述情景一,在DE引用变化时(用写屏障),记录E,E会被记录起来

保留对象的原始对象图。配合Rsets,查看还有那些引用到了E.

有新的引用进来(HE),(用写屏障),记录E

新增的引用,将其记录下等待变量,增量更新(Incremental Update)

在这里插入图片描述



2.5. 常用参数

在这里插入图片描述

-XX:+PrintFlagsFinal并使用grep命令搜索符合的默认值

java -XX:+PrintFlagsFinal | grep MaxHeapSize



2.6. G1中可以引起FullGC的四种情况

  1. 并发模式失效:标记周期,但在MixrecGC前老年代被填满。解决方案:增加堆大小,调整周期(增加并发线程-运行的更快,MixedGC更早触发-改变触发MixedGC比例)
  2. 晋升失败:G1收集器完成了标记阶段,启动混合回收,老年代空间在垃圾回收释放出足够内存之前就会被耗尽。解决方案:增加 -XX:G1ReservePercent 选项的值,提前启动标记周期,增加并行标记线程的数目
  3. 疏散失败:YoungGC时,Survivor空间和老年代中没有足够的空间容纳所有的幸存对象。解决方案:增加 -XX:G1ReservePercent 选项的值,提前启动标记周期,增加并行标记线程的数目
  4. 巨型对象分配失败:解决方案:避免分配大量的巨型对象,增加内存或者增大-XX:G1HeapRegionSize



3. 令人震惊的、革命性的ZGC

Oracle公司开发的ZGC就更像是AzulSystem公司独步天下的PGC(PauselessGC)和C4(ConcurrentContinuouslyCompactingCollector)收集器的同胞兄弟。

ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。


理念改变

:最先进的垃圾收集器的设计导向都不约而同地变为追求能够应付应用的内存分配速率(AllocationRate),而不追求一次把整个Java堆全部清理干净。这样,应用在分配,同时收集器在收集,只要收集的速度能跟得上对象分配的速度,那一切就能运作得很完美。这种新的收集器设计思路从工程实现上看是从G1开始兴起的,所以说G1是收集器技术发展的一个里程碑。

在这里插入图片描述

ZGC的优势

在这里插入图片描述

  • NUMA-aware(支持NUMA)
  • Using colored pointers(使用颜色指针)
  • Using load barriers(使用读屏障)

ZGC pause times do not increase with the heap or live-set size

ZGC pause times do increase with the root-set size



3.1. Region-based

在这里插入图片描述

在这里插入图片描述



3.2. 垃圾回收过程

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述



3.3. 染色指针技术(ColoredPointer)

染色指针:直接把标记信息记在引用对象的指针上。

传统垃圾收集器(CMS)在对象的对象头中Markword记录GC状态(更改堆内对象),G1使用相当于堆内存1/64大小的BitMap存储标记记录。ZGC是对指针进行修改。



3.3.1. 64位指针

ZGC仅支持64位系统,它把64位虚拟地址空间划分为多个子空间,如下图所示:

mmap函数和多视图映射

在这里插入图片描述

  • [0~4TB) 对应Java堆
  • [4TB ~ 8TB) 称为M0地址空间
  • [8TB ~ 12TB) 称为M1地址空间
  • [12TB ~ 16TB) 预留未使用
  • [16TB ~ 20TB) 称为Remapped空间

当应用程序创建对象时,首先在堆空间申请一个虚拟地址,但该虚拟地址并不会映射到真正的物理地址。ZGC同时会为该对象在M0、M1和Remapped地址空间分别申请一个虚拟地址,且这三个虚拟地址对应同一个物理地址,但这三个空间在同一时间有且只有一个空间有效。ZGC之所以设置三个虚拟地址空间,是因为它使用“空间换时间”思想,去降低GC停顿时间。“空间换时间”中的空间是虚拟空间,而不是真正的物理空间。

mmap函数和多视图映射

与上述地址空间划分相对应,ZGC实际仅使用64位地址空间的第0

41位,而第42

45位存储元数据,第47~63位固定为0。


为何就支持 4TB,不是还有不少位没用吗?


先 X86_64 的地址总线只有 48 条 ,因此最多其实只能用 48 位,指令集是 64 位没错,可是硬件层面就支持 48 位。

由于基本上没有多少系统支持这么大的内存,那支持 64 位就不必了,因此就支持到 48 位。

那如今对象地址就用了 42 位,染色指针用了 4 位,不是还有 2 位能够用吗?

是的,理论上能够支持 16 TB(OpenJDK 13已经支持),不过暂时认为 4TB 够了


linux指针64位

。高18位不能用来寻址,支持47位(128TB)虚拟地址空间和46位(64TB)的物理地指空间。取出其中4位指针来存储四个标志信息。

在这里插入图片描述



3.3.2. 地址视图的切换过程


地址视图

:指的就是此时地址指针的标记位。

接下来详细介绍ZGC一次垃圾回收周期中地址视图的切换过程:

  1. 初始化:ZGC初始化之后,整个内存空间的地址视图被设置为Remapped。程序正常运行,在内存中分配对象,满足一定条件后垃圾回收启动,此时进入标记阶段。
  2. 并发标记阶段:第一次进入标记阶段时视图为M0,如果对象被GC标记线程或者应用线程访问过,那么就将对象的地址视图从Remapped调整为M0。所以,在标记阶段结束之后,对象的地址要么是M0视图,要么是Remapped。如果对象的地址是M0视图,那么说明对象是活跃的;如果对象的地址是Remapped视图,说明对象是不活跃的。
  3. 并发转移阶段:标记结束后就进入转移阶段,此时地址视图再次被设置为Remapped。如果对象被GC转移线程或者应用线程访问过,那么就将对象的地址视图从M0调整为Remapped。

在这里插入图片描述

  1. 垃圾回收开始前视图为Remapped;
  2. 进行阶段标记,标记线程扫描到对象标为M0;应用线程若是建立新对象,也标为M0,且递归其引用对象;

    标记阶段结束,ZGC会有个对象活跃表来存储这些对象地址(M0视图对象)
  3. 并发转移阶段,地址视图被置为Remapped。即GC线程若是访问到对象为M0,且在活跃表时,将其转移,转移后置为Remapped,若不再活跃表中,则不处理

其实,在标记阶段存在两个地址视图M0和M1,上面的过程显示只用了一个地址视图。之所以设计成两个,是为了区别前一次标记和当前标记。也即,第二次进入并发标记阶段后,地址视图调整为M1,而非M0。

着色指针和读屏障技术不仅应用在并发转移阶段,还应用在并发标记阶段:将对象设置为已标记,传统的垃圾收集器需要进行一次内存访问,并将对象存活信息放在对象头中;而在ZGC中,只需要设置指针地址的第42~45位即可,并且因为是寄存器访问,所以速度比访问内存更快。



3.4. Using load barriers

用了

读屏障

,ZGC 能够

并发转移对象


G1 用的是

写屏障

,因此转移对象时候只能 STW

在这里插入图片描述



3.5. Enabling NUMA Support

在这里插入图片描述

这个架构被称为 SMP (Symmetric Multi-Processor),由于任一个 CPU 对内存的访问速度是一致的,不用考虑不一样内存地址之间的差别,因此也称一致内存访问(Uniform Memory Access,

UMA

)。

这个核心越加越多,渐渐的总线和北桥就成为瓶颈

在这里插入图片描述

每一个 CPU 访问本身的本地的内存比较快,访问别人的远程内存就比较慢,ZGC可以知道系统是否支持NUMA,并使用。



3.6 总结


优势

:

  1. 只要有一个Region就能回收
  2. 大幅减少内存屏障使用数量,只使用了load(读屏障),没有分代收集,启用了染色指针


缺点

:

它能承受的对象分配速率不会太高,ZGC准备要对一个很大的堆做一次完整的并发收集。在这段时间里面,由于应用的对象分配速率很高,将创造大量的新对象,这些新对象很难进入当次收集的标记范围,通常就只能全部当作存活对象来看待——尽管其中绝大部分对象都是朝生夕灭的,这就产生了大量的浮动垃圾。如果这种高速分配持续维持的话,每一次完整的并发收集周期都会很长,回收到的内存空间持续小于期间并发产生的浮动垃圾所占的空间,堆中剩余可腾挪的空间就越来越小了。目前唯一的办法就是尽可能地增加堆容量大小,获得更多喘息的时间。
在这里插入图片描述

在这里插入图片描述

附:图来自ZGC视频

相比于CMS和G1的GC触发机制,ZGC的GC触发机制有很大不同。ZGC的核心特点是并发,GC过程中一直有新的对象产生。如何保证在GC完成之前,新产生的对象不会将堆占满,是ZGC参数调优的第一大目标。因为在ZGC中,当垃圾来不及回收将堆占满时,会导致正在运行的线程停顿,持续时间可能长达秒级之久。

在这里插入图片描述

在这里插入图片描述



4. 垃圾收集器的选择和升级

从下面个方面简单分析:

  1. 程序需要什么特性?

    数据分析等,高吞吐就是关注点;web应用、低延迟就是首要考虑;客户端、嵌入式,内存占用就是不可忽视的。
  2. 基础设施如何?硬件规格、处理器数量和内存大小、操作系统等
  3. JDK发行商和版本?OracleJDK、OpenJDK还是其他公司,版本是7、8、11还是更高或更老版本,该JDK对应了《Java虚拟机规范》的哪个版本?

    目前,在硬件设施发展的今天,内存占用不像以前限制;JDK版本较高,且需要低延时,可以选择ZGC;JDK版本没有那么高,可以用G1,windows也可以考虑Shenandoah,较老的系统如果不需要大内存,例如堆内存在6G或4G甚至更低,CMS也可以处理的较好。

在这里插入图片描述



附:相关参考:

ZGC视频:

https://youtu.be/88E86quLmQA


G1视频:

https://www.youtube.com/watch?v=OhPGN2Av44E


G1官方文档:

https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html


oracle blog:

https://blogs.oracle.com/javamagazine/post/understanding-the-jdks-new-superfast-garbage-collectors


Java 11 到 Java 17 的最佳 HotSpot JVM 选项和开关:

https://blogs.oracle.com/javamagazine/post/the-best-hotspot-jvm-options-and-switches-for-java-11-through-java-17


Java 命令行检查器:

https://jacoline.dev/inspect


ZGC openJdk官方文档:

https://wiki.openjdk.java.net/display/zgc


Shenandoah openJdk官方文档 :

https://wiki.openjdk.java.net/display/shenandoah/Main


OrcleJDK:

https://docs.oracle.com/en/java/javase/15/gctuning/



版权声明:本文为do_finsh原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。