对象创建与内存分配
1. 这是什么
对象创建与内存分配描述了一个 Java 对象从 new 开始,到进入堆中被 JVM 管理的整个过程。
这是理解性能、GC 和内存行为的关键一环。
一句话理解:
new只是源码里的一个动作- 真正运行时还会经历类检查、内存分配、初始化和构造执行
2. 为什么重要
对象创建频率和分配方式会直接影响这些事情:
- 程序吞吐
- 堆内存占用
- GC 压力
- 延迟抖动
很多性能问题并不是“算法慢”,而是:
- 创建了太多短命对象
- 让本来应该很快分配的对象不断制造 GC 压力
3. 先建立直觉:对象创建为什么平时很快
很多人知道“对象创建有成本”,但又看到 Java 里大量 new 似乎也能跑得很快。
这是因为 JVM 在常见场景下做了很多优化,例如:
- 堆上顺序分配
- TLAB 线程本地分配缓冲区
- 对象头和内存布局优化
- 逃逸分析下的标量替换 / 栈上分配机会
所以更准确的理解是:
- 对象创建不一定慢
- 但大量对象分配和回收仍然会明显影响系统行为
4. 核心内容
4.1 对象从 new 到可用,大致经历什么
可以先用一个工程上足够实用的顺序理解:
- 检查类是否已加载、解析、初始化
- 为对象分配内存
- 把分配到的内存初始化为零值
- 设置对象头
- 执行构造方法,把对象变成业务意义上“可用”
这也是为什么:
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,内容如下:
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 编译并运行
先编译:
javac ObjectAllocationDemo.java再用较小堆和 GC 日志运行:
java -Xms32m -Xmx32m -Xlog:gc* ObjectAllocationDemo如果你只想先看程序输出,也可以直接:
java ObjectAllocationDemo7.3 你应该观察到什么
程序输出中应包含这些关键信息:
classLoadedAndMainStarted=true
firstUserCreated=true
shortLivedAllocationRounds=3
longLivedObjects=10
demoFinished=true如果使用 GC 日志运行,还应看到 GC 相关日志输出,通常会包含类似:
Pause Young
Eden
Heap具体格式和内容会因 JDK 版本不同而略有差异,但你通常能观察到:
- 短命对象分配会触发年轻代回收
- 堆容量较小时,GC 日志会更明显
7.4 每一行在验证什么
classLoadedAndMainStarted=true:说明对象创建是运行时流程的一部分,程序主流程开始后才发生实际分配firstUserCreated=true:说明执行new后对象实例已成功创建shortLivedAllocationRounds=3:说明程序中确实制造了大量短命对象longLivedObjects=10:说明被长期持有的对象会留在可达集合中- GC 日志中的
Pause Young等信息:说明分配行为确实会带来垃圾回收压力
7.5 再做两个延伸验证
你可以继续做下面两个实验:
- 把
-Xmx32m改成更小,例如-Xmx16m - 把
LONG_LIVED.add(...)的数量继续加大
你可以观察:
- 堆越小,分配压力越容易更快转化为 GC 压力
- 长寿命对象越多,存活对象越容易推高整体内存占用
7.6 TLAB 的可选观察方式
如果你想进一步观察 TLAB 行为,可以试试:
java -Xms32m -Xmx32m -Xlog:gc+tlab=debug ObjectAllocationDemo不同 JDK 版本日志内容会略有差异。
如果看到了和 TLAB 相关的日志,说明 JVM 确实在用线程本地分配缓冲区优化对象分配。
8. 练习建议
下面这些练习做完,这一章会更扎实:
- 分析一个大量创建对象的示例
- 对比短生命周期对象和长生命周期对象的行为差异
- 用 GC 日志观察对象分配带来的年轻代回收
- 总结对象创建流程图
9. 自测问题
- 对象从
new到可用大致经历了哪些步骤? - TLAB 的作用是什么?
- 为什么对象分配行为会影响 GC?
- 短命对象和长寿命对象对 JVM 行为的影响有什么不同?
- 逃逸分析试图优化什么问题?
10. 自测核对要点
如果你的回答能覆盖下面这些点,说明这一章基本掌握到位了:
- 对象创建包含类检查、分配、零值初始化、对象头设置和构造执行
- TLAB 的目标是减少多线程对象分配竞争
- 对象不只是字段数据,还包含对象头和对齐成本
- 大量短命对象会显著增加 GC 压力
- 对象生命周期和分配行为直接影响性能与回收表现