文章 62
浏览 15135
G1&ZGC垃圾收集器介绍

G1&ZGC垃圾收集器介绍

pg.jpg

G1 收集器(-XX:+UseG1GC)

G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.

image.png

image.png

G1 将 Java 堆划分为多个大小相等的独立区域( Region ),JVM 目标是不超过 2048 个 Region(JVM 源码里 TARGET_REGION_NUMBER 定义),实际可以超过该值,但是不推荐。

一般 Region 大小等于堆大小除以 2048,比如堆大小为 4096M,则 Region 大小为 2M,当然也可以用参数"-XX:G1HeapRegionSize"手动指定 Region 大
小,但是推荐默认的计算方式。
G1 保留了年轻代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region 的集合。
默认年轻代对堆内存的占比是 5%,如果堆大小为 4096M,那么年轻代占据 200MB 左右的内存,对应大概是 100 个 Region,可以通过“-
XX:G1NewSizePercent”设置新生代初始占比,在系统运行中,JVM 会不停的给年轻代增加更多的 Region,但是最多新生代的占比不会超过 60%,可
以通过“-XX:G1MaxNewSizePercent”调整。年轻代中的 Eden 和 Survivor 对应的 region 也跟之前一样,默认 8:1:1,假设年轻代现在有 1000 个
region,eden 区对应 800 个,s0 对应 100 个,s1 对应 100 个。
一个 Region 可能之前是年轻代,如果 Region 进行了垃圾回收,之后可能又会变成老年代,也就是说 Region 的区域功能可能会动态变化。

G1 垃圾收集器对于对象什么时候会转移到老年代跟之前讲过的原则一样, 唯一不同的是对大对象的处理 ,G1 有专门分配大对象的 Region 叫 Humongous 区 ,而不是让大对象直接进入老年代的 Region 中。在 G1 中,大对象的判定规则就是一个大对象超过了一个 Region 大小的 50%,比如按照
上面算的,每个 Region 是 2M,只要一个大对象超过了 1M,就会被放入 Humongous 中,而且一个大对象如果太大,可能会横跨多个 Region 来存放。
Humongous 区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的 GC 开销。
Full GC 的时候除了收集年轻代和老年代之外,也会将 Humongous 区一并回收。

G1 收集器一次 GC(主要值 Mixed GC)的运作过程大致分为以下几个步骤:

  • 初始标记 (initial mark,STW):暂停所有的其他线程,并记录下 gc roots 直接能引用的对象, 速度很快 ;
  • 并发标记 (Concurrent Marking):同 CMS 的并发标记
  • 最终标记 (Remark,STW):同 CMS 的重新标记
  • 筛选回收 (Cleanup,STW):筛选回收阶段首先对各个 Region 的 回收价值和成本进行排序 , 根据用户所期望的 GC 停顿 STW 时间(可以用 JVM 参
    数 -XX:MaxGCPauseMillis 指定)来制定回收计划 ,比如说老年代此时有 1000 个 Region 都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能
    停顿 200 毫秒,那么通过之前回收成本计算得知,可能回收其中 800 个 Region 刚好需要 200ms,那么就只会回收 800 个 Region( Collection Set ,要回
    收的集合),尽量把 GC 导致的停顿时间控制在我们指定的范围内。这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分
    Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。不管是年轻代或是老年代, 回收算法主要用的是复制算法 , 将一个 region
    中的存活对象复制到另一个 region 中,这种不会像 CMS 那样回收完因为有很多内存碎片还需要整理一次,G1 采用复制算法回收几乎不会有太多内存碎片 。(注意:CMS 回收阶段是跟用户线程一起并发执行的,G1 因为内部实现太复杂暂时没实现并发回收,不过到了 ZGC,Shenandoah 就实现了并
    发收集,Shenandoah 可以看成是 G1 的升级版本)

image.png

G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来),比如一个 Region 花 200ms 能回收 10M 垃圾,另外一个 Region 花 50ms 能回收 20M 垃圾,在回收时间有限情况下,G1 当然会优先选择后面这个 Region 回收 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率。

被视为 JDK1.7 以上版本 Java 虚拟机的一个重要进化特征。它具备以下特点:

  • 并行与并发 :G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收
    集器原本需要停顿 Java 线程来执行 GC 动作,G1 收集器仍然可以通过并发的方式让 Java 程序继续执行。
  • 分代收集 :虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
  • 空间整合 :与 CMS 的“标记--清理”算法不同,G1 从整体来看是基于“ 标记整理 ”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
  • 可预测的停顿 :这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立 可预测的停顿时间模型 ,能让使用者明确指定在一个长度为 M 毫秒的时间片段(通过参数" -XX:MaxGCPauseMillis "指定)内完成垃圾收集。

毫无疑问, 可以由用户指定期望的停顿时间是 G1 收集器很强大的一个功能, 设置不同的期望停顿时间, 可使得 G1 在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。 不过, 这里设置的“期望值”必须是符合实际的, 不能异想天开, 毕竟 G1 是要冻结用户线程来复制对象的, 这个停顿时间再怎么低也得有个限度。 它默认的停顿目标为两百毫秒, 一般来说, 回收阶段占到几十到一百甚至接近两百毫秒都很正常, 但如果我们把停顿时间调得非常低, 譬如设置为二十毫秒, 很可能出现的结果就是由于停顿目标时间太短, 导致每次选出来的回收集只占堆内存很小的一部分, 收集器收集的速度逐渐跟不上分配器分配的速度, 导致垃圾慢慢堆积。 很可能一开始收集器还能从空闲的堆内存中获得一些喘息的时间, 但应用运行时间一长就不行了, 最终占满堆引发 Full GC 反而降低性能, 所以通常把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的。

G1 垃圾收集分类

  • YoungGC
    YoungGC 并不是说现有的 Eden 区放满了就会马上触发,G1 会计算下现在 Eden 区回收大概要多久时间,如果回收时间远远小于参数 -
    XX:MaxGCPauseMills 设定的值,那么增加年轻代的 region,继续给新对象存放,不会马上做 Young GC,直到下一次 Eden 区放满,G1 计算回收时间
    接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发 Young GC
  • MixedGC
    不是 FullGC,老年代的堆占有率达到参数( -XX:InitiatingHeapOccupancyPercent )设定的值则触发,回收所有的 Young 和部分 Old(根据期望的 GC
    停顿时间确定 old 区垃圾收集的优先顺序)以及 大对象区 ,正常情况 G1 的垃圾收集是先做 MixedGC,主要使用复制算法,需要把各个 region 中存活的对象
    拷贝到别的 region 里去,拷贝过程中如果发现 没有足够的空 region 能够承载拷贝对象就会触发一次 Full GC
  • Full GC
    停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批 Region 来供下一次 MixedGC 使用,这个过程是非常耗时的。
    (Shenandoah 优化成多线程收集了)

G1 收集器参数设置

  • -XX:+UseG1GC:使用 G1 收集器
  • -XX:ParallelGCThreads:指定 GC 工作的线程数量
  • -XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是 2 的 N 次幂),默认将整堆划分为 2048 个分区
  • -XX:MaxGCPauseMillis:目标暂停时间(默认 200ms)
  • -XX:G1NewSizePercent:新生代内存初始空间(默认整堆 5%,值配置整数,默认就是百分比)
  • -XX:G1MaxNewSizePercent:新生代内存最大空间
  • -XX:TargetSurvivorRatio:Survivor 区的填充容量(默认 50%),Survivor 区域里的一批对象(年龄 1+ 年龄 2+ 年龄 n 的多个年龄对象)总和超过了 Survivor 区域的 50%,此时就会把年龄 n(含)以上的对象都放入老年代
  • -XX:MaxTenuringThreshold:最大年龄阈值(默认 15)
  • -XX:InitiatingHeapOccupancyPercent:老年代占用空间达到整堆内存阈值(默认 45%),则执行新生代和老年代的混合收集( MixedGC ),比如我们之前说的堆默认有 2048 个 region,如果有接近 1000 个 region 都是老年代的 region,则可能就要触发 MixedGC 了
  • -XX:G1MixedGCLiveThresholdPercent(默认 85%) region 中的存活对象低于这个值时才会回收该 region,如果超过这个值,存活对象过多,回收的的意义不大。
  • -XX:G1MixedGCCountTarget:在一次回收过程中指定做几次筛选回收(默认 8 次),在最后一个筛选回收阶段可以回收一会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。
  • -XX:G1HeapWastePercent(默认 5%): gc 过程中空出来的 region 是否充足阈值,在混合回收的时候,对 Region 回收都是基于复制算法进行的,都是把要回收的 Region 里的存活对象放入其他 Region,然后这个 Region 中的垃圾对象全部清理掉,这样的话在回收过程就会不断空出来新的 Region,一旦空闲
    出来的 Region 数量达到了堆内存的 5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。

G1 垃圾收集器优化建议

假设参数 -XX:MaxGCPauseMills 设置的值很大,导致系统运行很久,年轻代可能都占用了堆内存的 60% 了,此时才触发年轻代 gc。
那么存活下来的对象可能就会很多,此时就会导致 Survivor 区域放不下那么多的对象,就会进入老年代中。
或者是你年轻代 gc 过后,存活下来的对象过多,导致进入 Survivor 区域后触发了动态年龄判定规则,达到了 Survivor 区域的 50%,也会快速导致一些对象进入老年代中。所以这里核心还是在于调节 -XX:MaxGCPauseMills 这个参数的值,在保证他的年轻代 gc 别太频繁的同时,还得考虑每次 gc 过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发 mixed gc.

什么场景适合使用 GC

  1. 50% 以上的堆被存活对象占用
  2. 对象分配和晋升的速度变化非常大
  3. 垃圾回收时间特别长,超过 1 秒
  4. 8GB 以上的堆内存(建议值)
  5. 停顿时间是 500ms 以内

每秒几十万并发的系统如何优化 JVM

Kafka 类似的支撑高并发消息系统大家肯定不陌生,对于 kafka 来说,每秒处理几万甚至几十万消息时很正常的,一般来说部署 kafka 需要用大内存机器(比如 64G),也就是说可以给年轻代分配个三四十 G 的内存用来支撑高并发处理,这里就涉及到一个问题了,我们以前常说的对于 eden 区的 young gc 是很快的,这种情况下它的执行还会很快吗?很显然,不可能,因为内存太大,处理还是要花不少时间的,假设三四十 G 内存回收可能最快也要几秒钟,按 kafka 这个并发量放满三四十 G 的 eden 区可能也就一两分钟吧,那么意味着整个系统每运行一两分钟就会因为 young gc 卡顿几秒钟没法处理新消息,显然是不行的。那么对于这种情况如何优化了,我们可以使用 G1 收集器,设置 -XX:MaxGCPauseMills 为 50ms,假设 50ms 能够回收三到四个 G 内存,然后 50ms 的卡顿其实完全能够接受,用户几乎无感知,那么整个系统就可以在卡顿几乎无感知的情况下一边处理业务一边收集垃圾。G1 天生就适合这种大内存机器的 JVM 运行,可以比较完美的解决大内存垃圾回收时间过长的问题。

ZGC 收集器(-XX:+UseZGC)

参考文章

ZGC 是一款 JDK 11 中新加入的具有实验性质的低延迟垃圾收集器,ZGC 可以说源自于是 Azul System 公司开发的 C4(Concurrent ContinuouslyCompacting Collector) 收集器。

image.png

https://wiki.openjdk.java.net/display/zgc/Main
http://cr.openjdk.java.net/~pliden/slides/ZGC-Jfokus-2018.pdf

ZGC 目标

如下图所示,ZGC 的目标主要有 4 个:

image.png

  • 支持 TB 量级的堆 。我们生产环境的硬盘还没有上 TB 呢,这应该可以满足未来十年内,所有 Java 应用的需求了吧。
  • 最大 GC 停顿时间不超 10ms 。目前一般线上环境运行良好的 Java 应用 Minor GC 停顿时间在 10ms 左右,Major GC 一般都需要 100ms 以上(G1 可以调节停顿时间,但是如果调的过低的话,反而会适得其反),之所以能做到这一点是因为它的停顿时间主要跟 Root 扫描有关,而 Root 数量和堆大小是没有任何关系的。
  • 奠定未来 GC 特性的基础
  • 最糟糕的情况下吞吐量会降低 15% 。这都不是事,停顿时间足够优秀。至于吞吐量,通过扩容分分钟解决。另外,Oracle 官方提到了它最大的优点是:它的停顿时间不会随着堆的增大而增长!也就是说,几十 G 堆的停顿时间是 10ms 以下,几百 G 甚至上 T 堆的
    停顿时间也是 10ms 以下。

不分代(暂时)

单代,即 ZGC「没有分代」。我们知道以前的垃圾回收器之所以分代,是因为源于“「大部分对象朝生夕死」”的假设,事实上大部分系统的对象分配行为也确实符合这个假设。

那么为什么 ZGC 就不分代呢?因为分代实现起来麻烦,作者就先实现出一个比较简单可用的单代版本,后续会优化。

ZGC 内存布局

ZGC 收集器是一款基于 Region 内存布局的, 暂时不设分代的, 使用了 读屏障、 颜色指针 等技术来实现可并发的标记-整理算法的, 以低延迟为首要目
标的一款垃圾收集器。
ZGC 的 Region 可以具有如图 3-19 所示的大、 中、 小三类容量:

  • 小型 Region(Small Region) : 容量固定为 2MB, 用于放置小于 256KB 的小对象。
  • 中型 Region(Medium Region) : 容量固定为 32MB, 用于放置大于等于 256KB 但小于 4MB 的对象。
  • 大型 Region(Large Region) : 容量不固定, 可以动态变化, 但必须为 2MB 的整数倍, 用于放置 4MB 或以上的大对象。 每个大型 Region 中只会存放一个大对象 , 这也预示着虽然名字叫作“大型 Region”, 但它的实际容量完全有可能小于中型 Region, 最小容量可低至 4MB。 大型 Region 在 ZGC 的实现中是不会被重分配(重分配是 ZGC 的一种处理动作, 用于复制对象的收集器阶段, 稍后会介绍到)的, 因为复制一个大对象的代价非常高昂。

image.png

NUMA-aware

image.png

NUMA 对应的有 UMA,UMA 即 Uniform Memory Access Architecture ,NUMA 就是 Non Uniform Memory Access Architecture。UMA 表示内存只有一块,所有 CPU 都去访问这一块内存,那么就会存在竞争问题(争夺内存总线访问权),有竞争就会有锁,有锁效率就会受到影响,而且 CPU 核
心数越多,竞争就越激烈。NUMA 的话每个 CPU 对应有一块内存,且这块内存在主板上离这个 CPU 是最近的,每个 CPU 优先访问这块内存,那效率自然就提高了服务器的 NUMA 架构在中大型系统上一直非常盛行,也是高性能的解决方案,尤其在系统延迟方面表现都很优秀。ZGC 是能自动感知 NUMA 架构并充分利用 NUMA 架构特性的。

ZGC 运作过程

ZGC 的运作过程大致可划分为以下四个大的阶段:

image.png

  • 并发标记(Concurrent Mark) :与 G1 一样,并发标记是遍历对象图做可达性分析的阶段,它的初始标记( Mark Start )和最终标记( Mark End )也会出
    现短暂的停顿,与 G1 不同的是, ZGC 的标记是在指针上而不是在对象上进行的, 标记阶段会更新 颜色指针 (见下面详解)中的 Marked 0、 Marked 1 标志位。
  • 并发预备重分配(Concurrent Prepare for Relocate)** :这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set)。ZGC 每次回收都会扫描所有的 Region,用范围更大的扫描成本换取省去 G1 中记忆集的维护成本。
  • 并发重分配(Concurrent Relocate) :重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的 Region 上,并为重分配集中的每个 Region 维护一个 转发表(Forward Table) ,记录从旧对象到新对象的转向关系。ZGC 收集器能仅从引用上就明确得知一个对象是否处于
    重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障( 读屏障 (见下面详解))所截获,然后立即根据 Region 上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC 将这种行为称为指针的“自愈”(Self-
    Healing)能力。
1 ZGC的颜色指针因为“自愈”(Self-Healing)能力,所以只有第一次访问旧对象会变慢, 一旦重分配集中某个Region的存活对象都复制完毕后,
2 这个Region就可以立即释放用于新对象的分配,但是转发表还得留着不能释放掉, 因为可能还有访问在使用这个转发表。
  • 并发重映射(Concurrent Remap) :重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,但是 ZGC 中对象引用存在“自愈”功能,所以这个重映射操作并不是很迫切。ZGC 很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后, 原来记录新旧对象关系的转发表就可以释放掉了。

颜色指针

Colored Pointers,即颜色指针,如下图所示,ZGC 的核心设计之一。以前的垃圾回收器的 GC 信息都保存在对象头中,而 ZGC 的 GC 信息保存在指针中。

image.png

每个对象有一个 64 位指针,这 64 位被分为:

  • 18 位:预留给以后使用;
  • 1 位:Finalizable 标识,此位与并发引用处理有关,它表示这个对象只能通过 finalizer 才能访问;
  • 1 位:Remapped 标识,设置此位的值后,对象未指向 relocation set 中(relocation set 表示需要 GC 的 Region 集合);
  • 1 位:Marked1 标识;
  • 1 位:Marked0 标识,和上面的 Marked1 都是标记对象用于辅助 GC;
  • 42 位:对象的地址(所以它可以支持 2^42=4T 内存):
    为什么有 2 个 mark 标记?
    每一个 GC 周期开始时,会交换使用的标记位,使上次 GC 周期中修正的已标记状态失效,所有引用都变成未标记。
    GC 周期 1:使用 mark0, 则周期结束所有引用 mark 标记都会成为 01。
    GC 周期 2:使用 mark1, 则期待的 mark 标记 10,所有引用都能被重新标记。
    通过对配置 ZGC 后对象指针分析我们可知,对象指针必须是 64 位,那么 ZGC 就无法支持 32 位操作系统,同样的也就无法支持压缩指针了(CompressedOops,压缩指针也是 32 位)。
    颜色指针的三大优势:
  1. 一旦某个 Region 的存活对象被移走之后,这个 Region 立即就能够被释放和重用掉,而不必等待整个堆中所有指向该 Region 的引用都被修正后才能清理,这使得理论上只要还有一个空闲 Region,ZGC 就能完成收集。
  2. 颜色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,ZGC 只使用了读屏障。
  3. 颜色指针具备强大的扩展性,它可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。

读屏障

之前的 GC 都是采用 Write Barrier,这次 ZGC 采用了完全不同的方案读屏障,这个是 ZGC 一个非常重要的特性。
在标记和移动对象的阶段,每次「从堆里对象的引用类型中读取一个指针」的时候,都需要加上一个 Load Barriers。那么我们该如何理解它呢?看下面的代码,第一行代码我们尝试读取堆中的一个对象引用 obj.fieldA 并赋给引用 o(fieldA 也是一个对象时才会加上读屏
障)。如果这时候对象在 GC 时被移动了,接下来 JVM 就会加上一个读屏障,这个屏障会把读出的指针更新到对象的新地址上,并且把堆里的这个指针“修正”到原本的字段里。这样就算 GC 把对象移动了,读屏障也会发现并修正指针,于是应用代码就永远都会持有更新后的有效指针,而且不需要 STW。
那么,JVM 是如何判断对象被移动过呢?就是利用上面提到的颜色指针,如果指针是 Bad Color,那么程序还不能往下执行,需要「slow path」,修正指针;如果指针是 Good Color,那么正常往下执行即可:

image.png

❝ 这个动作是不是非常像 JDK 并发中用到的 CAS 自旋?读取的值发现已经失效了,需要重新读取。而 ZGC 这里是之前持有的指针由于 GC 后失效了,需要通过读屏障修正指针。❞

后面 3 行代码都不需要加读屏障:Object p = o 这行代码并没有从堆中读取数据;o.doSomething()也没有从堆中读取数据;obj.fieldB 不是对象引用,
而是原子类型。
正是因为 Load Barriers 的存在,所以会导致配置 ZGC 的应用的吞吐量会变低。官方的测试数据是需要多出额外 4% 的开销:

那么,判断对象是 Bad Color 还是 Good Color 的依据是什么呢?就是根据上一段提到的 Colored Pointers 的 4 个颜色位。当加上读屏障时,根据对象指
针中这 4 位的信息,就能知道当前对象是 Bad/Good Color 了。
PS: 既然低 42 位指针可以支持 4T 内存,那么能否通过预约更多位给对象地址来达到支持更大内存的目的呢?答案肯定是不可以。因为目前主板地址总线
最宽只有 48bit,4 位是颜色位,就只剩 44 位了,所以受限于目前的硬件,ZGC 最大只能支持 16T 的内存,JDK13 就把最大支持堆内存从 4T 扩大到了
16T。

ZGC 存在的问题

ZGC 最大的问题是 浮动垃圾 。ZGC 的停顿时间是在 10ms 以下,但是 ZGC 的执行时间还是远远大于这个时间的。假如 ZGC 全过程需要执行 10 分钟,在这个期间由于对象分配速率很高,将创建大量的新对象,这些对象很难进入当次 GC,所以只能在下次 GC 的时候进行回收,这些只能等到下次 GC 才能回收的对象就是浮动垃圾。

1 ZGC没有分代概念,每次都需要进行全堆扫描,导致一些“朝生夕死”的对象没能及时的被回收。

解决方案
目前唯一的办法是增大堆的容量,使得程序得到更多的喘息时间,但是这个也是一个治标不治本的方案。如果需要从根本上解决这个问题,还是需要引
入分代收集,让新生对象都在一个专门的区域中创建,然后专门针对这个区域进行更频繁、更快的收集。

ZGC 参数设置

启用 ZGC 比较简单,设置 JVM 参数即可:-XX:+UnlockExperimentalVMOptions 「-XX:+UseZGC」。调优也并不难,因为 ZGC 调优参数并不多,远不
像 CMS 那么复杂。它和 G1 一样,可以调优的参数都比较少,大部分工作 JVM 能很好的自动完成。下图所示是 ZGC 可以调优的参数:

image.png

ZGC 触发时机

ZGC 目前有 4 中机制触发 GC

  • 定时触发,默认为不使用,可通过 ZCollectionInterval 参数配置。
  • 预热触发,最多三次,在堆内存达到 10%、20%、30% 时触发,主要时统计 GC 时间,为其他 GC 机制使用。
  • 分配速率,基于正态分布统计,计算内存 99.9% 可能的最大分配速率,以及此速率下内存将要耗尽的时间点,在耗尽之前触发 GC(耗尽时间 - 一次 GC 最大持续时间 - 一次 GC 检测周期时间)。
  • 主动触发,(默认开启,可通过 ZProactive 参数配置) 距上次 GC 堆内存增长 10%,或超过 5 分钟时,对比距上次 GC 的间隔时间跟(49 * 一次 GC 的最大持续时间),超过则触发。

如何选择垃圾收集器

  1. 优先调整堆的大小让服务器自己来选择
  2. 如果内存小于 100M,使用串行收集器
  3. 如果是单核,并且没有停顿时间的要求,串行或 JVM 自己选择
  4. 如果允许停顿时间超过 1 秒,选择并行或者 JVM 自己选
  5. 如果响应时间最重要,并且不能超过 1 秒,使用并发收集器
  6. 4G 以下可以用 parallel,4-8G 可以用 ParNew+CMS,8G 以上可以用 G1,几百 G 以上用 ZGC
    下图有连线的可以搭配使用**

image.png

JDK 1.8 默认使用 Parallel(年轻代和老年代都是)
JDK 1.9 默认使用 G

安全点与安全区域

安全点

指代码中一些特定的位置,当线程运行到这些位置时它的状态是确定的,这样 JVM 就可以安全的进行一些操作,比如 GC 等,所以 GC 不是想什么时候做就立即触发的,是需要等待所有线程运行到安全点后才能触发。

这些特定的安全点位置主要有以下几种:

  1. 方法返回之前
  2. 调用某个方法之后
  3. 抛出异常的位置
  4. 循环的末尾

大体实现思想是当垃圾收集需要中断线程的时候, 不直接对线程操作, 仅仅简单地设置一个标志位, 各个线程执行过程时会不停地主动去轮询这个标志, 一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。 轮询标志的地方和安全点是重合的。

安全区域

Safe Point 是对正在执行的线程设定的。
如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。
因此 JVM 引入了 Safe Region。
Safe Region 是指在一段代码片段中, 引用关系不会发生变化 。在这个区域内的任意地方开始 GC 都是安全的。

  • 安全点

无论时在 GC 中还是线程按权重都会出现安全点(Safepoint)这个概念,当我们需要阻塞一个线程时都需要在安全点停止,简单说安全点就是指当前线程运行到这类位置时,堆对象状态时确定一致的,当前线程停止后,JVM 可以安全地进行操作,如 GC、偏向锁撤离等。安全点定义:

① 循环结束的末尾段

② 方法调用之后

③ 抛出异常的位置

④ 方法返回之前
当 JVM 需要发生 GC、偏向锁撤离等操作时如何让所有线程达到安全点阻塞?

① 主动式中断(JVM 采用的中断方式):不中断线程,而是设置一个标志然后让每个线程执行时主动轮询这个标志,当一个线程到达安全点后,发现中单标志位 true 时就自己中断挂起

② 抢断式中断:先中断所有线程,如果发现线程未执行到安全点则恢复线程让其运行到安全点位置。

  • 安全区域(SafeRegion)

当一个线程处于中断或者休眠状态时就不能响应 JVM 的中断请求走到安全点区域挂起了,所以出现了安全区域的概念。按段区域是指一个线程执行到一段代码时,该区域的代码不会改变堆内存对象的引用,在这个区域内 JVM 可以安全地进行操作。当线程进入到该区域时需要先标识自己进入了,这样 GC 线程则不会管这些已标识的线程,当线程要离开这个区域时需要先判断 GCRoots 是否完成,如果完成了则往下执行,如果没有 😭 则需要原地等待到 GC 线程发出安全离开信息为止


标题:G1&ZGC垃圾收集器介绍
作者:xiaohugg
地址:https://xiaohugg.top/articles/2023/05/06/1683363623001.html

人民有信仰 民族有希望 国家有力量