从诞生到回收:JVM对象生命周期与垃圾回收全链路解析

文章目录

  • 一、引子:一个对象的一生
  • [二、对象的诞生:从 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 在背后做了多少事?一个对象从"出生"到"死亡",大致经历三个阶段:

  1. 诞生:类加载检查 → 分配内存 → 初始化零值 → 设置对象头 → 执行构造方法
  2. 存活判定:通过可达性分析判断对象是否"还活着"
  3. 回收:死亡的对象由 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。

四阶段流程

  1. 初始标记(STW):标记 GC Roots 直接可达的对象,速度极快
  2. 并发标记:从初始标记的对象出发,并发遍历对象图
  3. 重新标记(STW):修正并发标记期间因用户线程运行导致的变动
  4. 并发清除:并发清理死亡对象

核心缺陷

  • 使用标记清除算法 → 产生内存碎片 → 可能提前触发 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     │
          │    吞吐量 ←→ 低停顿      │
          └─────────────────────────┘

七、面试高频考点速查

  1. 对象创建流程:类加载检查 → 分配内存 → 初始化零值 → 设置对象头 → 构造方法
  2. 对象头结构:Mark Word(哈希码 + GC 年龄 + 锁标记)+ 类型指针
  3. GC 年龄上限:Mark Word 中占 4 位,最大 15;动态年龄判定累加和超 50% 提前晋升
  4. 可达性分析 GC Roots:虚拟机栈、静态变量、常量、JNI 引用、同步锁对象
  5. 四种 GC 算法:标记清除(碎片)、复制(双倍空间)、标记整理(移动开销)、分代收集
  6. 收集器选型:吞吐量 → Parallel;低停顿 → CMS/G1;超大堆 → ZGC
  7. STW 含义:GC 时暂停所有用户线程,是所有 GC 调优的核心指标
相关推荐
SL_staff1 小时前
JVS-Logic:从页面配置工具到企业级业务逻辑中枢的技术演进
java·算法·全栈
DevRay1 小时前
Java 21虚拟线程深度实战:告别线程池焦虑,百万并发吞吐量直接翻倍(原理+代码+避坑)
java·开发语言
forestsea2 小时前
从零构建 Java 智能体 RAG 系统:Milvus 向量数据库实战指南
java·数据库·milvus
lifewange2 小时前
VSCode怎么运行java
java·ide·vscode
用户094248568032 小时前
第10章:OpenJDK异常体系、栈轨迹与错误诊断入门
java·jvm
醉颜凉2 小时前
Java 必看:如何彻底避免 HashMap 多线程死循环问题?
java·开发语言
默 语2 小时前
Java新手入门:从零开始安装JDK并配置环境变量
java·开发语言·python·mysql·group by·1024程序员节·数据去重
何以解忧,唯有..2 小时前
高并发下超卖问题解决方案:从数据库到分布式锁的完整实践
java
行者全栈架构师2 小时前
Spring Boot 接入 MaxKey 单点登录:6 个内部系统,一次登录全通行
java·vue.js·后端