
本篇是「JVM 与性能调优系列」第 2 篇。你每天new成百上千个对象但有没有想过这行new Object()创造出来的东西在内存里到底占多少字节、长什么样很多人以为「对象就是几个字段加起来的大小」结果线上一算内存占用差出好几倍。今天我们用java -XX:PrintFieldLayout之外的更直观方式把对象拆开看个明白。一、一个对象 对象头 实例数据 对齐填充在 HotSpot 里对象在堆中的布局分三块| 对象头 Mark Word | 对象头 Klass Pointer | 实例数据 | 对齐填充 |对象头Header固定开销承载「对象自己的元数据」实例数据Instance Data你定义的字段包括父类继承来的对齐填充PaddingJVM 要求对象大小是 8 字节的整数倍不足就补二、对象头之 Mark Word对象的「身份证」Mark Word 在 64 位 JVM 上占8 字节存的是和对象状态强相关的运行时数据而且同一块 8 字节在不同状态下存的东西会变状态Mark Word 存的内容无锁哈希码(hashCode) 分代年龄 是否偏向锁标志偏向锁偏向的线程 ID轻量级锁指向栈中锁记录的指针重量级锁指向互斥量(monitor)的指针GC 标记空被标记时复用看到没分代年龄就在这 8 字节里——占 4 个比特位所以最大就是 15MaxTenuringThreshold默认 15 不是拍脑袋定的是位数限制。hashCode()第一次调用才会写进 Mark Word这也是为什么未调用过hashCode的对象和调用过的对象布局不同。三、对象头之 Klass Pointer指向「类」的指针Klass Pointer 指向方法区的类元数据Klass 结构JVM 靠它知道「这个对象是哪个类的实例」。指针压缩是关键优化64 位 JVM 指针本应 8 字节但开启-XX:UseCompressedOopsJDK 6 起默认开后Klass Pointer 压成4 字节。原理是堆上限通常 32G用「基址 偏移×8」就能寻址省下一半头开销。一旦堆超过约 32G压缩自动失效对象头回到 8 字节内存占用明显上升——这就是为什么堆不是越大越好32G 是条隐形的分水岭。四、实例数据字段怎么排字段按类型宽度排列相同宽度会分到一起顺序大致是long/double→int/float→short/char→byte/boolean→ 引用类型。父类字段在子类字段之前。引用类型字段在开启指针压缩时也只占 4 字节。所以一个只有一个int字段的对象8(Mark) 4(Klass) 4(int) 16 字节正好 8 的倍数无需填充。五、数组对象多了「长度」数组对象比普通对象多一个4 字节的长度字段因为数组需要知道自己多长所以new int[0]也要 16 字节8 4 4(长度) 16。六、算一笔账为什么你的对象比你以为的大一个看似轻量的Point(x, y)只有两个int8(Mark) 4(Klass) 4(x) 4(y) 20 → 对齐到 24 字节如果换成两个Integer对象引用每个Integer本身又是 16 字节对象 4 字节引用开销直接翻倍。这就是大量小对象场景里用基本类型数组int[]比ListInteger省内存几个数量级的原因——也是调优时「对象膨胀」排查的着眼点。七、对齐填充的副作用伪共享铺垫填充除了对齐有时还被用来故意撑大对象避免伪共享两个变量在同一缓存行被不同 CPU 核心改写互相失效。第 12 篇讲性能时会再提Contended。这里先记住对齐不是浪费是硬件友好的代价。八、亲手量一量JOL 工具别光听我说OpenJDK 有个JOLJava Object Layout工具能打印真实布局System.out.println(ClassLayout.parseInstance(newPoint(1,2)).toPrintable());输出会清楚列出 Mark Word、Klass Pointer、各字段的偏移(offset)与大小以及末尾 padding。强烈建议你在自己项目里跑一遍——你会发现很多「以为很小」的对象其实带着 12~16 字节的固定税。九、字段顺序也影响内存前面说相同宽度的字段会聚到一起。如果你把boolean和long交错定义JVM 仍会按宽度重排但父类字段永远先于子类这一条无法改变。所以继承层级越深对象头前面的「父类实例数据」就越多。高频小对象如 DTO、事件对象应尽量减少不必要的父类字段避免无谓膨胀。十、压缩指针何时失效前面说开启-XX:UseCompressedOops后 Klass Pointer 压成 4 字节。但有个硬限制压缩指针用「基址 偏移×8」寻址能覆盖的最大堆是32G 左右4G × 8 32G实际约 32G 出头。一旦你设-Xmx40gJVM自动关闭压缩指针Klass Pointer 变回 8 字节每个对象头多 4 字节。看似只多 4 字节但在十亿级对象的内存里这就是几个 G 的差距——而且大堆本身 GC 更慢。结论除非真需要别轻易把堆推过 32G。超过这条线往往不如「两个 30G 实例」来得划算。十一、对象头与锁升级的伏笔本篇提到 Mark Word 在不同锁状态下存不同内容偏向锁/轻量/重量。这其实是后面「并发与锁」的入口当多个线程竞争同一个对象JVM 会让它的 Mark Word 在几种状态间升级升级过程就记录在对象头那 8 字节里。今天我们只要记住对象头不是死数据它动态记录着对象的运行时状态锁、哈希、年龄。理解它才能理解后面为什么synchronized有时快、有时慢。总结一个对象 8 字节 Mark Word锁/哈希/年龄 4 字节 Klass 指针压缩后 实例数据 对齐填充。指针压缩让我们在 32G 堆下省一半头开销分代年龄被锁死在 15数组多一个长度字段。下次有人问「这个对象多大」别只加字段——把 12 字节固定头和对齐算进去才靠谱。