2026年7月11日 · 6 分钟阅读
JVM 对象与垃圾回收:对象创建、内存分配、引用类型与 GC 收集器
梳理 Java 对象创建流程、TLAB、对象进入老年代的条件、对象内存布局、可达性分析、引用类型和常见垃圾收集器。
JVM 里最核心的两件事,一件是对象怎么创建,一件是对象什么时候被回收。理解这条生命周期,就能更好地分析内存分配、GC 日志和线上内存问题。
一个对象的一生,可以粗略分成:
创建 -> 分配内存 -> 使用 -> 判定是否存活 -> 回收或晋升
对象创建流程
当代码执行 new 时,JVM 创建对象大致会经历这些步骤:
- 检查常量池中是否能定位到对应类的符号引用。
- 确认类是否已经加载、解析和初始化。
- 为对象分配内存空间。
- 将分配到的内存初始化为零值。
- 设置对象头信息。
- 执行构造方法,把对象初始化为程序期望的状态。
类没有加载完成之前,对象无法真正创建。
对象内存怎么分配
对象内存分配方式主要有两种:
- 指针碰撞:堆内存规整时,只需要移动分界指针。
- 空闲列表:堆内存不规整时,用列表记录哪些内存块可用。
选择哪种方式,和垃圾收集器是否会整理内存有关。如果收集器能压缩整理内存,就更容易使用指针碰撞。
分配安全问题
多线程同时创建对象时,会竞争堆内存分配位置。JVM 通常用两种方式保证安全:
- CAS + 失败重试。
- TLAB,也就是线程本地分配缓冲。
TLAB 会给每个线程预先分配一小块 Eden 内存。线程创建小对象时,优先在自己的 TLAB 中分配,减少线程间竞争。
对象什么时候进入老年代
对象进入老年代常见有四种情况。
| 情况 | 说明 |
|---|---|
| 长期存活对象 | 对象每熬过一次 Minor GC,年龄增加,达到阈值后进入老年代 |
| 大对象 | 大数组、大字符串等可能直接进入老年代 |
| Survivor 放不下 | Minor GC 后存活对象无法放入 Survivor,触发空间担保 |
| 动态年龄判断 | 同年龄对象总大小超过 Survivor 一定比例时,年龄大于等于该值的对象提前晋升 |
对象晋升不是坏事,但老年代增长过快就要警惕。
内存担保机制
Minor GC 后,Eden 中存活对象通常会复制到 Survivor。如果 Survivor 放不下,就需要老年代进行空间担保,把存活对象放入老年代。
这也是为什么新生代配置过小、Survivor 配置不合理,可能导致对象过早进入老年代,进而增加 Full GC 压力。
对象内存布局
Java 对象在堆中的布局通常包括三部分:
| 区域 | 说明 |
|---|---|
| 对象头 Header | Mark Word、类型指针,数组还会包含长度信息 |
| 实例数据 Instance Data | 对象的成员变量数据 |
| 对齐填充 Padding | JVM 对象通常按 8 字节对齐,不足则补齐 |
对象头里保存了很多运行时信息,比如哈希码、GC 分代年龄、锁状态等。synchronized 锁升级也和对象头里的 Mark Word 有关。
对象访问方式
Java 程序通过引用访问对象。常见实现有两种:
- 句柄访问。
- 直接指针访问。
句柄访问的好处是对象移动时只需要修改句柄中的地址,引用本身比较稳定;直接指针访问的好处是少一次指针定位,速度更快。HotSpot 主要使用直接指针。
什么是垃圾
简单说,没有被任何有效引用关联的对象,就是垃圾。
但 JVM 不能只靠引用计数判断对象是否存活,因为循环引用会让两个无用对象互相引用,引用计数都不为 0。
HotSpot 主要使用可达性分析。
可达性分析
可达性分析从 GC Roots 出发,沿着引用链向下搜索。凡是能被 GC Roots 直接或间接到达的对象,就是存活对象;不能到达的对象,就可能被回收。
常见 GC Roots 包括:
- 虚拟机栈中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中 Native 方法引用的对象。
- 活跃线程对象等。
四种引用类型
Java 引用类型包括强引用、软引用、弱引用和虚引用。
| 类型 | 回收特点 | 典型场景 |
|---|---|---|
| 强引用 | 只要引用存在,一般不会被回收 | 普通对象引用 |
| 软引用 | 内存不足时可能被回收 | 缓存 |
| 弱引用 | 下一次 GC 时会被回收 | WeakHashMap |
| 虚引用 | 不影响对象生命周期,用于回收通知 | 直接内存清理等 |
引用类型决定了对象和 GC 之间的关系。
垃圾清除算法
常见垃圾回收算法有三类:
- 标记-清除:标记垃圾后直接清除,容易产生内存碎片。
- 标记-整理:标记后把存活对象向一端移动,减少碎片。
- 复制算法:把存活对象复制到另一块区域,适合新生代。
新生代对象大多朝生夕死,所以复制算法效率高;老年代对象存活率高,更适合标记-清除或标记-整理。
常见垃圾收集器
| 收集器 | 特点 |
|---|---|
| Serial | 单线程收集器,简单但会 Stop The World |
| ParNew | Serial 的多线程版本,常与 CMS 搭配 |
| Parallel Scavenge | 吞吐量优先的新生代收集器 |
| Parallel Old | Parallel Scavenge 的老年代版本 |
| CMS | 低停顿并发收集器,但会产生碎片 |
| G1 | 面向服务端应用,支持可预测停顿 |
| ZGC | 面向低延迟、大内存场景 |
选择收集器时要看目标:是吞吐量优先,还是响应时间优先。
G1 的基本思路
G1 不再简单按连续的新生代、老年代管理堆,而是把堆划分为多个 Region。它会评估每个 Region 的回收价值,优先回收收益高的区域。
G1 的特点包括:
- 并行与并发。
- 多代收集。
- 空间整合。
- 可预测停顿。
它适合大多数服务端应用,但也不是万能的。更短停顿通常意味着更多额外开销。
Minor GC、Major GC 和 Full GC
| 类型 | 含义 |
|---|---|
| Minor GC | 新生代 GC |
| Major GC | 通常指老年代 GC,不同资料中定义可能有差异 |
| Full GC | 整个堆和方法区相关区域的 GC |
线上排查时,不要只看发生了哪种 GC,还要看触发原因、耗时、回收前后内存变化。
小结
对象和 GC 可以按这条线理解:
- 对象创建前先确认类已加载。
- 对象优先在 Eden 或 TLAB 分配。
- 存活对象可能进入 Survivor,再逐步晋升老年代。
- 可达性分析决定对象是否存活。
- 不同引用类型影响对象回收时机。
- 不同收集器服务不同目标。
理解对象生命周期之后,GC 日志就不再是一堆看不懂的数字,而是对象分配和回收行为的记录。