你知道创建一个Java对象在内存中占多少字节吗对象头里都存了什么为什么JVM内存不建议超过32G指针压缩是如何工作的本文将深入剖析Java对象的内存布局详解对象头、实例数据、对齐填充三大组成部分揭秘指针压缩的实现原理对比句柄访问与直接指针访问的区别助你彻底掌握JVM内存模型 文章目录一、Java对象内存布局概述二、对象头Header三、实例数据Instance Data四、对齐填充Padding五、对象访问方式句柄 vs 直接指针六、指针压缩Compressed Oops七、JVM内存模型与对象创建过程八、高频面试题解析一、Java对象内存布局概述1.1 对象内存结构一个Java对象在内存中包括3个部分┌─────────────────────────────────────┐ │ Java对象内存结构 │ ├─────────────────────────────────────┤ │ │ │ ┌─────────────────────────────┐ │ │ │ 对象头 (Header) │ │ │ │ ┌─────────────────────┐ │ │ │ │ │ Mark Word │ │ │ │ │ │ (标记信息) │ │ │ │ │ └─────────────────────┘ │ │ │ │ ┌─────────────────────┐ │ │ │ │ │ Class Pointer │ │ │ │ │ │ (类型指针) │ │ │ │ │ └─────────────────────┘ │ │ │ │ ┌─────────────────────┐ │ │ │ │ │ Array Length │ │ │ │ │ │ (数组长度可选) │ │ │ │ │ └─────────────────────┘ │ │ │ └─────────────────────────────┘ │ │ │ │ ┌─────────────────────────────┐ │ │ │ 实例数据 (Instance Data) │ │ │ │ - 字段值 │ │ │ │ - 继承的字段 │ │ │ └─────────────────────────────┘ │ │ │ │ ┌─────────────────────────────┐ │ │ │ 对齐填充 (Padding) │ │ │ │ - 8字节对齐 │ │ │ └─────────────────────────────┘ │ │ │ └─────────────────────────────────────┘1.2 大小端存储大端存储Big Endian高位字节存放在低地址便于数据类型的符号判断Java使用大端存储小端存储Little Endian低位字节存放在低地址便于数据类型转换如long转int时直接截断高地址二、对象头Header2.1 Mark Word标记字Mark Word用于存储对象自身的运行时数据如哈希码、GC分代年龄、锁状态标志等。32位JVM的Mark Word结构┌────────────────────────────────────────────────────────┐ │ 锁状态 │ 25bit hashCode │ 4bit │ 1bit │ 2bit │ │ │ │ 分代年龄 │ 偏向锁 │ 锁标志 │ ├──────────┼──────────────────┼────────┼────────┼────────┤ │ 无锁 │ hashCode │ age │ 0 │ 01 │ │ 偏向锁 │ threadID(23) epoch(2) │ age │ 1 │ 01 │ │ 轻量级锁 │ ptr to lock record │ │ 00 │ │ 重量级锁 │ ptr to monitor │ │ 10 │ │ GC标记 │ │ │ 11 │ └──────────┴──────────────────┴────────┴────────┴────────┘64位JVM的Mark Word结构┌─────────────────────────────────────────────────────────────────────┐ │ 锁状态 │ 31bit unused/hash │ 4bit │ 1bit │ 2bit │ unused │ │ │ │ 分代年龄 │ 偏向锁 │ 锁标志 │ │ ├──────────┼─────────────────────┼────────┼────────┼────────┼─────────┤ │ 无锁 │ unused(25)hash(31)│ age │ 0 │ 01 │ 1bit │ │ 偏向锁 │ threadIDepochage │ age │ 1 │ 01 │ 1bit │ │ 轻量级锁 │ ptr to lock record (62bit) │ 00 │ │ │ 重量级锁 │ ptr to monitor (62bit) │ 10 │ │ │ GC标记 │ │ 11 │ │ └──────────┴─────────────────────┴────────┴────────┴────────┴─────────┘2.2 Class Pointer类型指针Class Pointer指向对象的类元数据JVM通过这个指针确定对象是哪个类的实例。开启指针压缩时4字节关闭指针压缩时8字节2.3 Array Length数组长度如果是数组对象对象头中还有一块用于记录数组长度的数据。占用4字节三、实例数据Instance Data实例数据存储对象的所有字段内容包括继承自父类的字段。3.1 字段排序策略JVM分配策略参数-XX:FieldsAllocationStyle策略值排序规则0基本类型 → 填充字段 → 引用类型1引用类型 → 基本类型 → 填充字段2父类引用类型 子类引用类型 → 基本类型 → 填充字段3.2 字段类型大小类型大小boolean1字节byte1字节short2字节char2字节int4字节float4字节long8字节double8字节reference开启压缩4字节reference未压缩8字节四、对齐填充Padding4.1 为什么需要对齐填充目的提高CPU访问数据的效率问题场景64位CPU一次可以读取8字节64bit如果数据跨内存地址区域存储需要多次读取4.2 对齐填充示例未对齐情况内存地址 0x00-0x07: [数据A: 4字节][数据B前4字节] 0x08-0x0F: [数据B后4字节][数据C: 4字节] 读取long类型数据B - 第一次读取 0x00-0x07获得前4字节 - 第二次读取 0x08-0x0F获得后4字节 - 合并两次结果对齐后情况内存地址 0x00-0x07: [数据A: 4字节][填充: 4字节] 0x08-0x0F: [数据B: 8字节完整存储] 0x10-0x17: [数据C: 4字节][填充: 4字节] 读取long类型数据B - 一次读取 0x08-0x0F直接获得完整数据4.3 对齐规则默认按8字节对齐对象总大小必须是8字节的整数倍五、对象访问方式句柄 vs 直接指针5.1 句柄池访问栈中的reference │ ▼ ┌─────────────┐ │ 句柄池 │ │ (Handle) │ │ ┌───────┐ │ │ │ 实例 │──┼──► 堆中的对象实例 │ │ 指针 │ │ │ ├───────┤ │ │ │ 类型 │──┼──► 方法区中的类元数据 │ │ 指针 │ │ │ └───────┘ │ └─────────────┘优点reference存储稳定的句柄地址对象移动时只需改变句柄中的指针reference本身不变缺点增加一次指针定位的时间开销5.2 直接指针访问栈中的reference │ ▼ ┌───────────────────┐ │ 堆中的对象 │ │ ┌─────────────┐ │ │ │ 对象头 │ │ │ │ ┌───────┐ │ │ │ │ │Class │──┼──┼──► 方法区中的类元数据 │ │ │Pointer│ │ │ │ │ └───────┘ │ │ │ └─────────────┘ │ │ ┌─────────────┐ │ │ │ 实例数据 │ │ │ └─────────────┘ │ └───────────────────┘优点节省一次指针定位的开销访问速度更快缺点对象移动时需要修改referenceHotSpot虚拟机使用直接指针访问。六、指针压缩Compressed Oops6.1 什么是指针压缩OOPOrdinary Object Pointer普通对象指针指针压缩将64位指针压缩为32位减少内存占用。6.2 指针压缩的目的保证CPU OOP缓存32位指针更容易被CPU缓存减少GC发生指针从8字节变为4字节节省堆空间6.3 指针压缩的启用条件条件说明64位操作系统必须是64位JVM堆内存 4G内存大于4G时默认开启堆内存 32G超过32G压缩失效6.4 为什么超过32G压缩失效原理分析32位寻址空间 2^32 4GB 指针压缩后 - 实际存储的是偏移量非完整地址 - 按8字节对齐4G × 8 32G 因此 - 堆内存 ≤ 32G可以用压缩指针表示 - 堆内存 32G压缩指针无法表示完整地址压缩失效6.5 指针压缩参数# 开启指针压缩默认开启-XX:UseCompressedOops# 关闭指针压缩-XX:-UseCompressedOops# 压缩类指针JDK8默认开启-XX:UseCompressedClassPointers6.6 生产环境建议JVM内存不要超过32G超过32G会导致指针压缩失效对象引用从4字节变为8字节内存占用反而增加。推荐配置# 如果业务需要大内存可以- 使用G1/ZGC垃圾收集器 - 堆内存设置为31G保留压缩 - 或者使用多实例部署七、JVM内存模型与对象创建过程7.1 JVM内存结构┌─────────────────────────────────────────────────────────────┐ │ JVM内存模型 │ ├─────────────────────────────────────────────────────────────┤ │ 非堆区Non-Heap │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ 方法区Method Area / Metaspace │ │ │ │ - 类信息 │ │ │ │ - 静态变量 │ │ │ │ - 常量池 │ │ │ │ - 即时编译后的代码 │ │ │ └───────────────────────────────────────────────────────┘ │ ├─────────────────────────────────────────────────────────────┤ │ 堆区Heap │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ 老年代Old Generation │ │ │ │ - 存放长期存活的对象 │ │ │ └───────────────────────────────────────────────────────┘ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ 年轻代Young Generation │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │ │ Survivor区S0 S1 │ │ │ │ │ │ - From Space │ │ │ │ │ │ - To Space │ │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │ │ Eden区 │ │ │ │ │ │ - 新对象优先分配 │ │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ └───────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘7.2 对象创建过程我是一个普通的Java对象... 出生在 Eden区 ↓ Eden满了触发Minor GC ↓ 存活下来进入 Survivor区的From区 ↓ 每次Minor GC在S0和S1之间来回移动 ↓ 年龄达到16岁默认 ↓ 晋升到 老年代Old Generation ↓ 在老年代度过余生...7.3 触发Full GC的条件老年代剩余空间 历次晋升平均大小基于历史平均水平Young GC后存活对象 老年代剩余空间基于预测Meta Space区域空间不足System.gc()被调用7.4 方法区演变版本实现存储位置特点JDK 1.7及之前Perm Space永久代JVM内存线性整理增加GC时间JDK 1.8Meta Space元空间直接内存减少内存碎片节省压缩时间八、高频面试题解析Q1: 为什么需要Survivor区只有Eden不行吗答如果没有SurvivorEden每进行Minor GC存活对象直接送入老年代老年代很快被填满触发Full GCFull GC执行时间远长于Minor GC影响程序响应速度Survivor作为缓冲减少进入老年代的对象数量减少Full GC频率Q2: 为什么需要两个Survivor区答解决内存碎片化问题如果只有一个Survivor区Eden和Survivor的存活对象混合存放内存不连续产生碎片使用两个Survivor区S0、S1永远有一个Survivor区是空的复制算法保证内存连续无碎片Q3: 新生代Eden:S1:S2为什么是8:1:1答新生代可用内存 : 担保内存 9:1复制算法Eden : Survivor 8:1在可用内存中因此 Eden:S1:S2 8:1:1IBM研究表明新生代对象98%是朝生夕死的Q4: 堆内存都是线程共享的吗答大部分区域是共享的Eden、OldTLAB是线程私有的Thread Local Allocation Buffer线程本地分配缓冲区默认在Eden区为每个线程开辟小对象优先在TLAB分配加速对象分配大对象直接在共享区域分配Q5: 一个对象占多少内存答对象大小 对象头 实例数据 对齐填充 示例Object obj new Object(); - 64位JVM开启压缩 - Mark Word: 8字节 - Class Pointer: 4字节压缩后 - 实例数据0字节 - 对齐填充4字节 - 总计16字节 示例String str new String(abc); - 需要考虑String对象 char[]数组 - String对象头 字段hash、char[]引用 对齐 - char[]对象头 数组长度 3个charQ6: 指针压缩的原理是什么答利用64位指针的高16位未使用实际使用42位表示地址最大4TB存储时去掉3个最低位的0按8字节对齐使用时恢复3个0因此4字节可以表示32GB内存地址总结核心知识点回顾知识点要点对象内存结构对象头 实例数据 对齐填充对象头Mark Word Class Pointer Array Length指针压缩堆内存32G时有效超过32G失效对象访问HotSpot使用直接指针访问对齐填充8字节对齐提高CPU访问效率TLAB线程私有加速对象分配常用参数速查# 查看对象内存布局需要Java Object Layout工具java-jarjol-cli.jar internals java.lang.Object# 指针压缩-XX:UseCompressedOops-XX:-UseCompressedOops# TLAB相关-XX:UseTLAB-XX:TLABSize1024# 打印类加载信息-XX:TraceClassLoading# 字段分配策略-XX:FieldsAllocationStyle0关键词Java对象内存布局, 对象头, Mark Word, 指针压缩, Compressed Oops, 句柄访问, 直接指针, 对齐填充, TLAB, JVM内存模型如果本文对你有帮助欢迎点赞、收藏、关注有任何JVM内存相关问题欢迎在评论区留言讨论。