新一代垃圾回收器:G1详解
作者:VioletTecQQ:595585575原创笔记,个人整理,欢迎并感谢指出错误。对应视频地址:<新一代垃圾回收器:G1详解_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili>视频内的PPT在本笔记的同一压缩包下文档MarkDown下载链接:https://wwa.lanzous.com/i1nVVjs84ba
1. Java的GC简介:
在了解G1之前,我们先回顾一下GC的历史以及各种GC算法和GC收集器对象的新建(new)后,会存储在堆中,
而我们的堆内存不可能无限大,但是Java中我们总是new一个对象而没有释放一个对象,那么一定有一个回收器在背后默默的帮助我们释放内存,这个回收器就是我们的GC(Garbage Collecotr)垃圾回收器。
1.1 什么是垃圾?
不能被GC Root引用到的对象就是垃圾,能被GC Root引用到的对象一定不是垃圾。
1.2 什么是GC Root?
在运行时方法区中,栈中的一个栈帧中的本地变量表(LVA)中,某一个插槽(slot)引用了堆中的对象,那么这个被引用的对象可以被称作一个GC Root。哪些对象可以作为 GC Roots 的对象:【重点补充】
- 虚拟机栈中局部变量(也叫局部变量表)中引用的对象
- 方法区中类的静态变量、常量引用的对象
- 本地方法栈中 JNI (Native方法)引用的对象
能被GC Root中引用到的就称之为活对象,不能被GC Root引用到的就称之为死对象
1.3 各种GC收集器和GC算法
1.3.1 GC收集器

1.3.2 GC算法

1.4 GC的分代假设
IBM曾经做过一个调查,在一个Java程序中,98%的对象都是朝升夕死的。所以我们在JVM的堆中划分出两个区域(代)
- 年轻代(Yong):用于存储刚创造出的对象
- 老年代(Old):经历了15次GC以上的对象 / 大对象 / 空间分配担保

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 限制线程数量
- ParNew
- Remove in JEP363 (JEP 363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector (java.net))

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)
4.2 为什么会出现G1收集器?
由于现在堆内存越来越大(32G / 64G / 128G),以及人们对性能的追求,之前的简单算法已经无法满足我们的需要,我们只能进行对算法的优化来进行降低GC带来的延迟。迄今为止,所有的串行/并行/CMS都是要么不回收,要么回收整个年轻代/老年代,会造成很高的延迟但是G1并不会这样,它适合在大的堆中工作。
4.3 G1收集器的特性
- 软实时、低延迟、可设定目标
- JDK9+默认GC(JEP248)(JEP 248: Make G1 the Default Garbage Collector (java.net))
- 适用于较大的内存( > 4 ~ 6 G)
- 用于代替CMS
在G1中,传统上的堆内存结构被抛弃
4.4 G1收集器的内存布局
- 将堆分成若干个等大的区域
- -XX:G1HeapRegionSize=N (2048 by default【默认2048个region】)
- 一个Region大小:
堆内存/Region个数
- 一个Region大小:
G1中的堆内存布局并非连续的。
一个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之间的相互调用
解决问题方法:
4.7 如何解决这个问题?
为了解决这个问题,G1在每一个Region中,都添加了两个表用于记录被引用的对象
- Card Table
- 表中的每个entry覆盖512Byte的内存空间
- 当对应的内存空间发生改变时(如赋值操作的时候,会将该对象所在卡片标记),标记为dirty
- RememberSet(RSet)
- 指向Card Table中对应的entry
- 可找到具体内存区域
- 空间换时间
- 用额外的空间维护引用信息
- 5%~10% memory overhead
当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
- 白/绿/黄/红四个颜色
