一、几个理论
1. 引用计数法
(1) 核心原理
给每个对象维护一个引用计数器:
- 有一个引用指向它,计数器 +1
- 引用失效 / 置为 null,计数器 -1
- 当计数器 = 0 ,说明没人用了,可立即回收
(2) 优点
- 回收及时:计数为 0 立刻回收,不用等 GC 全局扫描
- 停顿时间低:不用全局遍历所有对象
- 逻辑简单、好理解
(3) 缺点
-
**无法解决循环引用问题(最大硬伤):**两个对象互相引用,计数器都永远不为 0,永远无法被回收,造成内存泄漏。
-
**频繁加减计数器,开销大:**对象赋值、销毁都要实时修改计数器,并发场景下还要保证原子性,性能损耗高。
-
额外占用内存 ,每个对象都要单独存一个引用计数值,浪费对象头空间。
2. 可达性分析
(1) 核心原理
JVM 不靠引用计数 ,而是用可达性分析 :以 GC Roots 作为起始根节点,向下遍历整个引用链 ,能遍历到的对象就是存活对象 ,遍历不到的就是垃圾对象,可被回收。
(2) 执行流程
- 暂停所有用户线程(STW)
- 从 GC Roots 开始遍历引用链
- 标记所有可达存活对象
- 清理未被标记的垃圾对象
- 恢复用户线程
PS:可达性分析一定是STW吗?
- 可达性分析不是天生必须全程 STW;
- 早期串行 GC 为了保证引用链遍历准确,需要 STW;
- 现代并发 GC 通过增量更新 (CMS)、SATB 原始快照(G1、ZGC、Shenandoah),让可达性分析和业务线程并发运行,只需短暂极小停顿,不需要全程 STW。
(3) GC Roots 有哪些
- 虚拟机栈中引用的对象
- 本地方法栈 Native 引用对象
- 方法区静态变量、常量引用对象
- 活跃线程引用对象
(4) 优缺点
完美解决循环引用 问题,是 JVM 默认的垃圾回收判定算法;缺点是需要 STW 暂停用户线程。
3. 三色标记
三色标记是并发可达性分析的标记逻辑,把堆中所有对象分成三种颜色,用来遍历引用链、区分存活 / 垃圾,配合 CMS、G1、ZGC 并发标记使用。
(1) 三色定义
- 白色 White :还没被扫描,暂时判定为垃圾
- 灰色 Gray :自身已被标记存活,但下属引用还没遍历完
- 黑色 Black :自身已标记 + 所有引用全部遍历完毕,确定存活
记忆口诀:白 = 没扫,灰 = 正在扫,黑 = 扫完了
(2) 遍历流程
- 从 GC Roots 出发,把可达对象标为灰色
- 遍历灰色对象的引用,引用到的对象也标灰色
- 一个灰色对象所有子引用遍历完,升级成黑色
- 最后所有白色对象 = 可回收垃圾
(3) 并发标记隐患:漏标
业务线程和 GC 线程同时跑,会出现这种情况:
- 黑色对象已经扫描完毕
- 业务线程修改引用:黑色对象新增引用白色对象
- 同时断掉灰色对象对该白色对象的引用
结果:白色对象没人再扫描,本该存活却被当成垃圾回收 → 漏标
(4) 漏标问题的补救方案
增量更新:只要出现 黑色对象引用白色对象,就记录这个黑色对象,最后重新标记再扫一遍,防止漏标。
SATB:标记开始先拍引用链快照,不管中途引用怎么断,都按快照算可达,断开引用的对象强制当成存活
4.增量更新
(1) 定义
增量更新是 CMS 并发标记使用的三色标记补救方案。CMS 垃圾收集器在并发标记阶段用的快照规则:
当业务线程把一个已标记的存活对象,引用指向一个未标记的白色垃圾对象 时,把这条新引用记录下来,后续重新标记时修正,防止漏标。
(2) 三色标记
- 黑色:已标记完、存活、扫描完毕
- 灰色:正在扫描、自身已标完,下属引用还没遍历
- 白色:未标记、暂时看成垃圾
(3) 漏标场景(并发标记最大问题)
并发标记时,GC 线程在跑,业务线程也在改引用:
- 黑色对象 A 已经扫描完
- 业务线程把 A 的引用,从 B 改成引用 C
- 同时把原来指向 C 的引用断开
结果:C 变成没人能扫描到的白色对象 ,本该存活却被当成垃圾回收 → 浮动垃圾 / 误回收。
(4) 增量更新怎么解决
规则:只要黑色对象 新增了 对白色对象的引用,就把这个黑色对象记录起来,放入集合。
等进入 重新标记阶段 :以这些被记录的黑色对象为根,重新扫描一遍,把漏标的白色对象重新标成灰色、黑色,避免误回收。
(5) 核心特性
- 关注:黑新增引用白 这种行为
- 逻辑:记录黑色对象,后续再重新扫描
- 只补救新增引用这一种漏标情况
- CMS 专用
5. SATB(Snapshot At The Beginning)原始快照
(1) 定义
SATB:在并发标记一开始,就给整个堆对象引用链拍一张「原始快照」。 后续不管业务线程怎么改引用,GC 只按最开始那张快照做可达性分析,保证不会漏标。
(2) 并发标记的致命问题
并发标记时,业务线程同时修改引用:
- 灰色对象引用断掉 白色对象 C
- 黑色对象又不引用 C
- C 直接变成白色,找不到、被误回收
这就是 对象消失、漏标。
(3) SATB怎么解决
核心规则:
以「并发标记开始那一刻」的引用关系为准,做快照。 哪怕运行中引用被改掉、断开了,GC 依然认为原来的引用还存在,照样把这个白色对象当成可达、标记为存活。
实现做法:
当引用被删除 / 断开 时:把被断开的那个白色对象 记录下来,最后强制当作存活对象重新扫描,避免被误回收。
(4) 优缺点
优点:
- 完美解决并发标记漏标问题
- 逻辑比增量更新更简洁
- 适合分代、Region 式收集器(G1)
缺点:**浮动垃圾更多。**明明引用已经断了,还被快照保留活到下一次 GC。
(5) 增量更新和SATB区别
| 特性 | 增量更新(CMS) | SATB 原始快照(G1/ZGC/Shenandoah) |
|---|---|---|
| 关注点 | 关注新增引用 | 关注删除引用 |
| 解决思路 | 黑引用白 → 记录黑色对象,重扫 | 引用被断开 → 保留快照,强制当存活 |
| 漏标处理 | 补救新引用 | 保留旧快照 |
| 产生垃圾 | 浮动垃圾少 | 浮动垃圾更多 |
| 适用收集器 | CMS | G1、ZGC、Shenandoah |
6. 四种引用类型
(1) 强引用(默认)
写法 :正常 Object o = new Object()
特点:
- 最常用、默认引用
- 只要强引用链可达 ,永远不 GC,宁愿 OOM 也不回收
- 容易内存泄漏(静态集合持有对象不放空)
回收条件 :手动 o = null 断掉引用链
(2) 软引用 SoftReference( 适合缓存**)**
写法 :SoftReference<T>
规则:
- 内存充足:不回收
- 内存紧张、快要 OOM:触发 GC 回收
场景:图片缓存、页面缓存、大对象缓存
一句话 :没事留着,不够再清
(3) 弱引用 WeakReference( ThreadLocal、缓存常用**)**
写法 :WeakReference<T>
规则:
- 只要发生 GC,不管内存够不够,直接回收
场景:
- WeakHashMap
- ThreadLocal 的 Entry key
- 临时缓存、防内存泄漏
一句话 :一次 GC 就没
(4) 虚引用 PhantomReference
写法 :PhantomReference<T> + ReferenceQueue
特点:
- 不能 get 拿到对象
- 唯一作用:对象被 GC 回收时收到通知
- 必须绑定引用队列
场景:NIO 堆外内存释放、资源回收监控、对象销毁回调
一句话 :拿不到对象,只做回收通知
二、垃圾回收算法
1. 标记 - 清除(Mark-Sweep)
(1) 流程
- 标记:从 GC Roots 遍历,标记存活对象
- 清除:把未标记的垃圾直接清理
(2) 优点
逻辑最简单
(3) 缺点
- 产生大量内存碎片
- 只能用空闲列表分配对象
- 碎片多,大对象容易分配失败
(4) 适用
老年代
2. 复制算法
(1) 流程
- 把内存分成大小相等两块:From、To
- 只使用其中一块,满了就把存活对象复制到另一块
- 清空原整块内存,互换角色
(2) 优点
- 无内存碎片,内存永远规整
- 可用指针碰撞分配,速度极快
(3) 缺点
- 浪费一半内存空间
- 存活对象多的时候,复制开销大
(4) 适用
新生代(存活对象少、垃圾多)
3.标记 - 整理
(1) 流程
- 标记:标记所有存活对象
- 整理 :把存活对象向一端压缩移动,紧凑排列
- 末尾整块空闲
(2) 优点
- 无碎片
- 不浪费内存空间
(3) 缺点
- 需要移动对象、更新引用,开销比清除大
- 停顿比标记清除长
(4) 适用
老年代(存活对象多、不适合复制算法)
4. 分代收集算法
JVM实际使用的综合算法,根据对象存活周期分代:新生代对象存活时间短,用复制算法;老年代对象存活率高、无额外空间担保,用标记清除或标记整理。
PS:新生代 GC 过程?
新生代分为一块 Eden + 两块 Survivor(From / To)。
- 绝大多数对象在 Eden 创建
- Eden 满触发 MinorGC
- 存活对象复制到 To Survivor
- 清空 Eden 和 From
- 交换 From/To 角色,反复轮换
PS:为什么必须两个 Survivor?
如果只有一块 Survivor:复制存活对象时无处存放 ,会产生内存碎片、无法用复制算法。双 Survivor 保证:全程无碎片、复制成本最低、回收效率最高。
三、垃圾收集器
1. 新生代收集器
(1) Serial(串行 GC)
- 算法:复制算法
- 单线程、全程 STW
- 适合:客户端、内存小、单机程序
(2) ParNew
- Serial 多线程版
- 算法:复制算法
- 唯一能和 CMS 配合 的新生代收集器
(3) Parallel Scavenge
- 吞吐量优先
- 算法:复制算法
- 关注:最大化运行业务代码时间,不追求低延迟
2.老年队收集器
(1) Serial Old
- 单线程、STW
- 算法:标记 - 整理
- 给 Serial 做搭档
(2) Parallel Old
- 多线程、吞吐量优先
- 算法:标记 - 整理
- 和 Parallel Scavenge 配对
(3) CMS 并发标记清除
- 目标:低延迟
- 算法:标记 - 清除(有内存碎片)
- 漏标处理:增量更新
- 四大阶段:
- 初始标记(STW 很短)
- 并发标记(业务线程并发)
- 重新标记(STW 很短)
- 并发清除
- 缺点:内存碎片、CPU 占用高、浮动垃圾多
3.全局收集器(不分新生代老年代)
(1) G1
- 堆拆分成多个 Region
- 整体:标记 - 整理
- 局部:复制算法
- 漏标:SATB 原始快照
- 可指定预期停顿时间
- 兼顾 吞吐量 + 低延迟
- JDK9 默认
(2) ZGC
- JDK11 正式引入
- 主打 毫秒级超低停顿
- 染色指针、读屏障
- 支持 TB 级大堆,几乎无 STW
(3) Shenandoah
- 低延迟、并发压缩
- 比 G1 停顿更低,和 ZGC 对标
4. 收集器固定配对(必背)
- Serial + Serial Old
- ParNew + CMS
- Parallel Scavenge + Parallel Old
- G1 / ZGC / Shenandoah:独立收集,无需配对