掘金 后端 ( ) • 2024-04-14 21:28

theme: awesome-green highlight: a11y-dark

👈👈👈 欢迎点赞收藏关注哟

首先分享之前的所有文章 >>>> 😜😜😜
文章合集 : 🎁 https://juejin.cn/post/6941642435189538824
Github : 👉 https://github.com/black-ant
CASE 备份 : 👉 https://gitee.com/antblack/case

一. 前言

G1 回收器的回收循环主要分为3大主要的类型 : 年轻代循环 ,多步骤并行标记循环 ,混合收集阶段。 同时流程外还有一个保护性的 FullGC 同样存在。

而在流程上我会把年轻代再细分一下,来模拟一下当对象逐渐增长情况下,回收的演变。

历史文章 :

# 这一回 ,把 G1 回收器的特性聊通透

先来看张流程图 :

image.png

文章目的 :

  • 梳理 G1 GC 整个阶段的大流程
  • 梳理 每个环节的内存变化
  • 不涉及到源码及细节原理

二. 回收的阶段

2.1 第一阶段 : YoungGC (新生代 GC)

  • S1 : 初始时会分配给 5% 大小的 Region 给新生代
  • S2 : G1 从空闲区间里面挑选出区间加入年轻代
  • S3 : 当新生代的体积逐渐变大, Eden 区已经无法扩展 (60% Region)时,则会触发一次 YoungGC.
    • 这个阶段还会计算总量,统计RSet,当前容量等等数据,用于后续整理年轻代
    • 该环节收集的内存是不固定的 ,回收的分区数目也是不固定的
  • S4 : 在这个环节会触发 STW , 通过复制算法收集 Eden 区存活的对象,放入 S0 Survivor
  • S5 : 在这个过程中 , 会交替使用 S0 和 S1 两个幸存区,直到对象或者整体容积达到触发老年代的条件
    • 也就是说每次YGC后都会为对象生成年龄信息,放入一个年龄虚拟表
    • 每一次 YGC 后都会带来新生代分区数目的变化

在这个阶段中,会对 Eden 和 Survivor 区进行回收,清除垃圾对象,添加对象的生命周期。

image.png

  • 核心点首先是根集合 ,然后根据根集合处理的对象处理 RSet 的管理关系。
  • TODO : 当然这里还涉及到没有回收的区域的引用关系以及具体的引用实现,这一篇就不细说了。

2.2 第二阶段 :进化到老年代

  • S1 : 随着一轮一轮的 YoungGC , Survivor 的对象越来越多 , 部分对象熬过了多轮 YoungGC
  • S2 : 此时对达到了老年代阈值的对象进行转移,将这部分生命超过 MaxTenuringThreshold 的对象回收到老年代
  • S3 : 在高并发场景或者回收的继续迭代,Survivor 存活的对象超过了其本身空间的50%
  • S4 : 取当前 50% 的年龄对象 ,对大于该年龄的对象进行转移,放入老年代

这其实属于第一阶段,单独做了一层划分,在这阶段老年代对象会不断积累。

image.png

阶段总结

由于这个阶段的积累,老年代数据越来越多 ,就会触发混合 GC (Mixed GC ).

这个 GC 阶段分为两个阶段 :

  • 并发标记 👉 下文 2.3 第三阶段部分
    • 用于识别老年代里面的活跃对象 ,判断垃圾对象所占空间以及是否需要回收
  • 垃圾回收 👉 下文 2.4 第四阶段部分
    • 流程基本和新生代一致,区别在于会针对老年代的Region一起回收

好了 ,继续看下面的!!

2.3 第三阶段 : 并发标记周期

到了这个环节 , 老年代的对象也逐渐积累,这个时候就要为混合回收做准备了。 在这个过程中 ,不会阻碍应用程序的执行, 通过 GC Root 对堆中对象进行可达性分析,查找到所有可以回收的对象。

在这个阶段中 ,标记会分为几个阶段 :

初始标记(STW) 👉 根区域标记 👉 并行标记 👉 重新标记(STW) 👉 清理阶段


  • 初始化标记Initial Mark) :会标记出所有的根对象以及直接与根对象关联的对象。 在这过程中,会触发一次 STW , 从而建立一个 SATB 快照 ,用于后续的跟踪。
    • 根对象 : 栈对象 ,全局对象 ,JNI 对象等
    • 特点 :此阶段会直接借用 YGC 标记的结果,将 Survivor 分区作为根
  • 并发标记Concurrent Marking): 当 YGC 触发后 ,如果满足并发标记的条件,则会触发并发标记
    • InitiatingHeapOccupancyPercent : 默认45 , 即当内存分配超过内存总量的 45% 时 ,触发
    • XX:ConcGCThreads : 启动的并发线程的数量 , 每个线程每次只扫描一个分区
    • STW : 该阶段会扫描所有的分区,不需要 STW , 和主线程并发
  • 重新(最终)标记Final Remark) : 当 YoungGC 经历了多轮后,就可能会进入 Remark 重新标记阶段,
    • 目标 : 处理在并发标记期间发生的引用变化,确保标记信息的准确性。
    • 行为 : 该阶段保证了根扫描及存活对象扫描以及完成 ,同时待处理的标记栈为空
    • STW : 这个阶段也会触发STW ,原因在于需要保证引用都被处理(一直在变
  • 清理阶段Cleanup): 在这个阶段会进行选择性的回收
    • 独占清理 : 基于前面的标记信息来计算各个区域的存活数量以及可回收比例,放入排队序列
    • 并发清理 : 识别并且清理完全空闲的区域,这个阶段是并发执行的 , 清理完直接放入空闲区间
    • 并发整理 : 将存活的对象从多个 Region 复制到多个连续的 Region 中,保证空间的连续
    • 其他操作 : 包括 重置RSet交换标记位图
    • 特别注意 : 清理操作并不会清理垃圾对象 !!

这里我直接借用 《JVM G1源码分析和调优》 来展现整个流程 ,大家可以看看原书,写得很好

image.png

  • 重点一 : 可以明确的看到 ,并不是并发标记后就不 YGC 了, 在 MixedGC 前会一直有
  • 重点二 : 并发标记 和 混合回收整个时间周期其实不短

2.4 第四阶段 : 混合阶段垃圾回收

  • IHOP (-XX:InitiatingHeapOccupancyPercent) : 堆内存占用阈值,用于触发混合回收,默认 45%。
  • 不同于 CMS 可以针对老年代设置阈值 , G1 设置的 IHOP 是针对整个堆内存(Region没有明确的分代

混合回收会对新生代 ,老年代 ,以及大对象进行回收。 另外混合回收会触发 STW , 同时触发最终标记阶段,确定哪些对象是存活对象,哪些对象需要回收,

由于在之前的过程里面,已经对所有区域的所有对象进行过标记,所以可以很轻松的知道那些区域里面的垃圾比例,也就是之前提到的垃圾密度。 G1 回收器会首先收集垃圾比例较高的阶段。

与新生代回收一样,这个阶段剩余的对象会统一合并到多个区域里面去。

这个阶段的操作和 YGC 基本一致,这里就不细说了。

2.5 最终阶段 : FullGC

这个阶段并不是绝对执行,当吞吐量过大或者系统问题导致堆内存不足时,就会触发一次 FullGC.

FullGC 阶段和其他的垃圾回收器一样,也会 STW 同时对所有的区域进行清理。

  • S1 : 对象分配失败 ,进入 Evac 阶段
  • S2 : 尝试再次分配内存 ,如果分配失败 ,则进入 FullGC

这一块其实很复杂,涉及到 RSet的处理,一篇肯定说不清楚,关键是我也没完全看懂,总结不出来啊!

实在想了解的可以关注或者直接看书。

三. 关于回收过程中的标记

3.1 关于 Survivor 空间

谈到 G1 回收器的幸存者区的时候,通常会提到四个值 : S0 Survivor SpaceS1 Survivor Space 以及 From Survivor SpaceTo Survivor Space

那么他们有什么关系呢 ?

S0 Survivor Space 和 S1 Survivor Space 可以理解为一种物理上的概念 , 当对象创建的时候,如果 Eden 满了触发 YoungGC , 一般收集完成后,把存活的对象放在 S0 区域中。而在下一次垃圾回收的过程中 ,由于 S0 也会被回收, 此时就会把存活的对象放到 S1 区域中。从而实现两个幸存区的交替使用。

而 From 和 To 开头的幸存区则是一种逻辑概念。

  • From Survivor Space 是上一轮垃圾回收结束后被用作存放存活对象的Survivor区
  • To Survivor Space 是当前垃圾回收正在使用的Survivor区。

例如当前被回收的是 S0 , 当前空着的是 S1 . 我们会回收完成后把存活对象放在 S1 中。在这个场景下 : S0 = From , S1 = To.

3.2 关于大对象

G1 为大对象引入了一个大对象区间,当一个对象超过一个 Region 的 50% 时 , 就会被认定为大对象。 G1 对于这些大对象不会放在老年代,而是放在 老年代之外大对象区间 里面。

大对象区间通常是多个连续的 Region 区间 。而在回收时,年轻代回收,混合回收, FullGC 都会对大对象区间进行回收工作。

补充的问题 :

就像上文说的并发标记实际上是从 YGC 的结果作为根的 。 当时很多场景下 ,老年代的对象并没有被年轻代的引用.

这种情况下就会导致对象被漏标 ,所以在这阶段 ,要把直接从根出发到老生代的引用或者大对象分区的引用补上。

总结

这一块勉强算是弄清楚了 ,下一篇就针对其中的一些重点进行梳理了 ,膜拜大佬。

整个系列会很长,包括 G1 , ZGC 等多个系列,欢迎关注。

很多东西看的时候也是一知半解,如果有问题欢迎指出。

参考文档

👍 《JVM G1源码分析和调优》

《深入Java虚拟机:JVM G1GC的算法与实现》

《深入理解JVM & G1 GC》

https://zhuanlan.zhihu.com/p/405142523

我又懂了更多G1的GC大道理 - 知乎 (zhihu.com)

一次垃圾回收的革新——了解G1收集器 - 知乎 (zhihu.com)