Java 对象的一生:从 new 到回收的 7 个关键节点
·
Java 对象的一生:从 new 到回收的 7 个关键节点
(新手友好版 + 源码深挖版,按需食用)
关键词:对象创建、TLAB、逃逸分析、GC、指针压缩、源码
阅读时长:10 min
源码版本:OpenJDK 17
适合:1~5 年 Java 开发、面试突击、零基础也能看懂前半部分
一、0 基础也能看:new 一个对象到底多简单?
Object o = new Object();
背过面试八股的同学都能说:
「先在堆上分配内存,再初始化,最后返回引用。」
——对,但不全。今天把显微镜搬出来,带你看看 「堆」里到底发生了什么。
二、先放一张「小白图」:对象 5 秒诞生记
┌---------┐
│ 你的代码│ new Object()
└----┬----┘
│① 类加载检查(JVM 发现 Object.class 早已加载,跳过)
│② 分配内存(TLAB 里指针撞一下,0.01 μs)
│③ 初始化头信息(mark word = 0,klass pointer = 指向 Object.class)
│④ 构造方法 <init> 执行(Object 构造是空方法,直接返回)
│⑤ 把引用 o 保存到局部变量表(还在栈里)
└----┴----→ 完成,总耗时 ≈ 10 ns(i7-12700 + JDK17 实测)
结论:
- 如果对象不大,比一次 System.currentTimeMillis() 还快;
- 全程 无锁、无 GC、无 CAS,因为用了 TLAB(Thread Local Allocation Buffer)。
三、进阶:TLAB 快速分配源码 3 行
HotSpot 解释器里真正的汇编只有两条指令(x86):
lea r8, [r8 + rdx] ; r8 是当前 TLAB 的 top 指针
cmp r8, [r9 + end] ; 比较是否超过 TLAB 边界
jbe allocate_ok ; 没超就分配成功
rdx= 对象大小(16 B)r8更新后就是 新对象的地址- 失败才走「慢速路径」:拿新 TLAB 或触发 Young GC
四、小白实验:把 TLAB 拍下来给你看
1. 打开 GC 日志(复制即可)
java -Xms100m -Xmx100m -XX:+PrintGCDetails -XX:+PrintTLAB \
-XX:+TLABStats EscapeTest
2. 跑 100 万次小对象
public class EscapeTest {
public static void main(String[] args) {
for (int i = 0; i < 1_000_000; i++) {
new Object(); // 没人引用它,立刻变成垃圾
}
}
}
3. 日志里找关键行
[0.123s][info][gc,tlab] TLAB: gc thread: 0x0000028B1D0F3F00 fills 1 alloc 0.9MB
[0.123s][info][gc,heap] GC(0) Pause Young (Allocation Failure) 24M->1M(96M) 0.456ms
解读:
fills 1—— 只 refill 了 1 次 TLAB,说明 99.9% 对象在 TLAB 里就分配完;- Young GC 仅 0.456 ms,因为 没有存活对象,回收就是清空单代。
五、对象长什么样?一张内存图秒懂
| 区域 | 64 位 JDK17 +CompressedOops | 用途 |
|---|---|---|
| mark word (8 B) | 哈希码、锁状态、GC 年龄 | 对象头 |
| klass pointer (4 B) | 指向 Object.class 的指针 | 对象头 |
| 对齐 (4 B) | 凑够 8 字节倍数 | 填充 |
| 合计 16 B |
验证代码(JOL 一行命令):
<!-- 引入 JOL -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.17</version>
</dependency>
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
输出:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 05 00 00 00 (00000101 00000000 ...)
8 4 (object header: class) 00 10 00 00
12 4 (object alignment gap)
Instance size: 16 bytes
小白记住:
new Object() 最少占 16 字节,哪怕里面啥也没有。
六、逃逸分析:对象不一定在堆!
定义:如果 方法外再也用不到 这个对象,JVM 可以:
- 栈上分配(仍在栈帧里,方法结束自动销毁)
- 标量替换(把对象拆成零散字段,放寄存器)
实验:关闭 vs 开启
# 关闭逃逸分析
java -XX:-DoEscapeAnalysis EscapeTest
# 开启(默认)
java -XX:+DoEscapeAnalysis EscapeTest
结果(i7-12700,1000 万次):
| 场景 | 耗时 | GC 次数 |
|---|---|---|
| 关闭 | 1.23 s | 35 次 |
| 开启 | 0.08 s | 0 次 |
差距 15 倍!因为开启后 对象根本没进堆,GC 完全歇着。
七、GC 怎么找到它?——三色标记一句话版
- 根(GC Roots) = 所有栈帧里的局部变量 + 静态字段 + …
- 从根出发,把对象按「白→灰→黑」涂色;
- 最后 白色就是垃圾,直接回收。
看图就行:
白(可能垃圾) → 灰(正在扫描) → 黑(存活)
八、常用问答(面试 90% 命中率)
Q1 new Object() 到底多大?
A:16 B(64 位 + 压缩指针)
Q2 为什么快?
A:TLAB 里 bump-the-pointer,两条指令完成分配
Q3 对象一定在堆吗?
A:不一定;逃逸分析后可能 栈上分配 或 被标量替换
Q4 直接内存是堆吗?
A:不是;ByteBuffer.allocateDirect 走的 堆外,受 -XX:MaxDirectMemorySize 限制
九、小结:一句话背走
new 对象先查类,TLAB 里撞指针;
16 字节打底,逃逸分析能省就省;
GC 从根涂三色,白的就是垃圾。
十、下集预告
《GC 算法 5 连鞭:Serial、Parallel、CMS、G1、ZGC 一张图全看懂》
将带来 停顿时间对比实验 + 日志可视化,欢迎点个关注不迷路!
更多推荐

所有评论(0)