文章目录
- 一、引子:一个对象的一生
- [二、对象的诞生:从 new 指令到可用对象](#二、对象的诞生:从 new 指令到可用对象)
-
- [2.1 对象创建的五步流程](#2.1 对象创建的五步流程)
- [2.2 对象的内存布局](#2.2 对象的内存布局)
- [2.3 对象头:Mark Word 与类型指针](#2.3 对象头:Mark Word 与类型指针)
- [2.4 GC 年龄与晋升机制](#2.4 GC 年龄与晋升机制)
- 三、对象的死亡判定:可达性分析
-
- [3.1 引用计数法:为什么被淘汰](#3.1 引用计数法:为什么被淘汰)
- [3.2 可达性分析:JVM 的选择](#3.2 可达性分析:JVM 的选择)
- [3.3 GC Roots 有哪些](#3.3 GC Roots 有哪些)
- [四、回收策略:GC 四大算法](#四、回收策略:GC 四大算法)
-
- [4.1 标记清除](#4.1 标记清除)
- [4.2 复制算法](#4.2 复制算法)
- [4.3 标记整理](#4.3 标记整理)
- [4.4 分代收集](#4.4 分代收集)
- 五、执行者:垃圾收集器演进
-
- [5.1 收集器全览](#5.1 收集器全览)
- [5.2 吞吐量 vs 低停顿](#5.2 吞吐量 vs 低停顿)
- [5.3 CMS:并发标记清除](#5.3 CMS:并发标记清除)
- [5.4 G1:面向服务端的默认选择](#5.4 G1:面向服务端的默认选择)
- [5.5 ZGC:亚毫秒级停顿](#5.5 ZGC:亚毫秒级停顿)
- [5.6 收集器与代际/算法的对应关系](#5.6 收集器与代际/算法的对应关系)
- 六、总结:一张脑图记住全链路
- 七、面试高频考点速查
一、引子:一个对象的一生
User user = new User();
这行代码每天都在写,但 JVM 在背后做了多少事?一个对象从"出生"到"死亡",大致经历三个阶段:
- 诞生:类加载检查 → 分配内存 → 初始化零值 → 设置对象头 → 执行构造方法
- 存活判定:通过可达性分析判断对象是否"还活着"
- 回收:死亡的对象由 GC 算法和垃圾收集器负责清理
把这三步串起来,就是一条完整的对象生命周期全链路。下面逐层拆解。
二、对象的诞生:从 new 指令到可用对象
2.1 对象创建的五步流程
当 JVM 遇到字节码 new 指令时,按以下顺序执行:
第一步:类加载检查。 检查 new 指令的参数能否在常量池中定位到一个类的符号引用,并确认该类是否已被加载、解析和初始化。如果没有,先触发类加载过程。
第二步:分配内存。 对象所需内存大小在类加载完成后即可完全确定。分配方式有两种:
| 分配方式 | 适用条件 | 原理 |
|---|---|---|
| 指针碰撞 | 堆内存规整 | 已用内存在一侧,空闲在另一侧,中间一个指针,分配时指针向空闲方向移动对象大小的距离 |
| 空闲列表 | 堆内存不规整 | 维护一个可用内存块列表,分配时从列表中找到足够大的块 |
选择哪种方式,取决于垃圾收集器是否具备空间压缩整理能力。Serial、ParNew 等带压缩整理的收集器 → 指针碰撞;CMS 这类基于清除算法的收集器 → 空闲列表。
第三步:初始化零值。 将分配到的内存空间(不包括对象头)全部初始化为零值,保证对象的实例字段在 Java 代码中不赋初值也能直接使用。
第四步:设置对象头。 将对象的类元数据、哈希码、GC 分代年龄等信息写入对象头。
第五步:执行构造方法。 执行 <init> 方法,按程序员意愿完成初始化。至此,一个真正可用的对象才算完全构造出来。
并发安全 :分配内存时通过 CAS + 失败重试保证原子性,或使用 TLAB(本地线程分配缓冲),每个线程在堆中预分配一小块私有内存,优先在 TLAB 中分配,用完才需要同步锁定。
2.2 对象的内存布局
HotSpot 中对象在堆内存的布局分为三部分:
┌──────────────────────────────────────────────┐
│ 对象头 (Header) │
│ ┌────────────────────┬────────────────────┐ │
│ │ MarkWord │ 类型指针 (Klass) │ │
│ │ (哈希码/GC年龄/锁) │ (指向类元数据) │ │
│ └────────────────────┴────────────────────┘ │
├──────────────────────────────────────────────┤
│ 实例数据 (Instance Data) │
├──────────────────────────────────────────────┤
│ 对齐填充 (Padding) │
└──────────────────────────────────────────────┘
2.3 对象头:Mark Word 与类型指针
Mark Word 存储对象自身的运行时数据:哈希码、GC 分代年龄、锁状态标志等。在 64 位 HotSpot 中占 8 字节。由于 Mark Word 需要存储的信息超过 8 字节的容量,它被设计成一个非固定的数据结构,根据对象当前的状态复用存储空间:
| 锁状态 | Mark Word 内容(64 位) | 标志位 |
|---|---|---|
| 无锁 | unused:25 | hashCode:31 | unused:1 | age:4 | biased:0 | 01 |
| 偏向锁 | thread:54 | epoch:2 | unused:1 | age:4 | biased:1 | 01 |
| 轻量级锁 | 指向线程栈中 Lock Record 的指针(62 位) | 00 |
| 重量级锁 | 指向 Monitor 的指针(62 位) | 10 |
| GC 标记 | GC 过程用到的信息 (62位) | 11 |
GC 年龄只占 4 位,最大值为 15,这也是默认晋升阈值为 15 的根本原因。
类型指针(Klass Pointer) 指向方法区中该对象的类元数据,JVM 通过它确定这个对象是哪个类的实例。开启指针压缩时占 4 字节,否则 8 字节。
2.4 GC 年龄与晋升机制
对象在 Eden 中出生,经历一次 Minor GC 后若存活,被移入 Survivor,年龄加 1。年龄增长到一定阈值后晋升老年代。晋升有三条路径:
路径一:达到年龄阈值。 -XX:MaxTenuringThreshold 控制,默认 15(CMS 为 8)。因为 Mark Word 中 GC 年龄占 4 位,最大值就是 15。
路径二:动态年龄判定。 虚拟机并不要求对象年龄必须达到 15 才能晋升。HotSpot 在每次 Minor GC 后,按年龄从小到大累加各年龄段对象的总大小,当累加和超过 Survivor 空间的 TargetSurvivorRatio(默认 50%)时,取当前年龄和 MaxTenuringThreshold 中较小的值作为新的晋升阈值,大于等于该年龄的对象直接进入老年代。
这里有一个常见误区需要澄清:动态年龄判定累加的是"年龄从小到大的累加和",而不是"某个年龄段对象的大小" 。源码中通过 total += sizes[age] 逐级累加,当 total > desired_survivor_size 时停止。
路径三:大对象直接进入老年代。 通过 -XX:PretenureSizeThreshold 配置,超过该值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量复制。
口诀:年龄到了升,动态超半升,大对象直接升。
三、对象的死亡判定:可达性分析
3.1 引用计数法:为什么被淘汰
引用计数法给每个对象维护一个计数器,被引用时加 1,引用失效时减 1,计数为 0 即为垃圾。实现简单、效率高,但无法解决循环引用问题:A 引用 B,B 引用 A,即使外部没有任何引用指向它们,计数器也永远不为 0。因此主流 JVM 没有采用该算法。
3.2 可达性分析:JVM 的选择
可达性分析的核心思想是:从一组称为 GC Roots 的根对象出发,向下遍历引用链,所有可达的对象标记为存活,不可达的对象判定为可回收。
这里有一个重要的思维转变:垃圾回收不是"找到垃圾然后清除",而是"找到存活对象并标记,剩下的即为垃圾" 。
3.3 GC Roots 有哪些
GC Roots 是一组对象的集合,主要包括:
| GC Root 类型 | 说明 |
|---|---|
| 虚拟机栈中局部变量表引用的对象 | 方法参数、局部变量、临时变量 |
| 本地方法栈中 JNI 引用的对象 | Native 方法引用的对象 |
| 方法区中静态属性引用的对象 | 类的静态变量 |
| 方法区中常量引用的对象 | 字符串常量池等 |
| 同步锁持有的对象 | synchronized 持有的对象 |
| JVM 内部引用 | 系统类加载器、基础类对象等 |
记忆口诀:栈(虚拟机栈 + 本地方法栈)、静(静态变量)、常(常量)、锁(同步锁)、内(JVM 内部)。
四、回收策略:GC 四大算法
对象被判定为垃圾后,需要具体算法来回收内存。四种基础算法如下:
4.1 标记清除
流程:标记所有存活对象 → 统一回收未标记对象。
优点:实现简单。
缺点 :产生大量内存碎片,碎片过多导致大对象无法分配,提前触发 Full GC。
4.2 复制算法
流程:将内存分为两块,每次只用一块。GC 时将存活对象复制到另一块,然后清空当前块。
优点:无内存碎片,分配时可用指针碰撞。
缺点:可用内存减半(空间换效率)。
落地 :新生代使用。Eden:S0:S1 默认 8:1:1,只浪费 10% 的空间。每次 Minor GC 将 Eden + From 区的存活对象复制到 To 区,然后 From 和 To 互换角色。
4.3 标记整理
流程:标记存活对象 → 将所有存活对象向一端移动 → 清理边界以外的内存。
优点:无碎片。
缺点:移动对象需要更新所有引用,开销大。
落地:老年代使用(对象存活率高,复制算法不合适)。
4.4 分代收集
核心思想:不同生命周期的对象采用不同的回收策略。
| 区域 | 对象特征 | 适用算法 | 原因 |
|---|---|---|---|
| 新生代 | 朝生夕灭 | 复制算法 | 存活对象少,复制成本低 |
| 老年代 | 存活率高 | 标记清除 / 标记整理 | 存活对象多,复制不划算 |
分代收集的理论基础是弱分代假说 (绝大多数对象朝生夕灭)和强分代假说(熬过越多次 GC 的对象越难被回收)。
常见误区:Java 堆并非按"新生代/老年代/永久代"这样的物理结构划分。这些划分是 HotSpot 基于分代理论的设计,且 G1 之后打破了连续内存的分代布局。永久代也不等于方法区,JDK 8 已用元空间替代永久代。
五、执行者:垃圾收集器演进
算法是"怎么回收",收集器是"谁来回收"。按 吞吐量优先 和 低停顿优先 两条线梳理。
5.1 收集器全览
| 收集器 | 年代 | 分代 | 算法 | 线程 | 核心特点 |
|---|---|---|---|---|---|
| Serial | JDK 1.3 | 新生代 | 复制 | 单线程 | 简单高效,客户端场景 |
| ParNew | JDK 1.4 | 新生代 | 复制 | 多线程 | Serial 的多线程版,配合 CMS |
| Parallel Scavenge | JDK 1.4 | 新生代 | 复制 | 多线程 | 吞吐量优先,JDK 8 默认 |
| Serial Old | JDK 1.3 | 老年代 | 标记整理 | 单线程 | Serial 的老年代版 |
| Parallel Old | JDK 1.6 | 老年代 | 标记整理 | 多线程 | Parallel Scavenge 的老年代版 |
| CMS | JDK 1.5 | 老年代 | 标记清除 | 并发 | 低停顿,有碎片,JDK 9 废弃 |
| G1 | JDK 1.7 | 整堆 | 整体标记整理 + 局部复制 | 并发 | 可预测停顿,JDK 9+ 默认 |
| ZGC | JDK 11 | 整堆 | 染色指针 + 读屏障 | 并发 | 亚毫秒级停顿,TB 级堆 |
| Shenandoah | JDK 12 | 整堆 | 并发整理 | 并发 | 低延迟,RedHat 主导 |
5.2 吞吐量 vs 低停顿
| 维度 | 吞吐量优先 | 低停顿优先 |
|---|---|---|
| 代表收集器 | Parallel Scavenge + Parallel Old | CMS / G1 / ZGC |
| 目标 | 最大化 CPU 用于用户代码的时间占比 | 最小化 STW 时长 |
| 适用场景 | 后台计算、批处理 | Web 服务、实时系统 |
| 代价 | STW 时间较长 | 吞吐量略有下降 |
STW 全称 Stop The World,即 GC 时暂停所有用户线程。所有 GC 调优的核心目标都是降低 STW 的时长与频率。
5.3 CMS:并发标记清除
CMS 的核心贡献是让标记和清除阶段与用户线程并发执行,大幅缩短 STW。
四阶段流程:
- 初始标记(STW):标记 GC Roots 直接可达的对象,速度极快
- 并发标记:从初始标记的对象出发,并发遍历对象图
- 重新标记(STW):修正并发标记期间因用户线程运行导致的变动
- 并发清除:并发清理死亡对象
核心缺陷:
- 使用标记清除算法 → 产生内存碎片 → 可能提前触发 Full GC
- Concurrent Mode Failure:并发清除期间用户线程仍在分配对象,若老年代空间不足则触发 Full GC,导致长时间停顿
- 浮动垃圾:并发清除阶段新产生的垃圾只能等下次 GC 回收
JDK 9 起 CMS 被标记为废弃,JDK 14 正式移除。
5.4 G1:面向服务端的默认选择
G1(Garbage-First)从 JDK 9 起成为默认收集器,设计目标是可预测的停顿时间模型。
核心创新:Region 化内存布局。 G1 将堆划分为多个大小相等的 Region(1MB~32MB),每个 Region 可以动态扮演 Eden、Survivor 或 Old 角色,不再要求连续的内存分代布局。
核心机制:
- 整体标记整理 + 局部复制:整体上看是标记整理(无碎片),局部(Region 之间)是复制
- 可预测停顿 :用户可设置
-XX:MaxGCPauseMillis,G1 根据每个 Region 的回收价值(垃圾占比)优先回收收益最大的 Region - SATB 写屏障:采用 Snapshot At The Beginning 方案解决并发标记的漏标问题,在灰色对象删除引用时记录快照,标记结束后重新扫描
适用场景:堆内存 4GB~64GB 的服务端应用。
5.5 ZGC:亚毫秒级停顿
ZGC 从 JDK 11 引入,专为大堆内存(TB 级)和超低延迟场景设计。
核心创新:
- 染色指针:将 GC 状态信息编码在 64 位指针中(而非对象头),大幅减少内存占用
- 读屏障:对象读取时拦截并完成指针修正,替代传统写屏障
- 全并发 :标记、转移、重定位三大阶段几乎全部并发执行,STW 停顿控制在亚毫秒级
代价:吞吐量略低于 G1,且并发线程占用更多 CPU 资源。
适用场景:金融交易、实时游戏服务器、高频交易系统等对延迟极度敏感的业务。
5.6 收集器与代际/算法的对应关系
新生代(复制算法)
├─ Serial / ParNew / Parallel Scavenge
老年代(标记清除 / 标记整理)
├─ Serial Old / Parallel Old / CMS
整堆(并发标记 + 并发整理)
├─ G1 / ZGC / Shenandoah
选择口诀:小内存客户端用 Serial;吞吐量优先用 Parallel;低停顿用 CMS/G1;超大堆 + 极低延迟用 ZGC。
六、总结:一张脑图记住全链路
new User()
│
┌─────────────┴─────────────┐
│ 1. 类加载检查 │
│ 2. 分配内存(指针碰撞) │
│ 3. 初始化零值 │
│ 4. 设置对象头 │
│ └─ Mark Word + Klass │
│ 5. 执行构造方法 │
└─────────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ 存活判定 │
│ 可达性分析 │
│ GC Roots: 栈/静/常/锁 │
└─────────────┬───────────┘
│
▼
┌─────────────────────────┐
│ GC 算法 │
│ 标记清除 → 有碎片 │
│ 复制 → 无碎片,费空间 │
│ 标记整理 → 无碎片,慢 │
│ 分代收集 → 各取所长 │
└─────────────┬───────────┘
│
▼
┌─────────────────────────┐
│ 垃圾收集器 │
│ Serial → Parallel │
│ → CMS → G1 → ZGC │
│ 吞吐量 ←→ 低停顿 │
└─────────────────────────┘
七、面试高频考点速查
- 对象创建流程:类加载检查 → 分配内存 → 初始化零值 → 设置对象头 → 构造方法
- 对象头结构:Mark Word(哈希码 + GC 年龄 + 锁标记)+ 类型指针
- GC 年龄上限:Mark Word 中占 4 位,最大 15;动态年龄判定累加和超 50% 提前晋升
- 可达性分析 GC Roots:虚拟机栈、静态变量、常量、JNI 引用、同步锁对象
- 四种 GC 算法:标记清除(碎片)、复制(双倍空间)、标记整理(移动开销)、分代收集
- 收集器选型:吞吐量 → Parallel;低停顿 → CMS/G1;超大堆 → ZGC
- STW 含义:GC 时暂停所有用户线程,是所有 GC 调优的核心指标