new 关键字触发类加载检查、堆内存分配、零值初始化、对象头设置及构造方法执行;其中堆分配受GC策略、TLAB、大对象阈值等影响,对象头含Mark Word和Class Pointer,字段在构造前已分配默认值,逃逸分析可能使对象栈上分配或标量替换。

new 关键字触发的堆内存分配步骤
Java 中 new 不是简单“划一块内存”就完事,它是一连串 JVM 层面的动作:类加载检查 → 分配内存 → 初始化零值 → 设置对象头 → 执行 <init></init> 方法。其中堆内存分配发生在第二步,且具体策略取决于垃圾收集器和堆布局。
常见错误现象:没报 OOM 却发现对象初始化慢、GC 频繁、或 OutOfMemoryError: Java heap space 突然出现——往往不是内存不够,而是分配策略失配(比如大对象直接进老年代触发 Full GC)。
- 使用场景:高频创建小对象(如 DTO、临时集合)时,分配速度影响明显;大对象(>8KB,默认阈值)可能绕过 Eden 区,直接进入老年代
- 参数差异:
-XX:PretenureSizeThreshold控制大对象直接入老年代的大小门槛;-XX:+UseTLAB(默认开启)让每个线程在 Eden 区预分配私有缓冲区,避免并发分配锁竞争 - 性能影响:关闭 TLAB 后多线程频繁
new会争抢 Eden 区指针,吞吐下降明显;而 TLAB 空间不足时,仍需同步分配,所以对象大小波动大会放大碎片问题
对象头里存了什么,为什么影响 new 的开销
每次 new 出来的对象,JVM 必须在堆上写入对象头(Object Header),至少包含 Mark Word 和 Class Pointer。这部分内存虽小(通常 12 或 16 字节),但它是分配后立即写入的强制操作,且 Mark Word 在锁升级、GC 标记、哈希码生成时还会动态变更。
容易踩的坑:以为对象刚 new 完就“稳定”了,其实 hashCode() 第一次调用会把 hash 值写进 Mark Word,如果此时对象正被其他线程标记为待回收,可能触发写屏障开销;另外,开启压缩指针(-XX:+UseCompressedOops)时,Class Pointer 占 4 字节而非 8 字节,能省空间但需额外解压运算。
立即学习“Java免费学习笔记(深入)”;
- Mark Word 内容随对象状态变化:无锁态存哈希码/分代年龄/锁标志位;轻量级锁态存指向栈中锁记录的指针
- Class Pointer 指向方法区的类元数据,若类未加载,
new会先触发类加载过程,延迟对象创建 - 64 位 JVM 下关闭压缩指针(
-XX:-UseCompressedOops)会使对象头变大,间接增加 GC 扫描压力
构造函数执行前,字段已经存在但未必是你想的值
new 分配完内存并清零后,字段就有了默认值(int 是 0、Object 是 null),但此时还没跑任何 Java 代码。构造函数里的逻辑(包括字段赋值、this 调用、super 调用)全在 <init></init> 方法里,属于分配完成后的独立阶段。
典型误用:在父类构造器中调用子类重写的方法(比如 this.doInit()),此时子类字段仍是默认值,导致 NPE 或计算错误——因为子类字段内存已分配、但尚未执行子类构造器里的赋值语句。
- 字段初始化顺序固定:父类静态块 → 子类静态块 → 父类实例变量赋值 & 实例块 → 父类构造器 → 子类实例变量赋值 & 实例块 → 子类构造器
- 编译器会把字段声明处的初始化(如
private List<string> list = new ArrayList();</string>)挪到构造器开头,紧挨着super()或this()调用之后 - 使用 Lombok 的
@Data或@Builder时,生成的构造器逻辑依赖字段声明顺序,字段循环引用会导致编译期或运行期异常
对象真的只在堆里吗?逃逸分析可能让它消失
JVM 并不保证所有 new 都落在堆上。当 JIT 编译器通过逃逸分析判定一个对象的引用不会离开当前方法或线程(即“不逃逸”),就可能直接将对象拆解成字段,分配在栈上(栈上分配)或者干脆优化掉(标量替换)。
这解释了为什么有些微基准测试里 new 速度极快——对象根本没进堆。但这个优化非常脆弱:只要对象被传给任意一个非内联方法、放入集合、或发布为静态变量,逃逸分析立刻失效,回归堆分配。
- 开启条件:
-XX:+DoEscapeAnalysis(JDK8 默认开启,JDK15+ 移除该选项,但优化仍存在) - 验证方式:加
-XX:+PrintEscapeAnalysis看日志,或用 JMH +-prof gc观察是否发生 GC - 注意:栈上分配不是“真在栈”,只是语义等价;且仅限于非逃逸的短生命周期对象,对 long-lived 对象无效
堆分配看似透明,但每一步都牵扯类加载、GC 策略、JIT 优化和硬件缓存行对齐。最常被忽略的是:你以为的“对象创建”其实跨了 JVM 多个子系统,任何一个环节卡住(比如类加载死锁、TLAB 耗尽、逃逸分析失败),都会让 new 表现出意料之外的延迟或内存行为。






