文章目录
一、对象引用类型
垃圾回收的作用就是回收内存中的对象,不同的对象引用类型对垃圾回收有不同的影响:
- 强引用:
这种引用类型只有当引用丢失后,对象才会被清理。User user = new User();
- 软引用:
在引用没有丢失的时候,对象正常情况不会被回收,当 GC 后发现释放不出空间存放新的对象时,则会把这些软引用的对象回收掉。SoftReference<User> user = new SoftReference<User>(new User());
- 弱引用:
不管引用是否丢失,GC 时都会将对象回收。WeakReference<User> user = new WeakReference<User>(new User());
- 虚引用:
虚引用并不会决定对象的生命周期,虚引用(PhantomReference)必须和引用队列(ReferenceQueue)一起使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象之前,把这个虚引用加入到与之关联的引用队列中。这样,应用程序就可以通过检查引用队列来了解对象是否即将被回收或者已经被回收。ReferenceQueue<Object> queue = new ReferenceQueue<>(); Object obj = new Object(); PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue); obj = null; System.gc(); PhantomReference<Object> removedRef = (PhantomReference<Object>) queue.remove(); if (removedRef!= null) { System.out.println("The object has been collected."); }
二、垃圾判定方法
1. 引用计数法
给对象中添加一个引用计数器,每有一个地方引用它,计数器就加 1,当引用失效,计数器就减 1;计数器为 0 的对象就被判定为垃圾对象。
在主流的虚拟机中都没有选用这个方式来判定垃圾,因为这个方式虽然实现简单,但是却没有办法解决循环引用的问题。
循环引用代码:
public class App {
Object obj;
public static void main(String[] args) throws InterruptedException {
App r1 = new App();
App r2 = new App();
r1.obj = r2;
r2.obj = r1;
r1 = null;
r2 = null;
}
}
2. 可达性分析法
将 GCRoot 作为根节点向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象。图中绿色的都是非垃圾对象,灰色的都是垃圾对象。
可以当作 GCRoot 的对象:
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
三、垃圾回收算法
1. 分代收集理论
根据对象存活周期的不同将堆内存分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率比较高,无法划分出多余空间进行复制,所以更适合 “标记-清除” 或 “标记-整理” 算法进行垃圾收集。
2. 复制算法
复制算法将内存分为大小相同的两块,每次只使用其中的一块。当垃圾回收时,将这块内存中还存活的对象全部复制到另一块去,然后再把原来的那块空间一次清理掉。
优点:不会产生内存碎片。
缺点:只能使用一半内存,导致内存利用率不高。如果有大量对象存活时,复制比较耗性能。
复制算法清理垃圾后内存空间前后对比图:
3. 标记清除算法
标记清除算法,分为标记阶段和清除阶段:
- 标记阶段:将存活的对象进行标记
- 清除阶段:清除没有标记的对象(即非存活对象,垃圾对象)
这种回收算法的缺点就是在清理后会造成内存不连续,会有很多内存碎片。
标记清除算法清理垃圾后内存空间前后对比图:
4. 标记整理算法
标记整理算法,分为 3 个阶段:
- 标记:将存活的对象进行标记
- 整理:将所有标记的对象都移动到同一侧进行排列,并记录队尾位置
- 清除:从队尾后面开始进行全部清理
标记整理算法的优点就是不会产生内存碎片,也不会像复制算法一样浪费内存空间。
缺点是整理阶段会和复制算法一样因为移动存活对象而消耗性能。
标记整理算法清理垃圾后内存空间前后对比图:
四、垃圾收集器
垃圾收集器是对垃圾收集算法的具体实现。
1. Serial 收集器
Serial 收集器(串行收集器),是一个单线程的垃圾收集器,在执行垃圾回收时,仅有一个 GC 线程执行,而且还会暂停全部用户线程(Stop The World),直到垃圾清理完成再恢复用户线程。
Serial 收集器有两个:
- Serial:回收新生代,采用复制算法
- Serial Old:回收老年代,采用标记-整理算法
2. Parallel Scavenge 收集器
Parallel 收集器(并行收集器),相当于 Serial 收集器的多线程版本,在执行垃圾回收时,和 Serial 收集器一样会暂停全部用户线程(Stop The World),直到垃圾清理完成再恢复用户线程。但是在清理阶段是多线程并行执行垃圾清理工作。
Parallel 收集器更多的是关注吞吐量,吞吐量=用户代码执行时间 / 总耗时(用户代码执行时间 + GC 执行时间)
清理阶段的 GC 线程数默认和 CPU 核数相同,也可以使用 -XX:ParallelGCThreads=[整数]
设置 GC 线程数
Parallel 收集器有两个:
- Parallel:回收新生代,采用复制算法
- Parallel Old:回收老年代,采用标记-整理算法
3. ParNew 收集器
ParNew 收集器的原理和 Parallel 收集器差不多,它主要的特点是可以搭配 CMS,通常是用 ParNew 在新生代使用复制算法进行回收,用 CMS 在老年代使用标记清除算法进行回收。
4. CMS 收集器
CMS 在 Java 8 流行的年代是最火、最常用的垃圾收集器,但在 Java 9 中已经被标记为过时,并在 Java 14 中被移除。
CMS (Concurrent Mark Sweep)收集器更多的是关注用户线程的暂停时间,是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,它是 HotSpo t虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
CMS 收集器用于老年代,使用的是标记清除算法,它的执行逻辑分为 5 个步骤:
- 初始标记:
暂停所有的用户线程(STW),对 gc root 的直接子节点中存活的对象进行标记,因为只查找一层,所以速度很快。 - 并发标记:
以第一步标记的那些对象为起点,查找它们所有的子节点并进行标记。这个过程虽然耗时较长,但是不需要停顿用户线程, 可以并发执行。因为用户程序还在运行,可能会导致已经标记为存活的对象现在已经丢失引用,或者之前判定是垃圾的对象现在已经复活。 - 重新标记:
重新标记阶段就是对那些因为用户程序继续运行而产生变动的对象做重新标记,主要是利用三色标记的概念。这个阶段会再次 STW,但是因为只是对有变动的对象进行重新标记,所以速度仍然很快。 - 并发清理:
用户线程 和 GC 线程 并发执行,GC 线程开始清理所有未标记的区域。这个阶段如果有新增对象会被标记为黑色不做任何处理(三色标记中的概念)。 - 并发重置:
重置本次 GC 过程中的标记数据。
5. G1 和 ZGC 简述
G1 (Garbage-First)
G1 主要针对配备多颗处理器及大容量内存的机器。G1 不像之前的垃圾收集器,把 Eden、Survivor、Old 划分成几个固定区域。G1 把整个内存划分成很多相同大小的 Region,每个 Region 即可能是 Eden、也可能是 Survivor 或 Old,而且还新增了 Humongous 区的概念,专门存放大对象。从 JDK 9 开始,G1 垃圾收集器成为了默认的垃圾收集器。
ZGC
ZGC 消除了年龄分代的概念,号称 16 TB 的堆内存,STW 时间也会控制在 10ms 之内。ZGC 的可调优参数特别少,基本做到了全自动化调优,所以未来 ZGC 应该会是主流收集器.
五、三色标记算法
三色标记算法
在垃圾回收的标记阶段,垃圾回收器会用 3 种颜色来表示对象的标记状态,这种方式称为三色标记。
三色的含义:
- 白色:表示尚未被垃圾回收器访问过的对象。在垃圾回收开始阶段,所有对象初始都被标记为白色。
- 灰色:表示对象本身已经被垃圾回收器访问过,但该对象中直接引用的对象还没有被全部访问
- 黑色:表示对象本身及其直接引用的所有对象都已经被垃圾回收器访问过。黑色对象代表存活对象,在本次垃圾回收过程中不会被回收。
三色标记的工作原理:
- 初始标记时,所有对象初始化为白色。然后垃圾回收器从 gc root 开始访问,将直接子节点全部标记为灰色。
- 并发标记时访问所有灰色对象,将灰色对象的直接子节点标记为灰色,当灰色对象的所有直接子节点都标记为灰色后,该灰色对象标记为黑色,代表该对象的直接引用都已经访问过。
- 反复重复步骤 2,直到所有灰色对象都标记成黑色,标记为黑色的对象就代表是存活对象
- 第三步结束后,最后剩下的白色对象全部都视为垃圾。
多标、漏标
在并发标记阶段,因为用户线程仍在运行,可能会有新增对象,这时候所有新增的对象都会被当作存活对象标记为黑色,同时也m有可能对原有对象进行修改,修改原有对象的时候就可能出现两个问题:
- 多标:
已经标记为存活的对象在并发标记阶段丢失引用,变成了垃圾对象,但其对象身上的标记仍为存活。这种对象称为浮动垃圾,有浮动垃圾其实还好,无非就是本次清理不掉,占用一些空间,但是下一轮 GC 就可以将其清除。 - 漏标:
之前判定是垃圾的对象在并发标记阶段已经复活,但对象身上没有存活标记。这种问题就比较严重,因为明明是有用的对象,最后却会被当作垃圾回收掉。这种漏标的问题是一定要处理的,解决方案因垃圾回收器的不同而不同:- CMS:增量更新(Incremental Update)
- G1:原始快照(Snapshot At The Beginning,SATB)
- ZGC:染色指针
增量更新
增量更新就是当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。
原始快照
原始快照是删除对象的引用关系时, 将这个删除的引用记录下来, 并记录为灰色,在并发标记结束之后, 再对这些灰色对象进行重新扫描(目的就是让这种对象在本轮 gc 清理中能存活下来,待下一轮gc 的时候重新扫描,这个对象也有可能是浮动垃圾)
读屏障和写屏障
指在赋值或读取操作前后,加入一些处理,类似于 AOP。
记忆集和卡表
记忆集用来解决年龄分代收集器中的跨代引用问题,假设年轻代中有一个对象 A 被老年代引用,这时候如果发生 minor gc,则 A 会被判定为不可达的垃圾对象。而记忆集就是在新生代中建立一个集合,用来保存那些被老年代引用了的对象关系。这样 minor gc 时,只需要扫描新生代和记忆集就可以判定 A 为存活对象。
卡表是对记忆集的具体实现。它的本质是先将老年代编号成 N 个区域,每个区域称为卡页。然后在新生代保存一个数组,称为卡表,卡表的索引、长度和卡页的编号、数量相互对应,当老年代引用了新生代的对象 A,则就把老年代对应的卡页编号当作下标,在卡表中添加一条数据。当 minor gc 检查卡表时,发现有数据就会找到对应编号的卡页并扫描。