垃圾回收三大步骤:判断是不是垃圾(垃圾判断算法) -> 标记需要回收的垃圾(三色标记) -> 回收垃圾(垃圾回收算法)。
一、如何判断是不是垃圾(垃圾判断算法)?
(1)引用计数器算法:对象中记录引用次数(引用就++,引用失效--),此方式无法判断循环依赖的引用问题。
(2)可达性分析算法:将一些对象作为GC根向下搜索,无法被搜索到的说明无引用可回收。
GC根:比如栈帧中本地变量表引用的对象、方法区静态属性或常量引用的对象、本地方法(Nativie)栈引用的对象等等。
二、如何标记需要回收的垃圾(三色标记)?
(一)三色标记四个步骤
a. 初始标记阶段:STW,只标记GC根直接关联的对象,耗时极少。
b. 并发标记阶段:由GC根依次向下标记所有关联的对象,可达为存活对象,不可达为可回收对象。
c. 重新标记阶段:STW,标记新建的对象引用(GMC收集器通过读写屏障+增量更新记录新建的引用对象)。
d. 清除标记阶段:GC线程与用户线程并发清理被标记的垃圾对象。
- 白色:尚未被GC扫描过的对象;
- 灰色:被CG访问过但其直接引用的对象未被访问过(不可回收,全部访问后变成黑色);
- 黑色:被GC访问过且其直接引用的对象也全都被访问过(非垃圾对象)。
(二)三色标记的问题
- 多标:A→B→C,A黑B灰C白时,A→B引用断了。(不用考虑,因为下一次可能被CG)
- 少标/漏标:A→B,A黑B黑时,突然新增了B→C。(C变成浮动垃圾了)
(少标/漏标)解决方案:
- 读屏障+重新标记:并发结束后STW重新标记新增的引用;
- 写屏障+增量更新:将新增的引用加入待扫描的集合中;
- 写屏障+原始快照:引用关系变化时记录之前的引用关系快照,扫描旧的对象再次标记。
三、如何回收垃圾(垃圾回收算法)?
(一)垃圾回收的算法
- 标记-清除算法:效率低、容易产生大量空间碎片;
- 标记-复制算法:无碎片、但浪费一半的内存;(新生代回收算法)
- 标记-整理算法:先标记对象,然后让存活对象向一端移动,最后清理掉存活对象边界外的内存;
- 分代收集算法:内存划分为新生代(1/3)和老年代(2/3),新生代采用标记-复制算法,老年代采用标记-整理/清除。
(二)分代收集算法
新生代默认内存分配比:Enden:From:To=8:1:1;
进入老年代条件:
(1)15次GC依旧存活的对象;
(2)大对象:大小占Eden区的一半;
(3)空间分配担保:检查老年代最大可用内存是否大于新生代所有对象之和,成立(未开启则Full GC)则检查老年代空间是否大于历史晋升对象的平均大小,是则Minor GC;
(4)动态年龄判断:Survivor区(From或To)相同年龄对象的大小>Survivor区的一半,年龄大于此值的可以直接进入老年代。
老年代和新生代之间引用的解决方案:记忆集(卡表)。
四、垃圾收集器
(一)G1垃圾收集器
G1将堆分成很多个Region,每个Region只有在使用时才会确定其唯一角色(Eden、From/To、Old区、humongous区)
回收某个region的价值大小 = 回收获得的空间大小 + 回收所需时间。
(二)ZGC垃圾收集器
ZGC:Region分为小、中、大三种,染色指针。
染色指针:对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集(Remapped)、是否需要通过 finalize 方法来访问到(Finalizable)。