new Object() 到底做了啥?你以为只是一个“新建”,实则是一个“奇妙的诞生之旅”!
开篇语
哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区: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 是怎么玩的:
- 指针碰撞 (Pointer Bump):如果 Java 堆是规整的(即所有用过的内存都放在一边,空闲内存放在另一边),那么只需要一个指针不断地往空闲内存方向移动,挪出与对象大小相等的空间即可。快得飞起! 💨
- 空闲列表 (Free List):如果 Java 堆是不规整的(比如经过了标记-清除算法,内存碎片化严重),那就需要维护一个列表,记录哪些内存块是可用的。分配时从列表中找一块足够大的,然后更新列表。慢,但没办法! 🐌
并发问题? 多个线程同时 new 对象,都想抢同一块内存怎么办?
JVM 也想到了!通常有两种方案:
- CAS (Compare And Swap):通过 CAS + 重试来保证原子性。咱上一篇文章刚聊过,这玩意儿大家都很熟了!😏
- TLAB (Thread Local Allocation Buffer):推荐这种! 😈 每个线程在 Java 堆中预先分配一小块自己的专属内存空间,就叫 TLAB。线程在 TLAB 里分配内存就不用同步了,只有 TLAB 用完了,才需要申请新的 TLAB,这时候才需要加锁。这就像是公司给每个员工发了小金库,大家在自己金库里花钱随便花,没钱了才去大金库领。效率高到爆! 🚀
阶段三:初始化零值 (Zero Initialize)
“房子有了,但是里面都是毛坯房啊!总得有点默认配置吧?” 🛋️
内存分配完成后,JVM 会把分配到的内存空间(不包括对象头)都初始化为零值(null、0、false)。
这么做的好处是,即使你的代码里没有给成员变量赋初值,它们也能直接使用,得到一个默认的零值。这可比 C/C++ 里的随机值安全多了!👍
阶段四:设置对象头 (Set Object Header)
“嗯,毛坯房有了,那得给业主发个身份证、门牌号啥的吧?” 🏷️
这一步就是把新对象的对象头信息填充进去。这可是个重要的步骤!后面咱们会重点聊。它包括了这个对象的哈希码、GC 分代年龄、锁状态、指向类元数据的指针等等。这是对象的“户口本”!
阶段五:执行 <init> 方法 (Execute Constructor)
“好了!现在有了地址,有了身份证,终于可以装修入住,开始‘新生活’了!” 🎉
执行对象的构造函数(<init> 方法),按照程序员的意愿对对象进行初始化。成员变量赋初始值、执行业务逻辑等等。
到这儿,一个活生生的、可以被程序使用的对象,就正式“呱呱坠地”了!👶
整个流程总结一下:
- 查户口:类加载检查
- 分地盘:分配内存 (指针碰撞/空闲列表,TLAB/CAS 搞定并发)
- 清零房:初始化零值
- 发证件:设置对象头
- 搞装修:执行构造方法
是不是感觉这 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 12:MyObject.i,int类型,占 4 字节。OFFSET 16:MyObject.l,long类型,占 8 字节。OFFSET 24:MyObject.b,boolean类型,占 1 字节。OFFSET 28:MyObject.s,String类型,这是个引用,占 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 !!!
⭐️若喜欢我,就请关注我叭。
⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。
版权声明:本文由作者原创,转载请注明出处,谢谢支持!
- 点赞
- 收藏
- 关注作者
评论(0)