10.深入解密Java对象内存布局——从对象头到字段重排列

图片[1]-10.深入解密Java对象内存布局——从对象头到字段重排列-速优课

本文原创,转载请注明出处。关注博主,持续分享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位压缩指针3232GB默认开启,超过32GB自动关闭
64位普通指针64无限关闭压缩指针时使用

3.3 内存对齐规则

对象级对齐:

# 默认值为8,对象起始地址必须是8的倍数
-XX:ObjectAlignmentInBytes=8

字段级对齐:

  • long/double字段地址必须是8的倍数
  • 非压缩指针状态下,引用字段地址必须是8的倍数

对齐的好处:

  1. 性能提升:字段只出现在同一CPU缓存行中
  2. 避免跨缓存行:读取/写入不需要替换两个缓存行

四、字段重排列: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字节12324字节
关闭压缩指针48字节24408字节

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;
}

七、总结

核心要点回顾

  1. 对象头:每个对象都包含标记字段和类型指针,64位JVM中开启压缩指针为12字节
  2. 压缩指针:将64位指针压缩为32位,要求对象起始地址对齐至8的倍数
  3. 内存对齐:对象级对齐和字段级对齐,提升CPU缓存效率
  4. 字段重排列:JVM重新排列字段顺序以优化内存使用,遵循对齐规则
  5. @Contended:解决虚共享问题,让字段独占缓存行

关键理解

  • 对象的内存布局是JVM的实现细节,不同版本可能有所不同
  • 压缩指针是默认开启的,只有堆内存超过32GB时才会自动关闭
  • 字段重排列会改变字段的内存顺序,但不会影响代码逻辑
  • 了解内存布局有助于编写更高效的Java代码

写在最后

理解Java对象的内存布局,有助于我们写出更高效的代码。通过合理设计类的字段结构、使用压缩指针和@Contended注解等优化手段,可以显著提升程序的性能。如果你对JVM内存管理感兴趣,欢迎继续关注后续文章。

如果觉得本文对你有帮助,欢迎点赞、收藏、关注!后续我会继续分享更多JVM相关的深度解析文章。

📚 关注我,一起深入探索Java虚拟机的奥秘!

© 版权声明
THE END
喜欢就支持一下吧
点赞10
相关推荐
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容

温馨提示:
1、本内容转载于网络,版权归原作者所有!
2、本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
3、本内容若侵犯到你的版权利益,请联系我们,会尽快给予删除处理!