![图片[1]-10.深入解密Java对象内存布局——从对象头到字段重排列-速优课](http://www.suyouke.com/wp-content/uploads/2026/07/doubao_img_2304x1728_20260722_174942-1024x768.png)
本文原创,转载请注明出处。关注博主,持续分享Java进阶知识。
一、开篇:Java对象的诞生之旅
在Java程序中,我们有多种创建对象的方式:
| 创建方式 | 说明 | 是否初始化字段 |
| new语句 | 最常见的方式 | 是(调用构造器) |
| 反射机制 | 通过Class.newInstance() | 是(调用构造器) |
| Object.clone() | 复制已有对象 | 是(复制数据) |
| 反序列化 | 从字节流恢复对象 | 是(复制数据) |
| Unsafe.allocateInstance() | 直接分配内存 | 否(零初始化) |
new语句的字节码分析:
// Foo foo = new Foo(); 编译后的字节码
0 new Foo // 1. 分配内存
3 dup // 2. 复制栈顶引用
4 invokespecial Foo() // 3. 调用构造器
7 astore_1 // 4. 存储到局部变量
构造器的调用链:
子类构造器 → 父类构造器 → ... → Object构造器
↓ ↓ ↓
初始化子类字段 初始化父类字段 初始化根类字段
二、对象头:每个Java对象的”身份证”
2.1 对象头的组成
在64位JVM中,每个对象都包含一个对象头(Object Header):
| 组成部分 | 开启压缩指针 | 关闭压缩指针 | 说明 |
| 标记字段 | 8字节 | 8字节 | 存储哈希码、GC信息、锁信息等 |
| 类型指针 | 4字节 | 8字节 | 指向对象所属的类元数据 |
| 数组长度 | 4字节(仅数组) | 4字节(仅数组) | 存储数组元素个数 |
2.2 对象头的内存开销
class Integer {
private final int value; // 4字节
}
// 开启压缩指针:对象头12字节 + 实例字段4字节 = 16字节
// 关闭压缩指针:对象头16字节 + 实例字段4字节 = 24字节
内存浪费对比:
| 场景 | 对象头 | 实例字段 | 总大小 | 浪费比例 |
| Integer(开启压缩指针) | 12字节 | 4字节 | 16字节 | 75% |
| Integer(关闭压缩指针) | 16字节 | 4字节 | 24字节 | 83% |
三、压缩指针:节省内存的黑科技
3.1 压缩指针的原理
核心思想: 将64位指针压缩为32位,通过内存对齐实现寻址。
类比理解:
假设每辆车(对象)恰好占据两个车位(8字节),并且从偶数车位开始停:
车位号:0 1 2 3 4 5 6 7 8 9 10 11
车号: 0 1 2 3 4 5
原始指针:车位号6 → 找到3号车
压缩指针:车号3 → 3 * 2 = 6 → 找到3号车
数学公式:
实际地址 = 压缩指针值 * 8 + 偏移量
3.2 压缩指针的寻址范围
| 指针类型 | 位数 | 最大寻址范围 | 说明 |
| 32位压缩指针 | 32 | 32GB | 默认开启,超过32GB自动关闭 |
| 64位普通指针 | 64 | 无限 | 关闭压缩指针时使用 |
3.3 内存对齐规则
对象级对齐:
# 默认值为8,对象起始地址必须是8的倍数
-XX:ObjectAlignmentInBytes=8
字段级对齐:
- long/double字段地址必须是8的倍数
- 非压缩指针状态下,引用字段地址必须是8的倍数
对齐的好处:
- 性能提升:字段只出现在同一CPU缓存行中
- 避免跨缓存行:读取/写入不需要替换两个缓存行
四、字段重排列:JVM的内存优化魔术
4.1 字段重排列的规则
规则1:字段偏移量对齐
如果一个字段占据C个字节,那么该字段的偏移量需要对齐至NC。
规则2:父子类字段偏移量一致
子类所继承字段的偏移量,需要与父类对应字段的偏移量保持一致。
规则3:子类字段起始位置对齐
- 启用压缩指针:子类第一个字段对齐至4N
- 关闭压缩指针:子类第一个字段对齐至8N
4.2 实战:字段重排列效果
测试代码:
class A {
long l; // 8字节
int i; // 4字节
}
class B extends A {
long l; // 8字节
int i; // 4字节
}
启用压缩指针时的字段分布:
B object internals:
OFFSET SIZE TYPE DESCRIPTION
0 4 (object header)
4 4 (object header)
8 4 (object header)
12 4 int A.i 0
16 8 long A.l 0
24 8 long B.l 0
32 4 int B.i 0
36 4 (loss due to next object alignment)
关闭压缩指针时的字段分布:
B object internals:
OFFSET SIZE TYPE DESCRIPTION
0 4 (object header)
4 4 (object header)
8 4 (object header)
12 4 (object header)
16 8 long A.l
24 4 int A.i
28 4 (alignment/padding gap)
32 8 long B.l
40 4 int B.i
44 4 (loss due to next object alignment)
对比分析:
| 场景 | 总大小 | A.i偏移量 | B.i偏移量 | 空白填充 |
| 启用压缩指针 | 40字节 | 12 | 32 | 4字节 |
| 关闭压缩指针 | 48字节 | 24 | 40 | 8字节 |
4.3 @Contended:解决虚共享问题
虚共享的危害:
假设两个线程分别访问同一对象的不同volatile字段:
Thread 1 → volatileField1
Thread 2 → volatileField2
如果这两个字段在同一个缓存行中:
- Thread 1写入volatileField1 → 缓存行失效
- Thread 2读取volatileField2 → 需要重新加载缓存行
- 造成性能下降
@Contended解决方案:
public class CacheLineDemo {
@sun.misc.Contended
volatile long field1;
@sun.misc.Contended
volatile long field2;
@sun.misc.Contended
volatile long field3;
}
效果: 每个@Contended字段独占一个缓存行,避免虚共享。
使用注意:
# Java 8及以下版本
java -XX:-RestrictContended CacheLineDemo
# Java 9及以上版本
javac --add-exports java.base/jdk.internal.vm.annotation=ALL-UNNAMED CacheLineDemo.java
java -XX:-RestrictContended CacheLineDemo
五、实战:使用JOL工具查看对象内存布局
5.1 安装JOL工具
curl -L -O http://central.maven.org/maven2/org/openjdk/jol/jol-cli/0.9/jol-cli-0.9-full.jar
5.2 常用命令
查看对象内部结构:
java -cp jol-cli-0.9-full.jar org.openjdk.jol.Main internals java.lang.String
查看数组内部结构:
java -cp jol-cli-0.9-full.jar org.openjdk.jol.Main internals int[]
查看对象大小:
java -cp jol-cli-0.9-full.jar org.openjdk.jol.Main size java.lang.Integer
查看哈希码信息:
java -cp jol-cli-0.9-full.jar org.openjdk.jol.Main hashcode java.lang.String
5.3 实战练习
练习1:查看String对象的内存布局
java -cp jol-cli-0.9-full.jar org.openjdk.jol.Main internals java.lang.String
练习2:查看自定义类的内存布局
// 创建一个测试类
public class MyClass {
private byte b;
private int i;
private long l;
private Object obj;
}
java -cp jol-cli-0.9-full.jar org.openjdk.jol.Main internals MyClass
练习3:对比开启/关闭压缩指针的效果
# 开启压缩指针(默认)
java -cp jol-cli-0.9-full.jar org.openjdk.jol.Main internals java.lang.Integer
# 关闭压缩指针
java -XX:-UseCompressedOops -cp jol-cli-0.9-full.jar org.openjdk.jol.Main internals java.lang.Integer
六、内存布局的实际应用
6.1 对象大小计算
公式:
对象大小 = 对象头 + 实例字段总大小 + 对齐填充
示例:
class SmallObject {
private int id; // 4字节
private String name; // 4字节(压缩指针)
// 对象头:12字节
// 实例字段:8字节
// 总大小:20字节 → 对齐到24字节
}
6.2 数组对象的内存布局
// int[] arr = new int[5];
// 对象头:12字节(压缩指针)+ 4字节(数组长度)= 16字节
// 数组元素:5 * 4 = 20字节
// 总大小:36字节 → 对齐到40字节
6.3 性能优化建议
建议1:合理设计类的字段顺序
// 不好的设计(浪费空间)
class BadDesign {
boolean flag; // 1字节
long value; // 8字节 → 需要7字节填充
int count; // 4字节
}
// 好的设计(节省空间)
class GoodDesign {
long value; // 8字节
int count; // 4字节
boolean flag; // 1字节 → 仅需要3字节填充
}
建议2:避免创建过多小对象
// 使用数组替代多个小对象
int[] coordinates = new int[3]; // x, y, z
// 代替
class Coordinate {
int x, y, z;
}
建议3:使用@Contended优化并发访问
class ConcurrentCounter {
@sun.misc.Contended
volatile long counter1;
@sun.misc.Contended
volatile long counter2;
}
七、总结
核心要点回顾
- 对象头:每个对象都包含标记字段和类型指针,64位JVM中开启压缩指针为12字节
- 压缩指针:将64位指针压缩为32位,要求对象起始地址对齐至8的倍数
- 内存对齐:对象级对齐和字段级对齐,提升CPU缓存效率
- 字段重排列:JVM重新排列字段顺序以优化内存使用,遵循对齐规则
- @Contended:解决虚共享问题,让字段独占缓存行
关键理解
- 对象的内存布局是JVM的实现细节,不同版本可能有所不同
- 压缩指针是默认开启的,只有堆内存超过32GB时才会自动关闭
- 字段重排列会改变字段的内存顺序,但不会影响代码逻辑
- 了解内存布局有助于编写更高效的Java代码
写在最后
理解Java对象的内存布局,有助于我们写出更高效的代码。通过合理设计类的字段结构、使用压缩指针和@Contended注解等优化手段,可以显著提升程序的性能。如果你对JVM内存管理感兴趣,欢迎继续关注后续文章。
如果觉得本文对你有帮助,欢迎点赞、收藏、关注!后续我会继续分享更多JVM相关的深度解析文章。
📚 关注我,一起深入探索Java虚拟机的奥秘!








请登录后查看评论内容