new Object() 到底做了啥?你以为只是一个“新建”,实则是一个“奇妙的诞生之旅”!

举报
喵手 发表于 2025/12/08 20:30:50 2025/12/08
【摘要】 开篇语哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,...

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛

  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。

  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!

0. 前言:那个让人心潮澎湃的 new 关键字!

在 Java 里,万物皆对象(除了基本类型,但它们也有包装类)。而创造对象,靠的就是那个简单到几乎可以忽略的 new 关键字。

MyClass myObj = new MyClass();

有没有觉得这句话就像电影里的**“芝麻开门!”?🚪 只要你一喊,JVM 就忙活开了,帮你变出一个活生生的对象。但这个“变”的过程,可不是简简单单地“啪”一下就完成了。它是一个精密的、多阶段的、充满细节的流程**!

而且,你知道你 new 出来的每一个对象,除了你自定义的那些成员变量,JVM 还偷偷给它加了个**“户口本”吗?这个“户口本”就是神秘的对象头(Object Header)**!

今天,咱们就一步一个脚印,把这个“出生”和“户口本”的秘密彻底扒干净!🕵️‍♂️


1. Java 对象:从“胚胎”到“呱呱坠地”的曲折过程

咱们说的这个“对象创建”,实际上是指当 JVM 遇到 new 指令时,从那一刻到对象可以被使用之前的一系列动作。它大致可以分为以下几个步骤:

阶段一:类加载检查 (Class Loading Check)

“喂!你说的这个 MyClass 是个什么鬼?我认识它吗?” 🤨
  JVM 首先会去运行时常量池找这个类的符号引用。如果找不到,或者发现这个类还没被加载、解析、初始化过,那就得先乖乖地把这个类加载进来!
  这就像你要生娃,得先看看你有没有结婚证、准生证!要是没有,那就先去把证件办齐了!📝

阶段二:分配内存 (Allocate Memory)

“行,知道 MyClass 是个好东西了!那它得住哪儿啊?” 🏠
  这一步就是为新对象分配内存空间。对象所需的内存大小在类加载完成后就确定了。主要在**堆(Heap)**上分配。
  分配内存的方式有两种,这取决于你的 JVM 是怎么玩的:

  1. 指针碰撞 (Pointer Bump):如果 Java 堆是规整的(即所有用过的内存都放在一边,空闲内存放在另一边),那么只需要一个指针不断地往空闲内存方向移动,挪出与对象大小相等的空间即可。快得飞起! 💨
  2. 空闲列表 (Free List):如果 Java 堆是不规整的(比如经过了标记-清除算法,内存碎片化严重),那就需要维护一个列表,记录哪些内存块是可用的。分配时从列表中找一块足够大的,然后更新列表。慢,但没办法! 🐌

并发问题? 多个线程同时 new 对象,都想抢同一块内存怎么办?
  JVM 也想到了!通常有两种方案:

  • CAS (Compare And Swap):通过 CAS + 重试来保证原子性。咱上一篇文章刚聊过,这玩意儿大家都很熟了!😏
  • TLAB (Thread Local Allocation Buffer)推荐这种! 😈 每个线程在 Java 堆中预先分配一小块自己的专属内存空间,就叫 TLAB。线程在 TLAB 里分配内存就不用同步了,只有 TLAB 用完了,才需要申请新的 TLAB,这时候才需要加锁。这就像是公司给每个员工发了小金库,大家在自己金库里花钱随便花,没钱了才去大金库领。效率高到爆! 🚀

阶段三:初始化零值 (Zero Initialize)

“房子有了,但是里面都是毛坯房啊!总得有点默认配置吧?” 🛋️
  内存分配完成后,JVM 会把分配到的内存空间(不包括对象头)都初始化为零值null0false)。
  这么做的好处是,即使你的代码里没有给成员变量赋初值,它们也能直接使用,得到一个默认的零值。这可比 C/C++ 里的随机值安全多了!👍

阶段四:设置对象头 (Set Object Header)

“嗯,毛坯房有了,那得给业主发个身份证、门牌号啥的吧?” 🏷️
  这一步就是把新对象的对象头信息填充进去。这可是个重要的步骤!后面咱们会重点聊。它包括了这个对象的哈希码、GC 分代年龄、锁状态、指向类元数据的指针等等。这是对象的“户口本”!

阶段五:执行 <init> 方法 (Execute Constructor)

“好了!现在有了地址,有了身份证,终于可以装修入住,开始‘新生活’了!” 🎉
  执行对象的构造函数(<init> 方法),按照程序员的意愿对对象进行初始化。成员变量赋初始值、执行业务逻辑等等。
  到这儿,一个活生生的、可以被程序使用的对象,就正式“呱呱坠地”了!👶

整个流程总结一下:

  1. 查户口:类加载检查
  2. 分地盘:分配内存 (指针碰撞/空闲列表,TLAB/CAS 搞定并发)
  3. 清零房:初始化零值
  4. 发证件:设置对象头
  5. 搞装修:执行构造方法

是不是感觉这 new 关键字背后,比你想象中复杂多了?😉


2. 对象头结构:你对象的“身份证”和“百宝箱”

现在咱们来揭秘那个神秘的对象头(Object Header)!它可不是一个简单的东西,它是对象在内存中的元数据,是 JVM 用来管理对象的关键信息。

对象头通常分为两部分:Mark Word (标记字段)Klass Pointer (类型指针)。如果对象是数组,还会有一个数组长度字段

2.1 Mark Word (标记字段)

这是对象头的核心! 也是最让人眼花缭乱的部分。它存储了对象运行时的各种状态信息。
  在 64 位 JVM 中,Mark Word 占用 8 字节(64 位)。但在 32 位 JVM 中,它只占用 4 字节。它的结构是动态的,会根据对象的状态变化而变化!这就像一个变色龙!🦎

Mark Word 通常包含以下信息(但不限于):

  • 哈希码 (HashCode):当对象第一次调用 hashCode() 方法时,这个哈希码就会被计算并存储在这里。

  • GC 分代年龄 (Age):对象在 Minor GC 中幸存的次数。每经历一次 Minor GC 且幸存,年龄就加 1。当年龄达到某个阈值(通常 15),就会晋升到老年代。

  • 锁标志位 (Lock Bits):这几个位是 Mark Word 中最重要的,它们定义了对象处于什么锁状态(无锁、偏向锁、轻量级锁、重量级锁、GC 标记)。

    • 无锁状态 (Normal):最原始的状态。
    • 偏向锁 (Biased Locking):针对只有一个线程访问的场景,避免 CAS 操作,效率最高。
    • 轻量级锁 (Lightweight Locking):当出现少量线程竞争时,通过 CAS 和自旋实现。
    • 重量级锁 (Heavyweight Locking):也就是我们熟悉的 synchronized,需要操作系统互斥量,开销最大。
    • GC 标记 (Marking):在垃圾回收期间,Mark Word 可能会被用来标记对象状态。
  • 偏向线程 ID (Biased Thread ID):如果对象处于偏向锁状态,会记录偏向的线程 ID。

  • Epoch:偏向锁的时代计数器。

看,一个 8 字节(或 4 字节)的 Mark Word 竟然塞了这么多东西!这简直就是个微缩版的情报局!🕵️‍♀️

2.2 Klass Pointer (类型指针)

这部分是对象头的另一个重要组成部分,它占用 4 字节(32 位 JVM)或 8 字节(64 位 JVM)。
  这个指针指向对象的类元数据 (Klass Metadata)
  通俗地讲,它告诉 JVM:“嘿!这个对象是 MyClass 类型!它的方法、字段定义都在 MyClass 的元数据里呢!”
  通过这个指针,JVM 才能知道这个对象是什么类型,它有哪些方法,有多少个成员变量等等。

指针压缩 (Compressed Ordinary Pointers):在 64 位 JVM 中,为了节省内存空间,JVM 会默认开启指针压缩。
  原本一个 64 位的指针需要 8 字节,压缩后只需要 4 字节!这样可以大大减少内存开销。但它不是没有代价的,每次访问都需要解压缩,会有一点点性能损耗。但总的来说,内存省下来了,GC 压力小了,大部分情况下是划算的!💰

2.3 数组长度 (Array Length)

如果对象是一个数组(比如 new int[10]),那么在对象头里还会多出一个 4 字节的数组长度字段,用来记录数组的长度。
  普通对象是没有这个字段的。

2.4 对象内存布局示意图(64位 JVM + 指针压缩)

--------------------------------------------------------------------------------------------------------------------
|                          对象头 (Object Header)                                      | 实例数据 (Instance Data) | 对齐填充 (Padding) |
--------------------------------------------------------------------------------------------------------------------
| Mark Word (8 字节)                                     | Klass Pointer (4 字节)    | 各种成员变量              | 凑够 8 字节倍数   |
| (哈希码、GC年龄、锁状态、偏向锁ID)                   | (指向类元数据)            | (从父类到子类)            |                   |
--------------------------------------------------------------------------------------------------------------------

如果是个数组:

--------------------------------------------------------------------------------------------------------------------
|                          对象头 (Object Header)                                      | 数组数据 (Array Data)    | 对齐填充 (Padding) |
--------------------------------------------------------------------------------------------------------------------
| Mark Word (8 字节)                                     | Klass Pointer (4 字节)    | 数组长度 (4 字节)        | 数组元素          | 凑够 8 字节倍数   |
--------------------------------------------------------------------------------------------------------------------

为啥需要对齐填充?
  因为 JVM 要求对象的大小必须是 8 字节的倍数!这样可以提高 CPU 访问内存的效率。如果对象实际大小不是 8 字节的倍数,就会自动补齐,这就是对齐填充(有时候也是内存浪费的一部分,但为了性能,值了!)。


3. 实战:自己动手看看对象头!

光说不练假把式!咱们可以用一个神器来窥探对象头JOL (Java Object Layout)

首先,你需要在你的 Maven 项目里添加依赖:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.17</version>
</dependency>

然后,写一段代码,打印出对象的布局:

import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;

// 运行参数:-XX:+UseCompressedOops (默认开启,用于64位JVM指针压缩)
// 如果想看未压缩的,可以 -XX:-UseCompressedOops
public class ObjectLayoutDemo {

    static class MyObject {
        int i;          // 4 字节
        boolean b;      // 1 字节
        String s;       // 8 字节 (引用类型,指针压缩后是 4 字节)
        long l;         // 8 字节
    }

    public static void main(String[] args) {
        System.out.println(VM.current().details()); // 打印 JVM 详情,包括是否启用指针压缩

        MyObject obj = new MyObject();
        System.out.println("----------------------------------------------");
        System.out.println("MyObject 对象的内存布局:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());

        System.out.println("----------------------------------------------");
        System.out.println("数组对象的内存布局:");
        int[] arr = new int[5];
        System.out.println(ClassLayout.parseInstance(arr).toPrintable());
    }
}

运行这段代码,你会得到类似这样的输出(不同 JVM 版本和配置可能略有差异):

# VM version: JDK 17.0.8, OpenJDK 64-Bit Server VM, 17.0.8+7
# ... (省略部分 JVM 信息)
# Object alignment: 8 bytes.
# Field sizes:
#   _byte: 1
#   _char: 2
#   _short: 2
#   _int: 4
#   _long: 8
#   _float: 4
#   _double: 8
#   _boolean: 1
#   _reference: 4 (Compressed Oops enabled)

----------------------------------------------
MyObject 对象的内存布局:
org.example.ObjectLayoutDemo$MyObject object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           3e 00 00 21 (00111110 00000000 00000000 00100001) (553648670)
     12     4    int MyObject.i                                0
     16     8   long MyObject.l                                0
     24     4 boolean MyObject.b                               false
     28     4 String MyObject.s                                null
     32     0        (loss due to the next object alignment)   
Instance size: 32 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

来,老哥给你划重点! 🤓

  • OFFSET 0-4-8:这就是对象头Mark Word 占了 8 字节,Klass Pointer 占了 4 字节(因为 Compressed Oops enabled)。
  • OFFSET 12MyObject.iint 类型,占 4 字节。
  • OFFSET 16MyObject.llong 类型,占 8 字节。
  • OFFSET 24MyObject.bboolean 类型,占 1 字节。
  • OFFSET 28MyObject.sString 类型,这是个引用,占 4 字节(因为指针压缩)。
  • Instance size: 32 bytes:总大小是 32 字节,是 8 的倍数,完美对齐!

数组的布局也很有意思:

----------------------------------------------
数组对象的内存布局:
[I object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           3e 00 00 21 (00111110 00000000 00000000 00100001) (553648670)
     12     4        (array length)                            5
     16    20    int [I.<elements>                             N/A
Instance size: 32 bytes

看!OFFSET 12 多了个 (array length) 字段,占了 4 字节! 这就是我前面说的数组特有的部分!

是不是感觉整个世界都清晰了? 🤩 你现在对 new 出来的对象,有了更深刻的理解!


4. 写在最后:知其然,更要知其所以然!

咱们今天聊的这些,从一个简单的 new 关键字,挖到了 JVM 内部的对象创建流程,再到每个对象自带的“身份证”——对象头。

这有什么用呢?

  • 当你调优 JVM 参数,比如 TLAB 大小、GC 策略时,你就能明白背后的原理。
  • 当你遇到内存泄漏或者 OOM 时,能够更准确地分析是不是大量小对象堆积或者大对象分配不当。
  • 当你深入理解 synchronized 锁升级过程时,你会发现 Mark Word 简直是主角!
  • 当你进行性能分析,想知道对象占用多少内存时,JOL 工具和对象头知识就是你的火眼金睛!

所以啊,兄弟们,别再把 Java 当成一个“黑盒”语言了!深入了解它,你才能真正驾驭它,成为那个能让 JVM 都“颤抖”的全栈高手!💪

好了,今天的探险之旅就到这里。记得实践一下 JOL,你会发现更多有趣的细节!咱们下期接着聊 JVM 那些不为人知的故事!👋😎

… …

文末

好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

… …

学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

wished for you successed !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。


版权声明:本文由作者原创,转载请注明出处,谢谢支持!

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。