Java `Object Layout` (`Object Header`, `Mark Word`, `Klass Pointer`) 与内存对齐

各位观众老爷,晚上好!我是你们今晚的 Java 内存布局导游,今天咱们不聊诗和远方,就聊聊你写的对象在 JVM 里面是怎么安家的。

开场白:对象,你的家在哪里?

咱们 Java 程序员,天天 new 对象,new 得不亦乐乎。但是,你有没有想过,new 出来的对象,它住在哪里?它的房间长什么样?邻居都是谁?今天,我们就来扒一扒 Java 对象的底裤,啊不,是内存布局。

第一站:对象的基本结构——三室一厅

Java 对象在堆内存里,至少有这么三部分:

  1. 对象头 (Object Header): 这是对象的门牌号,记录着对象的身份信息。
  2. 实例数据 (Instance Data): 这是对象真正存储数据的地方,也就是对象的属性。
  3. 对齐填充 (Padding): 这是为了让对象的大小是 8 字节的倍数,方便 CPU 读取,就像装修房子的时候,为了美观做的填缝一样。

我们暂且把它们比作“三室一厅”。对象头是客厅,实例数据是卧室,对齐填充是卫生间(虽然有点不雅,但是形象啊!)。

第二站:客厅——对象头 (Object Header)

对象头是重中之重,它包含了两部分:

  • Mark Word: 这玩意儿信息量巨大,记录了对象的哈希码、GC 分代年龄、锁状态等等。可以把它想象成客厅的电视墙,上面挂满了照片和奖状,记录着对象的辉煌历史。
  • Klass Pointer: 这玩意儿指向对象所属的类,也就是对象从哪个模板(类)来的。可以把它想象成客厅的户口本,证明你是谁家的孩子。

2.1 Mark Word 的秘密

Mark Word 的长度在 32 位 JVM 上是 4 字节,在 64 位 JVM 上是 8 字节。但是里面的内容会根据对象的状态而变化。

状态 标志位 内容
无锁 01 对象的哈希码 (HashCode)、GC 分代年龄 (age)
偏向锁 01 偏向线程 ID、时间戳、epoch
轻量级锁 00 指向栈中锁记录的指针
重量级锁 10 指向 monitor 对象的指针
GC 标记 11 空,表示对象不可用
可偏向 01 匿名偏向或者偏向锁启用

哇,是不是很复杂?没关系,记住几个关键点:

  • HashCode: 对象的身份标识,用于 hashCode() 方法。
  • GC 分代年龄: 记录对象被垃圾回收器扫描的次数,用于判断对象是否需要被回收。
  • 锁状态: 标记对象是否被锁住,以及锁的类型(偏向锁、轻量级锁、重量级锁)。

2.2 Klass Pointer 的作用

Klass Pointer 简单来说就是指向对象所属类的指针。通过这个指针,JVM 就能知道对象的类型,以及对象有哪些方法和属性。

第三站:卧室——实例数据 (Instance Data)

实例数据就是对象真正存储数据的地方。它包含了对象的所有字段。

  • 基本类型: int, long, float, double, boolean, byte, char, short
  • 引用类型: 指向其他对象的指针

例如:

class Person {
    int age;
    String name;
}

Person person = new Person();

person 对象的实例数据就包含了 age (int) 和 name (String) 两个字段。age 直接存储了 int 值,name 存储的是指向 String 对象的指针。

第四站:卫生间——对齐填充 (Padding)

为什么要对齐填充?这是因为 CPU 在读取数据的时候,通常是按照 8 字节(64 位系统)或者 4 字节(32 位系统)对齐读取的。如果对象的大小不是 8 字节的倍数,CPU 可能需要多次读取才能获取完整的数据,影响性能。

JVM 为了提高性能,会对对象进行对齐填充,保证对象的大小是 8 字节的倍数。

第五站:代码实战——使用 JOL 工具查看对象布局

光说不练假把式。咱们用 JOL (Java Object Layout) 工具来实际看看对象的内存布局。

  1. 引入 JOL 依赖:

    在 Maven 项目中,添加以下依赖:

    <dependency>
        <groupId>org.openjdk.jol</groupId>
        <artifactId>jol-core</artifactId>
        <version>0.16</version>
    </dependency>
  2. 编写测试代码:

    import org.openjdk.jol.info.ClassLayout;
    
    public class ObjectLayoutExample {
        public static void main(String[] args) {
            Object o = new Object();
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
    
            Person person = new Person();
            System.out.println(ClassLayout.parseInstance(person).toPrintable());
    
            Person2 person2 = new Person2();
            System.out.println(ClassLayout.parseInstance(person2).toPrintable());
        }
    }
    
    class Person {
        int age;
        String name;
    }
    
    class Person2 {
        boolean flag;
        int age;
        String name;
    }
  3. 运行结果 (64 位 JVM):

    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    ObjectLayoutExample$Person object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
         12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
         16     4    int Person.age                                 0
         20     4   java.lang.String Person.name                      null
         24     8        (loss due to the next object alignment)
    Instance size: 32 bytes
    Space losses: 0 bytes internal + 8 bytes external = 8 bytes total
    
    ObjectLayoutExample$Person2 object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
         12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
         16     1 boolean Person2.flag                               false
         17     3        (alignment/padding gap)
         20     4    int Person2.age                                 0
         24     4   java.lang.String Person2.name                      null
         28     4        (loss due to the next object alignment)
    Instance size: 32 bytes
    Space losses: 3 bytes internal + 4 bytes external = 7 bytes total
    • OFFSET: 字段的起始地址(相对于对象起始地址的偏移量)。
    • SIZE: 字段的大小(字节数)。
    • TYPE: 字段的类型。
    • DESCRIPTION: 字段的描述。
    • VALUE: 字段的值。

    解释:

    • Object 对象:对象头占用 12 字节,因为需要对齐到8字节的倍数,因此需要 4 字节的填充,总大小为 16 字节。
    • Person 对象:对象头占用 12 字节,int age 占用 4 字节,String name 占用 4 字节(指针),总共 20 字节。由于需要对齐到 8 字节的倍数,因此需要 8 字节的填充,总大小为 32 字节。
    • Person2 对象:对象头占用 12 字节,boolean flag 占用 1 字节,然后填充 3 字节来对齐int ageint age 占用 4 字节,String name 占用 4 字节(指针),总共 24 字节。由于需要对齐到 8 字节的倍数,因此需要 4 字节的填充,总大小为 32 字节。

第六站:内存对齐的策略

JVM 在进行内存对齐的时候,会遵循一些策略:

  1. 字段的排序: JVM 会对字段进行排序,尽量将相同类型的字段放在一起,减少填充。
  2. 对齐的大小: 通常是 8 字节对齐(64 位 JVM)或者 4 字节对齐(32 位 JVM)。
  3. 顺序: 按照字段的声明顺序进行排列。

第七站:总结与优化建议

  • 了解对象布局: 理解对象在内存中的布局,可以帮助我们更好地理解 JVM 的工作原理,以及优化内存使用。
  • 合理安排字段顺序: 将相同类型的字段放在一起,可以减少填充,节省内存。
  • 避免过大的对象: 过大的对象容易导致内存碎片,影响性能。
  • 使用基本类型代替对象: 如果可以使用基本类型,尽量避免使用对象,可以减少内存开销。

第八站:进阶思考

  • 压缩指针 (Compressed Oops): 在 64 位 JVM 上,可以使用压缩指针技术,将指针的大小从 8 字节压缩到 4 字节,节省内存。
  • 逃逸分析 (Escape Analysis): JVM 可以通过逃逸分析,判断对象是否只在方法内部使用。如果是,可以将对象分配在栈上,而不是堆上,减少 GC 的压力。

第九站:问答环节

欢迎大家提问,我会尽力解答。

结束语:

今天我们一起探索了 Java 对象的内存布局,希望大家以后在写代码的时候,能够更加关注内存使用,写出更加高效、健壮的程序。下次再见!

彩蛋:

记住,一切皆对象,对象皆有家。了解对象的家在哪里,才能更好地管理它们,让你的程序跑得更快、更稳!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注