作者:VioletTec
QQ: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 收集器 + 老年代串行
- Parallel 收集器 + 老年代并行
3. 中古时代的GC:CMS
CMS:(Concurrent Mark Sweep)并行标记清除算法
- 低延时的系统
- 不进行Compact (压缩)
- 用于老年代
- 配合Serial/ParNew使用 (由于Parallel Scavenge算法无法和CMS一起用,所以就开发了一个ParNew算法用于年轻代的回收
- ParNew
- 新生代并行
- 老年代串行
- Serial 收集器新生代的并行版本 在新生代回收时使用复制算法 多线程,需要多核支持
- 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
- 白/绿/黄/红四个颜色
为什么不直接放如RSet呢?为什么还要存入DirtyCardQueue这个队列呢?
因为Java程序是多线程的,如果每次在更新Dirty的时候直接存入RSet,可能会产生多个线程竞争写入RSet,因为这个更新RSet的操作是非常频繁的在进行的。(因为在Java程序中赋值操作的频率很频繁)
那么什么时候更新Remember Set(RSet)呢?
我们注意到DirtyCardQueue有四种颜色。四种颜色对应不同的状态
- 白区:
- 天下太平,无事发⽣
- 绿区: (-XX:G1ConcRefinementGreenZone=N)
- Refinement(优化)线程开始被激活,开始更新RS
- 具体操作:正常的从队列中拿出DirtyCard,并更新到对应的RSet中
- Refinement(优化)线程开始被激活,开始更新RS
- 黄区:(-XX:G1ConcRefinementYellowZone=N) 当应用产生DirtyCard的速度非常快的时候,会进入黄区
- 全部Refinement线程开始激活
- 目的:全力以赴的把队列排空,目标就是不要让队列太慢
- 全部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
所有的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遍历堆中的对象,默认一开始都是白色,将可以访问到的对象先标记为灰色,然后读取其引用的对象,当该对象引用的其他也被扫描完的时候,从灰色标记为黑色。
最后所有可达对象都变成了黑色,所有不可达对象都是白色,清除所有白色对象即可。
- 一开始所有对象都是白色
- 进行Old GC的时候进行扫描GC Root,把GC Root标记为黑色,GC Root直接引用的对象标记为灰色,然后将所有灰色组成一个队列
- 从灰色队列中取出一个灰色对象,把它标记为黑色,并把它所有直接引用的对象都标记成灰色
4. 以此类推,把所有的对象都标记成黑色之后,慢慢的把灰色队列清空,在最后,灰色队列已空,只剩白色对象,GC Root标记完成。
但是由于工作线程一直在运行,所以可能会发生Lost Object Problem
最后造成;
本来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)
当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)
- 立刻回收全空的区
- Remark(保证找到的所有对象都是活对象)
- 恢复应用线程
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
- 由于清理的Region是活对象最少的,所以耗时也是最短的。