新一代垃圾回收器:G1详解

作者:VioletTec

QQ:595585575

原创笔记,个人整理,欢迎并感谢指出错误。

对应视频地址:<新一代垃圾回收器:G1详解_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili>

视频内的PPT在本笔记的同一压缩包下

文档MarkDown下载链接:https://wwa.lanzous.com/i1nVVjs84ba


1. Java的GC简介:

在了解G1之前,我们先回顾一下GC的历史以及各种GC算法和GC收集器

对象的新建(new)后,会存储在堆中,

image-20201226144227153

而我们的堆内存不可能无限大,但是Java中我们总是new一个对象而没有释放一个对象,那么一定有一个回收器在背后默默的帮助我们释放内存,这个回收器就是我们的GC(Garbage Collecotr)垃圾回收器。

1.1 什么是垃圾?

不能被GC Root引用到的对象就是垃圾,能被GC Root引用到的对象一定不是垃圾。

image-20201226144513082

1.2 什么是GC Root?

在运行时方法区中,栈中的一个栈帧中的本地变量表(LVA)中,某一个插槽(slot)引用了堆中的对象,那么这个被引用的对象可以被称作一个GC Root。

哪些对象可以作为 GC Roots 的对象:【重点补充】

  • 虚拟机栈中局部变量(也叫局部变量表)中引用的对象
  • 方法区中类的静态变量、常量引用的对象
  • 本地方法栈中 JNI (Native方法)引用的对象

能被GC Root中引用到的就称之为活对象,不能被GC Root引用到的就称之为死对象

image-20201226145509222

1.3 各种GC收集器和GC算法

1.3.1 GC收集器

image-20201226150352443

1.3.2 GC算法

image-20201226150330476

1.4 GC的分代假设

image-20201226150751473

IBM曾经做过一个调查,在一个Java程序中,98%的对象都是朝升夕死的。所以我们在JVM的堆中划分出两个区域(代)

  • 年轻代(Yong):用于存储刚创造出的对象
  • 老年代(Old):经历了15次GC以上的对象 / 大对象 / 空间分配担保

image-20201226151544934

2. 古典时代的GC:Serial/Parallel

大部分的产品都是从简单到复杂进行演化,GC也一样,古典时代的Serial/Parallel算法也非常简单,只有并行和串行两种方式。

2.1 Serial收集器

(串行收集器【单线程垃圾处理器】)

  • 年轻代Serial
  • 老年代SerialOld
  • 特点:串行收集器是最古老,最稳定以及效率高的收集器 可能会产生较长的停顿,只使用一个线程去回收
    • 新生代、老年代使用串行回收
    • 新生代复制算法
    • 老年代标记 - 清除 - 压缩算法
  • 相关命令: -XX:+UseSerialGC

2.2 Parallel收集器

(并行收集器【多线程垃圾处理器】)

  • 年轻代:Parallel Scavenge
  • 老年代:Parallel Old
  • 特点:类似 ParNew, 新生代复制算法 老年代标记 - 清除 - 压缩算法 更加关注吞吐量
  • 相关命令: -XX:+UseParallelGC
    • Parallel 收集器 + 老年代串行
    -XX:+UseParallelOldGC
    • Parallel 收集器 + 老年代并行

3. 中古时代的GC:CMS

CMS:(Concurrent Mark Sweep)并行标记清除算法

  • 低延时的系统
  • 不进行Compact (压缩)
  • 用于老年代
  • 配合Serial/ParNew使用 (由于Parallel Scavenge算法无法和CMS一起用,所以就开发了一个ParNew算法用于年轻代的回收
    • ParNew
      • 新生代并行
      • 老年代串行
      • Serial 收集器新生代的并行版本 在新生代回收时使用复制算法 多线程,需要多核支持
      相关命令:-XX:+UseParNewGC(new 代表新生代,所以适用于新生代) -XX:ParallelGCThreads 限制线程数量
  • Remove in JEP363 (JEP 363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector (java.net)

image-20201226151738506

3.1 CMD收集器中的Remark

由于你和工作线程在并行执行,所以工作线程可能会干扰你的垃圾回收标记(GC标记的是活对象,没有标记的对象都会被清除),所以在真正回收之前,会(Stop-The-World)停止工作线程,重新进行一次标记,防止由于正在运行中的工作线程的干扰而出现错误(比如工作线程又新建了一个对象,但是GC没有标记,会误杀)。

但是通常这个(Stop-The-World)会用时比较短。


4. 现代的GC:G1【重点】

G1的设计目标:

  • 尽可能进行自动调优
  • 尽可能小于你所设置的GC延迟时间限制
  • 用于代替CMS

4.1 GC的发展史

GC收集器的发展顺序从左上角到右下角(Serial —> Parallel —> CMS —> G1)

image-20201226153445246

4.2 为什么会出现G1收集器?

由于现在堆内存越来越大(32G / 64G / 128G),以及人们对性能的追求,之前的简单算法已经无法满足我们的需要,我们只能进行对算法的优化来进行降低GC带来的延迟。

迄今为止,所有的串行/并行/CMS都是要么不回收,要么回收整个年轻代/老年代,会造成很高的延迟

但是G1并不会这样,它适合在大的堆中工作。


4.3 G1收集器的特性

在G1中,传统上的堆内存结构被抛弃

image-20201226154852556

4.4 G1收集器的内存布局

  • 将堆分成若干个等大的区域
  • -XX:G1HeapRegionSize=N (2048 by default【默认2048个region】)
    • 一个Region大小: 堆内存/Region个数

G1中的堆内存布局并非连续的。

image-20201226154918861

一个region就是一个小方块。默认大小是 堆内存/2048,即一个堆中有2048个region

4.5 G1收集器的内部细节

  • 无需回收整个堆,而是选择一个Collection Set(CS)
  • 有两种GC
    • Fully yong GC(全年轻代GC)
    • Mixed GC(混合GC【包含了一次全年轻代GC】)
  • 可以估计每一个Region中的垃圾比例,优先回收垃圾多的Region
    • That's why it's called Garbage First(G1)

4.6 G1收集器存在的问题

  • 老年代和年轻代的跨代引用(老年代可能会持有年轻代的引用)
  • 不同Region之间的相互调用

解决问题方法:

image-20201226202615146

4.7 如何解决这个问题?

为了解决这个问题,G1在每一个Region中,都添加了两个表用于记录被引用的对象

  • Card Table
    • 表中的每个entry覆盖512Byte的内存空间
    • 当对应的内存空间发生改变时(如赋值操作的时候,会将该对象所在卡片标记),标记为dirty
  • RememberSet(RSet)
    • 指向Card Table中对应的entry
    • 可找到具体内存区域
  • 空间换时间
    • 用额外的空间维护引用信息
    • 5%~10% memory overhead
image-20201226202837350

当Region2被回收器回收时,先检查RSet中是否有被其他的Region引用,若有,则根据RSet中的card进行扫描该对象是否也需要回收。


4.7.1 什么时候将Card放进RSet中?

当进行赋值操作的时候,进行一个Write Barrier操作。

object.field = <reference> (putfield)

即在赋值的时候,更新被引用对象的RSet,添加object对象所在的entry到被引用对象的RSet


  • 当更新指针时
    • 标记Card为dirty
    • 将Card存入Dirty Card Queue
    • 白/绿/黄/红四个颜色image-20201226210823697

为什么不直接放如RSet呢?为什么还要存入DirtyCardQueue这个队列呢?

因为Java程序是多线程的,如果每次在更新Dirty的时候直接存入RSet,可能会产生多个线程竞争写入RSet,因为这个更新RSet的操作是非常频繁的在进行的。(因为在Java程序中赋值操作的频率很频繁)

那么什么时候更新Remember Set(RSet)呢?

我们注意到DirtyCardQueue有四种颜色。四种颜色对应不同的状态

  • 白区:
    • 天下太平,无事发⽣
  • 绿区: (-XX:G1ConcRefinementGreenZone=N)
    • Refinement(优化)线程开始被激活,开始更新RS
      • 具体操作:正常的从队列中拿出DirtyCard,并更新到对应的RSet中
  • 黄区:(-XX:G1ConcRefinementYellowZone=N) 当应用产生DirtyCard的速度非常快的时候,会进入黄区
    • 全部Refinement线程开始激活
      • 目的:全力以赴的把队列排空,目标就是不要让队列太慢
  • 红区:(-XX:G1ConcRefinementRedZone=N) 当应用产生的DirtyCard太快了的时候
    • 应用线程也参与排空队列的工作
      • 目的:把应用线程拖慢,使得Refinement能够得到及时的执行完毕。

4.8 G1的工作流程


4.8.1 G1中不同的收集器

4.8.1.1 Fully Yong GC 流程
  • STW(Stop The World)
    • 构建CS【Collection Set】(Eden+Survivor)
    • 扫描GC Roots
    • Update RS:排空Dirty Card Queue,并更新Remember Set
    • Process RS:在Remember Set中找到被哪些老年代的对象跨代引用的。
    • Object Copy:常规的对新生代进行标记复制算法
    • Reference Processing:回收可以被回收的引用类型
      • 强引用:强引用所指向的对象在任何时候都不会被系统回收。JVM宁愿抛出OOM异常,也不会回收强引用所指向的对象。
      • 软引用:不会被JVM很快回收,JVM会根据当前堆的使用情况来判断何时回收。当堆使用率临近阈值时,才会去回收软引用的对象。
      • 弱引用:在系统GC时,只要发现弱引用,不管系统堆空间是否足够,都会将对象进行回收。(即在弱引用新建后的下一次GC时就会被回收。 )
      • 虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。【更多关于四种引用类型的知识具体可见知乎科普贴:Java的四种引用类型 - 知乎 (zhihu.com)

  • G1记录每个阶段的时间,用于自动调优。
  • 记录Eden/Survivor的数量和GC时间
    • 根据暂停目标自动调整Region数量(如果达不到你设定的时间,则减少该Region的数量) 如果你设置了Eden区GC时间只能小于5ms,但是你一次回收了100ms,你只能减少Eden区的数量来尽量满足你对该Region的GC最小暂停时间的设置
    • 暂停目标越短,Eden数量越少 如果你设置的Eden区的Region过于短,那么可能会导致Eden区过于少,从而导致CPU大部分时间都在回收Eden区上。导致——> 吞吐量下降【单位时间工作数量】
    • 打印自适应的尺寸调节策略:-XX:+PrintAdaptiveSizePolicy
    • 打印老年代的提升的分布:-XX:+PrintTenuringDistribution
image-20201226214105724

所有的Eden区和Suvivor区都进入一个新的Surivor区(标记复制算法)


4.8.1.2 Old GC 老年代GC
  • 当堆用量达到一定程度时除法
    • -XX:IntiatingHeapOccupancyPercent=N
    • 45 by default(默认是45)
  • Old GC是并发(concurrent)进行的

Old GC问题:你如何在工作线程还在继续的情况下,标记所有的活对象?


4.9 三色标记算法

并发标记阶段(即GC线程和工作线程一起工作的时候),会使用该算法。

白色:还未被扫描到

灰色:正在被扫描

黑色:已经被扫描完成以及其所引用的内容也被扫描完成

三色标记算法就是从GC Root遍历堆中的对象,默认一开始都是白色,将可以访问到的对象先标记为灰色,然后读取其引用的对象,当该对象引用的其他也被扫描完的时候,从灰色标记为黑色。

最后所有可达对象都变成了黑色,所有不可达对象都是白色,清除所有白色对象即可。

  1. 一开始所有对象都是白色
image-20201226214535395
  1. 进行Old GC的时候进行扫描GC Root,把GC Root标记为黑色,GC Root直接引用的对象标记为灰色,然后将所有灰色组成一个队列
image-20201226214618782
  1. 从灰色队列中取出一个灰色对象,把它标记为黑色,并把它所有直接引用的对象都标记成灰色

4. 以此类推,把所有的对象都标记成黑色之后,慢慢的把灰色队列清空,在最后,灰色队列已空,只剩白色对象,GC Root标记完成。

但是由于工作线程一直在运行,所以可能会发生Lost Object Problem

image-20201226215039579

最后造成;

本来C是活对象,但是由于G1和工作线程是并行,所以被遗漏,这叫做Lost Object Problem


4.9.1 三色标记算法可能会产生的问题
  • B.c = null
    • 即当C指针被删除时,G1认为C仍然是活对象。(误标,即造成浮动垃圾)
  • Snapshot-At-The-Beginning (SATB)
    • 保持在marking阶段开始的object graph
    • C仍被remark阶段处理
    • 可能产生浮动垃圾

4.10 Snapshot-At-The-Beginning (SATB)

img

当G1线程与工作线程并行标记的时候,可能会造成对象的漏标(即活对象没有被正确标志)

那么会导致本来应该是不被回收的对象,被GC回收,对程序的正确性产生影响。

导致对象漏标的场景:

  • ①:对Black对象新引用了一个White对象,然后又从Gray对象中删除了对White对象的引用,这样就会造成了White对象露标记
    • 原因:因为Black对象是已经被扫描完成的对象,不会再进行第二次的引用扫描,而之前与White对象有联系的Gray对象又解除了对White对象的引用。故那一个White对象永远不会被扫描到,则会被GC回收,但是Black对象却又需要引用White对象,那么当Black对象要调用White对象的时候,就会发生崩溃。
  • ②:对Black新引用了一个White对象(A),然后又从Gray对象删除了一个引用该White对象(A)的White对象(B),这样也会造成了该White对象漏标记。
    • 原因:对象A的间接引用对象B被删除,导致A无法和Gray对象建立引用关系,又因为和建立引用关系的Black对象是已经被完全标记完的对象,不会进行第二次标记,则对象A永远无法被访问,造成漏标。
  • ③:对Black对象引用了一个新New出来的White对象,没有其他Gray对象引用该White对象,也会造成了该White对象露标记。
    • 原因:类似于①的原因,只不过没有了Gray对象。

解决办法:

  • 对于第③种情况,利用post-write barrier,记录所有新增的引用关系,然后根据这些引用关系为根重新扫描一遍。
  • 对于第①②种情况,利用pre-write barrier,将所有既将被删除的引用关系的旧引用记录下来,最后以这些旧引用为根重新扫描一遍。

4.11 G1 Old GC流程(补充 4.8.1.2)

  • STW (Fully young GC)
    • piggy-backed on young GC 【骑在年轻GC头上面】
    • 利用Young GC的信息
  • 恢复应用线程
  • 并发初始标记 (Init marking)
  • STW
    • Remark(保证找到的所有对象都是活对象)
      • SATB/Reference processing
    • Cleanup(不进行老年代的回收,仅仅只回收全部是垃圾的Region)
      • 立刻回收全空的区
  • 恢复应用线程
image-20201227222725774

4.12 Mixed GC

Mixed GC的时候,会进行老年代和年轻代的回收。

  • 不⼀定立即发生
  • 选择若干个Region进行
    • 默认1/8的Old Region
    • -XX:G1MixedGCCountTarget=N
    • Eden+Survivor Region
    • STW, Parallel, Copying
      • (因为需要拷贝,所以算法和年轻代GC是完全相同的算法)
      • 同时由于年轻代Region和老年代Region大小完全相同,所以都使用同一种算法处理
    • 根据暂停目标,选择垃圾最多的Old Region优先进行(Garbage First 【G1】的名字的意义)
      • 由于清理的Region是活对象最少的,所以耗时也是最短的。
        • (因为GC延迟跟堆大小无关,跟活对象多少有关)
      • -XX:G1MixedGCLiveThresholdPercent=N (default 85)
      • -XX:G1HeapWastePercent=N
标签:

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

Captcha Code