JVM垃圾回收

一、几个理论

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) 遍历流程

  1. GC Roots 出发,把可达对象标为灰色
  2. 遍历灰色对象的引用,引用到的对象也标灰色
  3. 一个灰色对象所有子引用遍历完,升级成黑色
  4. 最后所有白色对象 = 可回收垃圾

(3) 并发标记隐患:漏标

业务线程和 GC 线程同时跑,会出现这种情况:

  1. 黑色对象已经扫描完毕
  2. 业务线程修改引用:黑色对象新增引用白色对象
  3. 同时断掉灰色对象对该白色对象的引用

结果:白色对象没人再扫描,本该存活却被当成垃圾回收漏标

(4) 漏标问题的补救方案

增量更新:只要出现 黑色对象引用白色对象,就记录这个黑色对象,最后重新标记再扫一遍,防止漏标。

SATB:标记开始先拍引用链快照,不管中途引用怎么断,都按快照算可达,断开引用的对象强制当成存活

4.增量更新

(1) 定义

增量更新是 CMS 并发标记使用的三色标记补救方案。CMS 垃圾收集器并发标记阶段用的快照规则:

当业务线程把一个已标记的存活对象,引用指向一个未标记的白色垃圾对象 时,把这条新引用记录下来,后续重新标记时修正,防止漏标。

(2) 三色标记

  • 黑色:已标记完、存活、扫描完毕
  • 灰色:正在扫描、自身已标完,下属引用还没遍历
  • 白色:未标记、暂时看成垃圾

(3) 漏标场景(并发标记最大问题)

并发标记时,GC 线程在跑,业务线程也在改引用:

  1. 黑色对象 A 已经扫描完
  2. 业务线程把 A 的引用,从 B 改成引用 C
  3. 同时把原来指向 C 的引用断开

结果:C 变成没人能扫描到的白色对象 ,本该存活却被当成垃圾回收 → 浮动垃圾 / 误回收

(4) 增量更新怎么解决

规则:只要黑色对象 新增了 对白色对象的引用,就把这个黑色对象记录起来,放入集合。

等进入 重新标记阶段 :以这些被记录的黑色对象为,重新扫描一遍,把漏标的白色对象重新标成灰色、黑色,避免误回收。

(5) 核心特性

  • 关注:黑新增引用白 这种行为
  • 逻辑:记录黑色对象,后续再重新扫描
  • 只补救新增引用这一种漏标情况
  • CMS 专用

5. SATB(Snapshot At The Beginning)原始快照

(1) 定义

SATB:在并发标记一开始,就给整个堆对象引用链拍一张「原始快照」。 后续不管业务线程怎么改引用,GC 只按最开始那张快照做可达性分析,保证不会漏标。

(2) 并发标记的致命问题

并发标记时,业务线程同时修改引用:

  1. 灰色对象引用断掉 白色对象 C
  2. 黑色对象又不引用 C
  3. 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) 流程

  1. 标记:从 GC Roots 遍历,标记存活对象
  2. 清除:把未标记的垃圾直接清理

(2) 优点

逻辑最简单

(3) 缺点

  • 产生大量内存碎片
  • 只能用空闲列表分配对象
  • 碎片多,大对象容易分配失败

(4) 适用

老年代

2. 复制算法

(1) 流程

  1. 把内存分成大小相等两块:From、To
  2. 只使用其中一块,满了就把存活对象复制到另一块
  3. 清空原整块内存,互换角色

(2) 优点

  • 无内存碎片,内存永远规整
  • 可用指针碰撞分配,速度极快

(3) 缺点

  • 浪费一半内存空间
  • 存活对象多的时候,复制开销大

(4) 适用

新生代(存活对象少、垃圾多)

3.标记 - 整理

(1) 流程

  1. 标记:标记所有存活对象
  2. 整理 :把存活对象向一端压缩移动,紧凑排列
  3. 末尾整块空闲

(2) 优点

  • 无碎片
  • 不浪费内存空间

(3) 缺点

  • 需要移动对象、更新引用,开销比清除大
  • 停顿比标记清除长

(4) 适用

老年代(存活对象多、不适合复制算法)

4. 分代收集算法

JVM实际使用的综合算法,根据对象存活周期分代:新生代对象存活时间短,用复制算法;老年代对象存活率高、无额外空间担保,用标记清除或标记整理。

PS:新生代 GC 过程?

新生代分为一块 Eden + 两块 Survivor(From / To)。

  1. 绝大多数对象在 Eden 创建
  2. Eden 满触发 MinorGC
  3. 存活对象复制到 To Survivor
  4. 清空 Eden 和 From
  5. 交换 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 并发标记清除

  • 目标:低延迟
  • 算法:标记 - 清除(有内存碎片)
  • 漏标处理:增量更新
  • 四大阶段:
    1. 初始标记(STW 很短)
    2. 并发标记(业务线程并发)
    3. 重新标记(STW 很短)
    4. 并发清除
  • 缺点:内存碎片、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:独立收集,无需配对
相关推荐
AC赳赳老秦1 小时前
文案策划提效:OpenClaw批量生成活动文案、宣传海报配文,适配不同渠道调性
java·大数据·服务器·人工智能·python·deepseek·openclaw
_codemonster1 小时前
系统分析师系列目录
java·网络·数据库
带刺的坐椅2 小时前
Spring AI 2.0 GA 倒计时:先别急,来看看 Java AI 框架的另一条路
java·spring·ai·llm·agent·solon
沉下去,苦磨练!2 小时前
python的全局解释器锁(GIL)到垃圾回收机制
jvm
TE-茶叶蛋2 小时前
Java 8 引入的Stream API-stream()
java·windows·python
Stream_Silver2 小时前
【 libusb4java实战:跨平台USB设备通信完全指南】
java·笔记·嵌入式硬件·microsoft
极光代码工作室2 小时前
基于SpringBoot的宿舍管理系统
java·springboot·web开发·后端开发
Ting-yu2 小时前
SpringCloud快速入门(8)---- OpenFeign(远程调用)
java·spring·spring cloud
Co_Hui2 小时前
JVM 内存结构
jvm