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

作者:VioletTecQQ: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-20201226150751473IBM曾经做过一个调查,在一个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