全文目录开篇语0. 前言那个让人心潮澎湃的 new 关键字1. Java 对象从“胚胎”到“呱呱坠地”的曲折过程阶段一类加载检查 (Class Loading Check)阶段二分配内存 (Allocate Memory)阶段三初始化零值 (Zero Initialize)阶段四设置对象头 (Set Object Header)阶段五执行 init 方法 (Execute Constructor)整个流程总结一下2. 对象头结构你对象的“身份证”和“百宝箱”2.1 Mark Word (标记字段)2.2 Klass Pointer (类型指针)2.3 数组长度 (Array Length)2.4 对象内存布局示意图64位 JVM 指针压缩3. 实战自己动手看看对象头4. 写在最后知其然更要知其所以然文末开篇语哈喽各位小伙伴们你们好呀我是喵手。运营社区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)|--------------------------------------------------------------------------------------------------------------------|MarkWord(8字节)|KlassPointer(4字节)|各种成员变量|凑够8字节倍数||(哈希码、GC年龄、锁状态、偏向锁ID等)|(指向类元数据)|(从父类到子类)||--------------------------------------------------------------------------------------------------------------------如果是个数组--------------------------------------------------------------------------------------------------------------------|对象头(Object Header)|数组数据(Array Data)|对齐填充(Padding)|--------------------------------------------------------------------------------------------------------------------|MarkWord(8字节)|KlassPointer(4字节)|数组长度(4字节)|数组元素|凑够8字节倍数|--------------------------------------------------------------------------------------------------------------------为啥需要对齐填充因为 JVM 要求对象的大小必须是 8 字节的倍数这样可以提高 CPU 访问内存的效率。如果对象实际大小不是 8 字节的倍数就会自动补齐这就是对齐填充有时候也是内存浪费的一部分但为了性能值了。3. 实战自己动手看看对象头光说不练假把式咱们可以用一个神器来窥探对象头JOL (Java Object Layout)。首先你需要在你的 Maven 项目里添加依赖dependencygroupIdorg.openjdk.jol/groupIdartifactIdjol-core/artifactIdversion0.17/version/dependency然后写一段代码打印出对象的布局importorg.openjdk.jol.info.ClassLayout;importorg.openjdk.jol.vm.VM;// 运行参数-XX:UseCompressedOops (默认开启用于64位JVM指针压缩)// 如果想看未压缩的可以 -XX:-UseCompressedOopspublicclassObjectLayoutDemo{staticclassMyObject{inti;// 4 字节booleanb;// 1 字节Strings;// 8 字节 (引用类型指针压缩后是 4 字节)longl;// 8 字节}publicstaticvoidmain(String[]args){System.out.println(VM.current().details());// 打印 JVM 详情包括是否启用指针压缩MyObjectobjnewMyObject();System.out.println(----------------------------------------------);System.out.println(MyObject 对象的内存布局);System.out.println(ClassLayout.parseInstance(obj).toPrintable());System.out.println(----------------------------------------------);System.out.println(数组对象的内存布局);int[]arrnewint[5];System.out.println(ClassLayout.parseInstance(arr).toPrintable());}}运行这段代码你会得到类似这样的输出不同 JVM 版本和配置可能略有差异#VMversion:JDK17.0.8,OpenJDK64-Bit ServerVM,17.0.87#...(省略部分JVM信息)# Object alignment:8bytes.# 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:OFFSETSIZETYPEDESCRIPTIONVALUE04(object header)01000000(00000001000000000000000000000000)(1)44(object header)00000000(00000000000000000000000000000000)(0)84(object header)3e000021(00111110000000000000000000100001)(553648670)124int MyObject.i0168long MyObject.l0244boolean MyObject.bfalse284String MyObject.snull320(loss due to the next object alignment)Instance size:32bytes Space losses:0bytes internal0bytes external0bytes 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 的倍数完美对齐数组的布局也很有意思----------------------------------------------数组对象的内存布局[Iobject internals:OFFSETSIZETYPEDESCRIPTIONVALUE04(object header)01000000(00000001000000000000000000000000)(1)44(object header)00000000(00000000000000000000000000000000)(0)84(object header)3e000021(00111110000000000000000000100001)(553648670)124(array length)51620int[I.elementsN/AInstance size:32bytes看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 ⭐️若喜欢我就请关注我叭。⭐️若对您有用就请点赞叭。⭐️若有疑问就请评论留言告诉我叭。版权声明本文由作者原创转载请注明出处谢谢支持