如果判断对象可以回收
1. 引用计数法 (Reference Counting)
- 原理:
每个对象关联一个引用计数器。当被引用时 +1,引用失效时 -1。计数器为 0 时判定对象可回收。 - 缺陷:
❗ 循环引用问题(如对象 A 引用 B,B 又引用 A),即使二者已无外界引用,计数器仍不为 0,导致 内存泄露。 - 现状:
未被 JVM 采用(Python、PHP 等语言使用)。
2. 可达性分析算法 (Reachability Analysis)
- 原理:
以 GC Roots 为起点,向下搜索引用链。不在任何引用链上的对象(即 不可达)判定可回收。 - GC Roots 包括:
- 虚拟机栈中的局部变量(如当前方法中的对象引用)
- 方法区中静态变量、常量(如
public static final Object
) - JNI 引用的本地方法对象
- 已启动且未终止的线程
- 优势:
彻底解决 循环引用问题(JVM 核心算法)。
流程:GC Roots → 引用链 → 对象可达性判定 → 不可达对象标记回收。
3. 四种引用类型
引用强度决定对象回收策略(强度:强 > 软 > 弱 > 虚):
引用类型 | 实现类 | 回收条件 | 典型场景 |
---|---|---|---|
强引用 (Strong) | Object obj = new Object() |
永不回收(除非引用断开 obj = null ) |
普通对象创建 |
软引用 (Soft) | SoftReference<T> |
内存不足时回收(OOM 前触发) | 缓存(如图片缓存) |
弱引用 (Weak) | WeakReference<T> |
下次 GC 必回收(无论内存是否足够) | ThreadLocal 、缓存 |
虚引用 (Phantom) | PhantomReference<T> |
无法获取对象,仅用于跟踪垃圾回收状态(必须配合 ReferenceQueue 使用) |
堆外内存管理(如 NIO) |
关键特性:
- 软/弱/虚引用 必须配合
ReferenceQueue
(引用队列)使用,用于在回收后通知系统。- 虚引用 的
get()
永远返回null
,仅通过队列感知对象被回收。
垃圾回收算法
1. 标记-清除(Mark-Sweep)
- 核心:标记存活对象 → 清除未标记对象。
- 优缺点:
- ✅ 无对象移动,实现简单。
- ❌ 内存碎片化,大对象分配困难。
2. 复制算法(Copying)
- 核心:内存分两块 → 存活对象复制到空白区 → 清空原区。
- 优缺点:
- ✅ 无内存碎片,分配高效。
- ❌ 浪费50%内存,存活率高时效率低。
3. 标记-整理(Mark-Compact)
- 核心:标记存活对象 → 向一端移动 → 清理边界外空间。
- 优缺点:
- ✅ 无碎片且内存利用率高。
- ❌ 对象移动耗时(STW暂停长)。
分代垃圾回收
1、内存分区
代区 | 内容 | 特性 |
---|---|---|
新生代 | Eden(新对象) | 空间小,回收频繁 |
Survivor(存活对象) | S0/S1轮换复制 | |
老年代 | 长期存活对象 & 大对象 | 空间大,回收频率低 |
元空间 | 类元数据(方法区) | Java8+ 代替永久代 |
2、核心流程(四步循环)
新对象分配(Eden区)
- 所有新对象优先分配在Eden区
- 若Eden满 → 触发Minor GC
新生代回收(Minor GC)
- 触发:Eden区空间不足
- 操作:
- 扫描Eden + 存活Survivor区
- 标记存活对象 → 复制到空闲Survivor区
- 对象年龄+1(初始为0)
- 清空Eden + 原Survivor区
- 耗时:毫秒级(STW短暂)
对象晋升(到老年代)
满足以下条件时晋升:
if (对象年龄 >= 15) → 晋升 // -XX:MaxTenuringThreshold
if (Survivor空间不足) → 晋升
if (大对象) → 直接入老年代 // -XX:PretenureSizeThreshold
全局回收(Full GC)
- 触发条件:
- 老年代空间不足
- 元空间不足
- 显式调用
System.gc()
- 操作:
- 回收整个堆+元空间
- 采用标记-清除/标记-压缩算法
- 耗时:秒级(严重STW)
3、关键特点
- 年龄计数:每存活1次Minor GC年龄+1
- Survivor轮换:S0/S1交替作为"目标区"(复制算法核心)
- 晋升阈值:默认15次(可调)
- 担保机制:
- 老年代为新生代分配担保
- 若老年代无法容纳晋升对象 → 触发Full GC
一句话总结流程:
新对象 → Eden区 → Minor GC存活 → Survivor轮转 → 年龄达标 → 晋升老年代 → 老年代满 → Full GC回收
垃圾回收器
1、单线程组合:最小化资源占用
目标:资源敏感型场景(如客户端、微服务),牺牲停顿时间换取最小内存/CPU开销。
收集器 | 代 | 算法 | 线程模型 | 特点 |
---|---|---|---|---|
Serial | 新生代 | 复制 | 单线程 | 全程 STW,简单高效,JDK 默认客户端模式收集器(-client )。 |
Serial Old | 老年代 | 标记-整理 | 单线程 | Serial 的老年代版,CMS 失败时的后备方案。 |
组合:Serial
(新生代) + Serial Old
(老年代)
适用场景:
- 内存 < 100MB 的轻量级应用(嵌入式、IoT)。
- 对停顿时间不敏感(如后台定时任务)。
配置参数:
-XX:+UseSerialGC # 显式启用
2、高吞吐组合:最大化 CPU 利用率
目标:后台计算密集型任务(如批处理),追求单位时间内GC总时间最短(吞吐量 > 99%)。
收集器 | 代 | 算法 | 线程模型 | 特点 |
---|---|---|---|---|
Parallel Scavenge | 新生代 | 复制 | 多线程 | 吞吐量优先,精准控制 MaxGCPauseMillis (最大停顿时间)和 GCTimeRatio (吞吐占比)。 |
Parallel Old | 老年代 | 标记-整理 | 多线程 | Parallel Scavenge 的老年代搭档。 |
组合:Parallel Scavenge
(新生代) + Parallel Old
(老年代)
适用场景:
- 数据计算、科学运算等长时间运行任务。
- JDK 8 默认垃圾收集器。
配置参数:
-XX:+UseParallelGC -XX:+UseParallelOldGC # 显式启用
-XX:MaxGCPauseMillis=100 # 目标停顿时间(毫秒)
-XX:GCTimeRatio=99 # GC时间占比(1/(1+99)=1%)
3、低延迟组合:最小化停顿时间
目标:响应敏感型系统(如Web服务),追求单次GC停顿最短(通常 < 100ms)。
收集器 | 代 | 算法 | 线程模型 | 特点 |
---|---|---|---|---|
ParNew | 新生代 | 复制 | 多线程 | Serial 的多线程版,唯一支持与 CMS 搭配的新生代收集器。 |
CMS | 老年代 | 标记-清除 | 并发 | 并发标记清除,分4阶段: 1. 初始标记 (STW) 暂停应用,标记GC Roots直接引用的老年代对象。耗时极短。 2. 并发标记 (并发) 与应用线程并行执行,深度遍历标记所有存活对象。耗时最长(可能产生浮动垃圾)。 3. 重新标记 (STW) 二次暂停,修正并发标记阶段因应用运行导致的标记错误。耗时中等。 4. 并发清除 (并发) 与应用线程并行清理死亡对象,释放内存(不压缩空间→可能产生碎片)。 |
组合:ParNew
(新生代) + CMS
(老年代)
适用场景:
- 电商实时接口、在线服务(要求响应快)。
- 需避免 CMS 的 内存碎片和 并发失败风险。
配置参数:
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC # 显式启用
-XX:CMSInitiatingOccupancyFraction=70 # 老年代70%时触发CMS
-XX:+UseCMSCompactAtFullCollection # Full GC时压缩碎片(默认启用)
4、G1
G1(Garbage First)垃圾回收器
G1垃圾收集器在JDK7被开发出来,JDK8功能基本完全实现。并且成功替换掉了Parallel Scavenge成为了服务端模式下默认的垃圾收集器。对比起另外一个垃圾回收器CMS,G1不仅能提供能提供规整的内存,而且能够实现可预测的停顿,能够将垃圾回收时间控制在N毫秒内。这种“可预测的停顿”和高吞吐量特性让G1被称为"功能最全的垃圾回收器"。
G1同时回收新生代和老年代,但是分别被称为G1的Young GC模式和Mixed GC模式。这个特性来源于G1独特的内存布局,内存分配不再严格遵守新生代,老年代的划分,而是以Region为单位,G1跟踪各个Region的并且维护一个关于Region的优先级列表。在合适的时机选择合适的Region进行回收。这种基于Region的内存划分为一些巧妙的设计思想提供了解决停顿时间和高吞吐的基础。
Region内存划分
Region 设计
- 堆划分规则
- 堆内存被切割为 大小固定 的 Region(每个 Region 为 1MB~32MB)
- 自动计算:Region大小 = 堆大小 ÷ 2048(取最接近的 2^n)
- 手动设置:
-XX:G1HeapRegionSize=32m
Region 的四种类型
类型 | 用途 | 特点 |
---|---|---|
Eden Region | 分配新对象 | 占用比最高(默认 5%~60%) |
Survivor Region | 存放 Young GC 后存活的对象 | 晋升年龄达到阈值后进入 Old |
Old Region | 存储长期存活的垃圾对象 | 通过 Mixed GC 回收 |
Humongous Region | 存储巨型对象(> Region 50%) | 直接分配在 Old 区且连续占用 |
关键逻辑
- 角色动态切换:GC 后 Region 类型可被重置(如回收的 Eden 转为 Survivor)
- 巨型对象:单对象跨多个 Region 时需连续空间(分配失败可能触发 Full GC)
- 无物理分代:Eden/Old 由分散的 Region 组成(非连续内存)
Region内部结构
Card Table(卡表)
作用:
- 粗粒度标记:跟踪堆中哪些 512字节的小内存块(Card) 被修改并可能包含跨 Region 引用。
- 触发机制:通过 写屏障(Write Barrier) 实现(修改对象引用字段时触发)。
原理:
- 堆逻辑划分为 固定大小的 Card(如 512字节)。
- 写屏障检测到 跨 Region 引用写入 时,标记对应 Card 为 脏(Dirty)(记录在全局字节数组)。
- 不记录细节:仅标记可能包含引用的 Card,不记录具体对象或目标 Region。
价值:
- 低开销捕获修改位置,为 RSet 提供脏 Card 源头信息。
Remembered Set(RSet)
- 作用:
- 精确保存外部引用:每个 Region 私有,记录 哪些外部 Regions 的哪些 Card 引用了本 Region 的对象。
- 回答 GC 关键问题:“谁引用了当前 Region?”。
- 原理:
- 结构为 哈希表:记录
{ 来源 Region → [Card1, Card2, ...] }
。 - 由后台线程更新(如 Refinement 线程):
- 扫描 Card Table 中的 脏 Card(如 Region1-CardX)。
- 精扫脏 Card,找出其中 指向其他 Region 的具体引用(如 Region1-CardX → Region2 对象)。
- 将引用信息写入目标 Region 的 RSet(在
Region2 的 RSet
中记录:Region1-CardX 引用了 Region2
)。
- 结构为 哈希表:记录
- 价值:
- 回收 Region 时,直接查询其 RSet,仅需扫描 少量关联的外部 Card(避免全堆扫描)。
协同工作流程
- 写屏障:对象 A(Region1)引用对象 B(Region2) → 标记 Region1 的对应 Card 为 脏。
- 后台线程:
- 扫描脏 Card,发现
Region1-CardX → Region2
。- 更新 Region2 的 RSet:记录
Region1-CardX
引用了 Region2。
- GC 回收 Region2:
- 查询
Region2 的 RSet
→ 得知需扫描Region1-CardX
。- 仅扫描该 Card 确认引用(极大减少扫描范围)。
核心总结
组件 角色 粒度 关键能力 Card Table 脏卡记录簿 512字节 Card 低开销捕获跨 Region 写操作位置 RSet 外部引用通讯录 Region + Card 精准定位“谁引用我”,避免全堆扫描 协作本质:
卡表 捕获可疑位置 → RSet 精准记录跨区引用关系 → GC 按需极小范围扫描。
目的:实现 G1 高效分区回收与低停顿的核心基础设施。
三色标记算法
在可达性分析的思想指导下,我们需要标记对象是否可达,那么我们采用将对象标记为不同的颜色来区分对象是否可达。可以理解如果一个对象能从GC Roots出发并且遍历到,那么对象就是可达的,这个过程我们称为检查。
- 白色:对象还没被检查。
- 灰色:对象被检查了,但是对象的成员Field还没有被检查。
- 黑色:对象被检查了,对象的成员Fileld也被检查了。
那么整个检测的过程,就是从GC Roots出发不断地遍历对象,并且将可达的对象标记成黑色的过程。当标记结束时,还是白色的对象就是没被遍历到的对象,即不可达的对象。
举个例子
第一轮检查,找到所有的GC Roots,GC Roots被标记为灰色,有的GC Roots因为没有成员Field则被标记为黑色。
第二轮检查,检查被GC Roots引用的对象,并标记为灰色
第三轮检查,循环之前的步骤,将被标记为灰色对象的子Field检查。因为这里就假设了3次循环检查的对象,所以是最后一次检查。这一路检查结束,还是白色的对象就是可以被回收的对象。即图例里的objectC
)
以上描述的是一轮三色标记算法的工作过程,但是这是一个理想情况,因为标记过程中,标记的线程是和用户线程交替运行的,所以可能出现标记过程中引用发生变化的情况。
试想一下,在第二轮检查到第三轮检查之间,假设发生了引用的变化,objectD不再被objectB引用,而是被objectA引用,而且此时ObjectA的成员已经被检查完毕了,objectB的成员Field还没被检查。这时,objectD就永远不会再被检查到。这就导致了漏标
。
还有一种漏标的情况,就是新产生一个对象,这个对象被已经被标记为黑色的对象持有。比如图例中的newObjectF。因为黑色对象已经被认为是检查完毕了,所以新产生的对象不会再被检查,这也会导致漏标。
漏标发生的条件(需同时满足)
- 黑色对象新增了对白色对象的引用(即并发过程中新增引用)。
- 灰色对象断开了对白色对象的引用(即并发过程中引用消失)。
此时,白色对象因失去灰色对象的引用链,且黑色对象无法被重新扫描,最终被误回收。
解决方案
1. 增量更新(Incremental Update)—— CMS回收器方案
破坏第一个条件(阻止黑色对象引用白色后不被扫描)。 当黑色对象新增对白色对象的引用时,将该黑色对象重新标记为灰色,并记录该引用关系。
2. 原始快照(Snapshot At The Beginning, SATB)—— G1回收器方案
破坏第二个条件(阻止灰色对象断开引用后导致白色对象丢失)。 在并发标记开始时建立对象图的逻辑快照。若灰色对象断开对白色对象的引用,记录该删除的引用关系。
G1 垃圾回收全流程
G1通过 Young GC → 并发标记 → Mixed GC 的流程实现低停顿回收,辅以Full GC兜底,适合大内存、低延迟要求的应用场景(如Web服务)。
(1) Young GC(新生代回收)
触发条件:当Eden区空间耗尽时触发(需STW暂停应用线程)。
流程:
- 标记存活对象:
- 扫描GC Roots(静态变量、方法栈局部变量等),标记所有直接可达对象。
- 更新Remembered Sets(RS):
- 处理
dirty card queue
(记录老年代对新生代的引用),确保RS能准确反映跨代引用关系。
- 处理
- 复制存活对象:
- 将Eden区和Survivor区的存活对象复制到新的Survivor区(或直接晋升到老年代)。
- 回收空间:
- 清空Eden区和已处理的Survivor区,释放内存。
关键点:仅回收新生代(Eden + Survivor),通过复制算法避免碎片。
(2) Concurrent Marking(并发标记)
触发条件:堆内存使用达到一定阈值(默认45%),与Young GC并发执行,无需暂停应用。
流程:
- 初始标记(Initial Mark):
- 短暂STW,标记GC Roots直接关联的对象(依赖Young GC的暂停完成)。
- 并发标记(Concurrent Mark):
- 与应用线程并发,遍历对象图标记所有可达对象(使用三色标记法)。
- 最终标记(Remark):
- 短暂STW,处理并发期间引用变动的对象(通过SATB算法保证准确性)。
- 清理(Cleanup):
- 统计各Region中垃圾比例,生成回收集(Collection Set),为Mixed GC做准备。
(3) Mixed GC(混合回收)
触发条件:并发标记完成后触发(需STW暂停)。
流程:
- 同时回收新生代 + 老年代的高垃圾比例Region:
- 基于并发标记统计的数据,优先回收垃圾最多的Region(Garbage-First原则)。
- 复制存活对象到空闲Region,清空原Region空间。
特点:综合处理多代内存,避免Full GC,最大程度减少停顿。
(4) Full GC(备用方案)
触发条件:当Mixed GC无法快速释放足够内存时(如内存分配过快)。
流程:
- 单线程STW回收(Serial Old):
- 全堆扫描并压缩内存(效率低,需尽量避免)。