深入理解Java虚拟机之垃圾收集器篇(垃圾回收器的深入解析待完成TODO)
目录
- **一. 如何判断对象的存亡**
- 引用计数算法:
- 可达性分析算法:
- **二. Java中的四种引用**
- **三. 垃圾回收算法**
- **1. 标记 - 清除算法**
- **2. 标记 - 复制算法**
- **3. 标记 - 整理算法**
- **4. 分代收集理论**(了解即可)
- **四. 十种主流垃圾收集器**
- **3.1 Serial 收集器**
- **3.2 ParNew 收集器**
- **3.3 Parallel Scavenge 收集器(吞吐量优先)**
- **3.4 CMS(Concurrent Mark Sweep)收集器(低延迟优先)**
- **3.5 G1(Garbage-First)收集器(JDK9默认)**
- **3.6 ZGC(Z Garbage Collector)**
- **3.7 Shenandoah 收集器**
- **五. 记忆集与卡表**
- 1. 记忆集
- 2. 卡表
- 3. 写屏障
- **六. 并发的可达性分析**
- **三色标记法**:
- 并发扫描时的"对象消失"问题:
一. 如何判断对象的存亡
引用计数算法:
定义: 在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的.
缺点: 无法解决循环引用的问题:
A和B中互相有对方的引用, 因此垃圾回收器认为它们是不可回收的.
可达性分析算法:
定义: 通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,则证明此对象是不可能再被使用的.
GC Roots: (来自原文)
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
- 所有被同步锁(synchronized关键字)持有的对象。
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
二. Java中的四种引用
分别为: 强引用、软引用、弱引用 和 虚引用4种,这4种引用强度依次逐渐减弱。
- 强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
- 软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。
- 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。
- 虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用。
三. 垃圾回收算法
1. 标记 - 清除算法
-
流程:标记存活对象 → 清除未标记对象。
-
优点:简单直接。
-
缺点:
-
效率低:标记和清除阶段耗时。
-
内存碎片化:可能导致大对象分配失败。
-
2. 标记 - 复制算法
-
流程:将内存分为两块,存活对象复制到另一块后清空原块。
-
优化:HotSpot采用Appel式回收,新生代划分为Eden(80%)、From Survivor(10%)、To Survivor(10%)。Minor GC时,Eden和From中的存活对象复制到To,随后清空Eden和From。
-
优点:无碎片,适合新生代。
-
缺点:空间利用率低(仅50%)。
3. 标记 - 整理算法
-
流程:标记存活对象 → 移动到内存一端 → 清理边界外内存。
-
优点:减少碎片,适合老年代。
-
缺点:对象移动需暂停用户线程(STW)。
4. 分代收集理论(了解即可)
- 核心假说:
- 弱分代假说:大多数对象生命周期短(新生代)。
- 强分代假说:存活时间越长的对象越难回收(老年代)。
- 实现:将堆划分为新生代(Young)和老年代(Old),分别采用不同算法(如复制算法 vs 标记-整理)。
- 跨代引用问题:通过记忆集(后面会解释)记录跨代引用关系,减少扫描范围。
四. 十种主流垃圾收集器
各款经典收集器之间的关系如图3-6所示。
3.1 Serial 收集器
- 特点:单线程,适用于单核CPU或小型应用。
- 算法:新生代用复制算法,老年代用标记-整理。
- 缺点:STW时间长,不适合多核环境。
- 适用场景:客户端模式(如Java Web Start)或资源受限环境。
- 参数:
-XX:+UseSerialGC
。
3.2 ParNew 收集器
- 特点:Serial的多线程版本,配合CMS使用。
- 算法:新生代复制算法。
- 优点:多线程并行,减少停顿时间。
- 缺点:老年代需配合CMS,配置复杂。
- 参数:
-XX:+UseParNewGC
(需与CMS搭配)。
3.3 Parallel Scavenge 收集器(吞吐量优先)
- 特点:多线程,关注吞吐量(运行用户代码时间占比)。
- 算法:新生代复制算法,老年代标记-整理。
- 调优参数:
-XX:MaxGCPauseMillis
:控制GC停顿时间。-XX:GCTimeRatio
:设置吞吐量目标(默认99%)。
- 适用场景:后台计算任务(如科学计算)。
3.4 CMS(Concurrent Mark Sweep)收集器(低延迟优先)
- 特点:并发收集,追求最短停顿时间。
- 工作流程:
- 初始标记(STW):标记GC Roots直接关联对象。
- 并发标记:与用户线程并行标记存活对象。
- 重新标记(STW):修正并发标记期间的遗漏。
- 并发清除:与用户线程并行清除无用对象。
- 优点:低延迟,适合Web服务器。
- 缺点:
- 内存碎片:无法回收浮动垃圾(并发阶段产生的新垃圾)。
- CPU资源敏感:默认占用25% CPU。
- 可能触发“Concurrent Mode Failure”:若老年代空间不足,需降级为Serial Old。
3.5 G1(Garbage-First)收集器(JDK9默认)
- 特点:分区化堆管理,兼顾吞吐量和低延迟。
- 分区策略:将堆划分为多个Region(默认2MB~32MB),优先回收垃圾最多的Region。
- 工作流程:
- 初始标记(STW):标记GC Roots。
- 并发标记:标记存活对象。
- 最终标记(STW):修正标记。
- 筛选回收(STW):清理Region中的垃圾。
- 优点:
- 可预测的停顿时间(通过
-XX:MaxGCPauseMillis
设置)。 - 降低内存碎片风险。
- 适合大内存应用(如GB级堆)。
- 可预测的停顿时间(通过
- 适用场景:现代服务器应用(如大数据处理、分布式系统)。
3.6 ZGC(Z Garbage Collector)
- 特点:超低延迟(<10ms),支持TB级堆。
- 技术:基于染色指针和读屏障,实现并发标记和清理。
- 优点:
- 停顿时间与堆大小无关。
- 支持大内存应用(如云计算、实时系统)。
- 缺点:需要较新JDK版本(JDK11+)和硬件支持。
- 参数:
-XX:+UseZGC
。
3.7 Shenandoah 收集器
- 特点:低延迟,堆大小无关。
- 技术:基于Brooks指针和读屏障,实现并发整理。
- 优点:
- 停顿时间短(<1s)。
- 适合大内存应用。
- 缺点:对小堆性能可能不如G1。
- 参数:
-XX:+UseShenandoahGC
。
五. 记忆集与卡表
1. 记忆集
**定义: **记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构. 为解决跨代引用问题, 引入记忆集用以避免把整个老年代加进GC Roots扫描范围.
作用: 在垃圾收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了,并不需要了解这些跨代指针的全部细节
**优点: **在垃圾收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了,并不需要了解这些跨代指针的全部细节
**缺点: **空间占用 与 维护成本 高昂
2. 卡表
定义: 卡表是记忆集的一种精度,
卡表是怎么作用的:
字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作“卡页”.
一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0.
在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描.
**缺点: 在高并发情况下会出现"伪共享"**问题.(伪共享: 多个线程并发写入同一个Page Cache导致数据失效, 线程私有内存多次写回, 浪费性能)
3. 写屏障
**定义: **在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的。
写屏障可以看作在虚拟机层面对**“引用类型字段赋值”这个动作的AOP切面**,在引用对象赋值时会产生一个环形通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴内。在赋值前的部分的写屏障叫作写前屏障,在赋值后的则叫作写后屏障.
六. 并发的可达性分析
三色标记法:
- 白色:
表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。 - 黑色:
表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。 - 灰色:
表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。
作用: 方便地进行对象图遍历的分析
这里引用书本原图来更清晰地解释三色标记法:
并发扫描时的"对象消失"问题:
当且仅当以下两个条件同时满足时,会产生“对象消失”的问题,即原本应该是黑色的对象被误标为白色:
- 赋值器插入了一条或多条从黑色对象到白色对象的新引用;
- 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。
**解决方法: **
-
增量更新:
增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次.
即: 黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了
-
原始快照:
原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。
即: 无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。
应用: CMS是基于增量更新来做并发标记的,G1、Shenandoah则是用原始快照来实现。