new在堆上分配连续内存并返回首地址,栈中仅存该地址;它先分配内存再调用构造函数,支持继承链初始化,但对象实际存储于堆,类元数据共享于元空间。

new 关键字到底在内存里干了什么
它不是“创建对象”,而是「在堆上分配一块足够大的连续内存,并返回首地址」——这个地址被赋给引用变量。栈里只存这个地址,不存对象本身。
常见错误现象:NullPointerException 往往是因为栈中引用为 null,但代码误当作已分配的对象调用方法;根源常是忘了用 new,或 new 被条件分支跳过。
-
new触发类的<init></init>方法(即构造器),但构造器执行前,内存已分配完毕 - 如果类有父类,
new会递归触发父类构造,但所有字段内存都在子类new的那一瞬间统一分配 - JVM 可能对无逃逸对象做栈上分配(Escape Analysis 优化),但这是透明的,开发者不能依赖或控制
类是模板,对象是实例:但模板本身也占内存
类定义本身会被加载进方法区(JDK 8+ 是元空间),包含字段表、方法表、常量池等结构。一个类只加载一次,但可 new 出无数对象——每个对象独占堆内存,共享同一份类元数据。
使用场景:当你发现多个对象的 getClass() 返回同一个 Class 实例,或用 obj1.getClass() == obj2.getClass() 判断类型一致性时,背后就是这个机制在起作用。
- 静态字段属于类,存在元空间;实例字段属于对象,存在堆
- 类加载过程(加载 → 验证 → 准备 → 解析 → 初始化)中,“准备”阶段就为静态字段分配内存并设默认值,和
new无关 - 不要混淆
Class.forName("X")和new X():前者只加载类,后者才分配实例内存
不 new 就没有对象?也不一定
多数对象必须靠 new 分配,但有三类例外容易被忽略:
- 字符串字面量(如
"hello")由 JVM 在字符串常量池中复用,不一定触发new - 基本类型包装类(
Integer、Boolean等)在小值范围内(如Integer.valueOf(100))会走缓存,不 new - 反序列化(
ObjectInputStream.readObject())、反射(Constructor.newInstance())或Unsafe.allocateInstance()可绕过构造器直接分配内存
这些方式分配的内存仍位于堆,只是跳过了常规构造流程——这也是为什么反序列化对象的 transient 字段为默认值,且不会执行构造器逻辑。
调试时怎么看 new 的实际效果
光看代码不够,得观察运行时内存行为。推荐两个轻量手段:
- 加 JVM 参数
-XX:+PrintGCDetails -Xlog:gc*(JDK 11+)可看到每次 GC 前后堆内存变化,间接反映new频率 - 用
jmap -histo <pid></pid>查看当前堆中各类实例数量,验证是否意外泄漏(比如本该复用却反复new) - 注意:
System.identityHashCode(obj)返回的是对象内存地址哈希(非hashCode()重写结果),可用于确认两个引用是否指向同一块堆内存
真正难的不是理解 new,而是判断什么时候不该用它——比如 Builder 模式中缓存中间对象、池化资源(ThreadLocal 或对象池)或延迟初始化(Lazy<t></t>),都是为了避开无谓的堆分配。










