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 实测)

结论:

  1. 如果对象不大,比一次 System.currentTimeMillis() 还快
  2. 全程 无锁、无 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 可以:

  1. 栈上分配(仍在栈帧里,方法结束自动销毁)
  2. 标量替换(把对象拆成零散字段,放寄存器)

实验:关闭 vs 开启

# 关闭逃逸分析
java -XX:-DoEscapeAnalysis EscapeTest
# 开启(默认)
java -XX:+DoEscapeAnalysis EscapeTest

结果(i7-12700,1000 万次):

场景 耗时 GC 次数
关闭 1.23 s 35 次
开启 0.08 s 0 次

差距 15 倍!因为开启后 对象根本没进堆,GC 完全歇着。


七、GC 怎么找到它?——三色标记一句话版

  1. 根(GC Roots) = 所有栈帧里的局部变量 + 静态字段 + …
  2. 从根出发,把对象按「白→灰→黑」涂色;
  3. 最后 白色就是垃圾,直接回收。

看图就行:

白(可能垃圾) → 灰(正在扫描) → 黑(存活)

八、常用问答(面试 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 一张图全看懂》
将带来 停顿时间对比实验 + 日志可视化,欢迎点个关注不迷路!


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐