Skip to content

对象创建与内存分配

1. 这是什么

对象创建与内存分配描述了一个 Java 对象从 new 开始,到进入堆中被 JVM 管理的整个过程。
这是理解性能、GC 和内存行为的关键一环。

一句话理解:

  • new 只是源码里的一个动作
  • 真正运行时还会经历类检查、内存分配、初始化和构造执行

2. 为什么重要

对象创建频率和分配方式会直接影响这些事情:

  • 程序吞吐
  • 堆内存占用
  • GC 压力
  • 延迟抖动

很多性能问题并不是“算法慢”,而是:

  • 创建了太多短命对象
  • 让本来应该很快分配的对象不断制造 GC 压力

3. 先建立直觉:对象创建为什么平时很快

很多人知道“对象创建有成本”,但又看到 Java 里大量 new 似乎也能跑得很快。
这是因为 JVM 在常见场景下做了很多优化,例如:

  • 堆上顺序分配
  • TLAB 线程本地分配缓冲区
  • 对象头和内存布局优化
  • 逃逸分析下的标量替换 / 栈上分配机会

所以更准确的理解是:

  • 对象创建不一定慢
  • 但大量对象分配和回收仍然会明显影响系统行为

4. 核心内容

4.1 对象从 new 到可用,大致经历什么

可以先用一个工程上足够实用的顺序理解:

  1. 检查类是否已加载、解析、初始化
  2. 为对象分配内存
  3. 把分配到的内存初始化为零值
  4. 设置对象头
  5. 执行构造方法,把对象变成业务意义上“可用”

这也是为什么:

  • new 看起来简单
  • 但运行时并不只是“申请一块内存”这么单薄

4.2 类加载检查

当你执行 new SomeClass() 时,JVM 首先要确认:

  • 这个类是否已经被加载
  • 是否已经完成必要的链接和初始化

如果没有,先完成类相关流程,再继续对象创建。

这也是为什么对象创建和类加载机制是相互关联的。

4.3 内存分配是怎么做的

对象通常分配在堆上。
在堆空间规整、可顺序分配时,JVM 可以采用类似“指针碰撞”的方式:

  • 直接把堆顶指针向后挪一段

如果空间不规整,则会更依赖空闲列表等管理方式。

你不需要死记具体实现细节,但要知道:

  • 分配是否高效,和堆布局、垃圾回收策略、并发竞争都有关系

4.4 TLAB 是什么

TLAB 是 Thread Local Allocation Buffer,线程本地分配缓冲区。

可以把它理解成:

  • JVM 从 Eden 区先切一小块空间给线程自己用
  • 线程在这块区域里分配对象时,就不必每次都和其他线程抢全局堆指针

它的核心价值是:

  • 降低多线程分配对象时的竞争成本

所以 TLAB 不是“对象不在堆里了”,而是:

  • 对象仍然在堆的年轻代里
  • 只是先在属于线程的小块区域里更快分配

4.5 对象头、实例数据、对齐填充

一个对象在内存中,不只是你代码里写的字段值。
大致还会包含:

  • 对象头
  • 实例数据
  • 对齐填充

对象头通常会关联:

  • 类型信息
  • 哈希码相关信息
  • 锁状态相关信息

所以对象大小并不等于“字段大小简单相加”。

4.6 短命对象和长寿命对象

理解对象分配行为时,最重要的不是单个对象多大,而是:

  • 它活多久

常见情况:

  • 很多业务对象是短命对象,创建后很快变成垃圾
  • 少量对象会被长期持有,进入更长寿命区域

这也是为什么 GC 设计中经常会有分代思想:

  • 大部分对象朝生夕死
  • 少量对象长期存活

4.7 逃逸分析是什么

逃逸分析可以先这样理解:

  • JVM 在分析一个对象是否“逃出当前方法或线程”

如果没有逃逸,就可能获得一些优化机会,例如:

  • 栈上分配机会
  • 标量替换
  • 锁消除

学习阶段不需要把它神化,先记住:

  • 它的目标是减少不必要的堆分配和同步成本

5. 学习重点

这一章最重要的是理解这些点:

  • 对象创建是运行时流程,不是单纯语法
  • 类加载检查是对象创建的前置条件之一
  • 分配效率和 TLAB、堆布局、竞争情况有关
  • 短命对象会放大 GC 压力
  • 对象生命周期对性能影响很大

6. 常见问题

6.1 以为对象创建成本可以忽略不计

单个对象不一定昂贵,但海量短命对象会很快把问题放大。

6.2 不理解高频创建短命对象为什么会增加 GC 压力

因为:

  • 这些对象虽然很快会死
  • 但 JVM 仍然要负责给它们分配空间、扫描、回收

6.3 把所有性能问题都归结为“代码写得慢”

很多时候慢的不是计算本身,而是:

  • 分配频繁
  • 回收频繁
  • 由此带来的停顿和缓存扰动

7. 动手验证

这一节可以直接复制运行,边看边验证。

7.1 准备一个可运行示例

新建文件 ObjectAllocationDemo.java,内容如下:

java
import java.util.ArrayList;
import java.util.List;

public class ObjectAllocationDemo {
    static class User {
        private final int id;
        private final byte[] payload;

        User(int id, int sizeKb) {
            this.id = id;
            this.payload = new byte[sizeKb * 1024];
        }
    }

    private static final List<User> LONG_LIVED = new ArrayList<>();

    public static void main(String[] args) {
        System.out.println("classLoadedAndMainStarted=true");

        User first = new User(1, 256);
        System.out.println("firstUserCreated=" + (first != null));

        for (int i = 0; i < 3; i++) {
            allocateShortLived();
        }
        System.out.println("shortLivedAllocationRounds=3");

        for (int i = 0; i < 10; i++) {
            LONG_LIVED.add(new User(1000 + i, 512));
        }
        System.out.println("longLivedObjects=" + LONG_LIVED.size());
        System.out.println("demoFinished=true");
    }

    private static void allocateShortLived() {
        for (int i = 0; i < 2000; i++) {
            User temp = new User(i, 16);
            if (temp.id == -1) {
                System.out.println("never");
            }
        }
    }
}

7.2 编译并运行

先编译:

bash
javac ObjectAllocationDemo.java

再用较小堆和 GC 日志运行:

bash
java -Xms32m -Xmx32m -Xlog:gc* ObjectAllocationDemo

如果你只想先看程序输出,也可以直接:

bash
java ObjectAllocationDemo

7.3 你应该观察到什么

程序输出中应包含这些关键信息:

text
classLoadedAndMainStarted=true
firstUserCreated=true
shortLivedAllocationRounds=3
longLivedObjects=10
demoFinished=true

如果使用 GC 日志运行,还应看到 GC 相关日志输出,通常会包含类似:

text
Pause Young
Eden
Heap

具体格式和内容会因 JDK 版本不同而略有差异,但你通常能观察到:

  • 短命对象分配会触发年轻代回收
  • 堆容量较小时,GC 日志会更明显

7.4 每一行在验证什么

  • classLoadedAndMainStarted=true:说明对象创建是运行时流程的一部分,程序主流程开始后才发生实际分配
  • firstUserCreated=true:说明执行 new 后对象实例已成功创建
  • shortLivedAllocationRounds=3:说明程序中确实制造了大量短命对象
  • longLivedObjects=10:说明被长期持有的对象会留在可达集合中
  • GC 日志中的 Pause Young 等信息:说明分配行为确实会带来垃圾回收压力

7.5 再做两个延伸验证

你可以继续做下面两个实验:

  1. -Xmx32m 改成更小,例如 -Xmx16m
  2. LONG_LIVED.add(...) 的数量继续加大

你可以观察:

  • 堆越小,分配压力越容易更快转化为 GC 压力
  • 长寿命对象越多,存活对象越容易推高整体内存占用

7.6 TLAB 的可选观察方式

如果你想进一步观察 TLAB 行为,可以试试:

bash
java -Xms32m -Xmx32m -Xlog:gc+tlab=debug ObjectAllocationDemo

不同 JDK 版本日志内容会略有差异。
如果看到了和 TLAB 相关的日志,说明 JVM 确实在用线程本地分配缓冲区优化对象分配。

8. 练习建议

下面这些练习做完,这一章会更扎实:

  • 分析一个大量创建对象的示例
  • 对比短生命周期对象和长生命周期对象的行为差异
  • 用 GC 日志观察对象分配带来的年轻代回收
  • 总结对象创建流程图

9. 自测问题

  • 对象从 new 到可用大致经历了哪些步骤?
  • TLAB 的作用是什么?
  • 为什么对象分配行为会影响 GC?
  • 短命对象和长寿命对象对 JVM 行为的影响有什么不同?
  • 逃逸分析试图优化什么问题?

10. 自测核对要点

如果你的回答能覆盖下面这些点,说明这一章基本掌握到位了:

  • 对象创建包含类检查、分配、零值初始化、对象头设置和构造执行
  • TLAB 的目标是减少多线程对象分配竞争
  • 对象不只是字段数据,还包含对象头和对齐成本
  • 大量短命对象会显著增加 GC 压力
  • 对象生命周期和分配行为直接影响性能与回收表现