对象头存mark word和class pointer;hashcode()调用后哈希值写入mark word并固化,覆盖偏向锁信息,导致锁撤销且影响内存布局。

对象头里存了啥,为什么 hashCode() 会改变对象内存布局
Java 对象在堆中不是随便放的,对象头是 JVM 管理对象的“身份证”。它通常包含两部分:Mark Word 和 Class Pointer(开启压缩指针时占 4 字节,否则 8 字节)。Mark Word 复用程度极高——锁状态、GC 分代年龄、偏向线程 ID 全挤在这 8 字节里。重点来了:调用过 Object.hashCode() 的对象,JVM 会在 Mark Word 中写入哈希值,这会覆盖原本可能存放的偏向锁信息;一旦写入,这个哈希值就固化了,哪怕对象被移动(GC 搬家),JVM 也会从对象头里读出原值,不再重新计算。
常见错误现象:System.identityHashCode(obj) 和 obj.hashCode() 在重写了 hashCode() 的类上结果不同,但如果你没重写,又发现两次调用 obj.hashCode() 返回值不一致——那说明对象还没被“哈希固化”,且期间发生过 GC 导致对象移动、JVM 重算了地址哈希(仅限未调用过、未锁、未进入老年代的对象)。
- 不要依赖未重写的
hashCode()做唯一标识,尤其在缓存 key 或序列化场景 - 开启
-XX:+UseBiasedLocking(JDK 15+ 默认关闭)时,偏向锁标记和哈希值互斥,首次调用hashCode()会直接撤销偏向 -
Mark Word在 64 位 JVM 上默认 8 字节,但启用-XX:+UseCompressedOops(默认开)且堆 ≤ 32GB 时,Class Pointer压缩为 4 字节,对象头共 12 字节(非 16)
实例数据排列顺序真由字段声明顺序决定吗
不一定。JVM 会对字段做**重排序(reordering)**,目标是减少内存空洞、提升 CPU 缓存行利用率。规则是:按宽度从大到小排——long / double → int / float → char / short → byte / boolean → 引用类型(压缩后 4 字节)。同一宽度字段才按源码顺序排。
使用场景:写高性能缓存或需要精确控制对象大小(如 Netty 的 ByteBuf 内部对象)、排查 false sharing(伪共享)问题时,必须看真实布局,不能信源码顺序。
立即学习“Java免费学习笔记(深入)”;
- 用
Unsafe.objectFieldOffset()或 JOL(Java Object Layout)工具验证实际偏移,例如new org.openjdk.jol.vm.VM().details() - 把高频访问的字段(如
volatile int state)和相邻字段尽量凑成 64 字节缓存行内,避免跨行;但别手动插padding字段——JVM 不保证它们不被重排 - 子类字段永远排在父类字段之后,哪怕父类字段是
long、子类是int
对齐填充不是凑整数,而是对齐 CPU 缓存行
对象总大小必须是 8 字节的整数倍,这是 JVM 的硬性要求,不是为了“好看”。真正关键的是:现代 CPU 以 64 字节缓存行为单位加载内存。如果两个频繁修改的变量落在同一缓存行,即使逻辑无关,也会因总线锁导致性能陡降(false sharing)。JVM 的对齐填充只解决前者(8 字节对齐),后者得靠程序员干预。
性能影响明显:在高并发计数器场景,未隔离的 long counter 可能比加了 7 个 long p0..p6 填充的版本慢 3–5 倍。
- 不要以为加了
transient就能跳过对齐——它只影响序列化,不影响内存布局 -
java.lang.Thread类内部用了@Contended注解(需-XX:+UnlockExperimentalVMOptions -XX:+RestrictContended)来强制字段独占缓存行,这是比手写 padding 更可靠的方案(JDK 8+) - 数组对象额外有 4 字节的
length字段,所以new int[0]占 16 字节(对象头 12 + length 4),而new long[0]也是 16 字节,不是 12
32 位 vs 64 位 JVM 下对象大小差异在哪
差别集中在两处:对象头里的 Class Pointer 和所有引用类型字段。64 位 JVM 默认引用占 8 字节,32 位占 4 字节;但只要开启 -XX:+UseCompressedOops(JDK 7u40+ 默认开启),且堆 ≤ 32GB,引用就压成 4 字节——此时对象头从 16 字节(64 位无压缩)变成 12 字节,普通对象内存占用接近 32 位 JVM。
容易踩的坑:本地开发用默认配置(堆小、压缩开启),上线却配了 40GB 堆但忘了关压缩或没意识到已失效,结果对象头涨回 16 字节,引用变 8 字节,集合类内存暴增 30%+。
- 用
jstat -gc <pid></pid>看OC(old capacity)和OU(old used)变化趋势,突增可能就是压缩失效了 -
-XX:+PrintCompressedOopsMode启动时会打印压缩是否生效及基址,务必检查 - 数组引用(如
String[])同样受压缩影响,但byte[]这种 primitive 数组不涉及引用,大小不变
对象布局不是静态图纸,它是 JVM、CPU 架构、GC 策略和代码行为共同作用的动态结果。光看《深入理解 JVM》里的图不够,得用 JOL 验证、用 jstat 观察、在 GC 日志里找线索——尤其是当对象大小和预期差 4 或 8 字节时,八成是压缩开关或字段重排序在捣鬼。









