Skip to content

子类的内存创建布局

# Java 子类创建的内存布局详解

在 Java 中,子类的创建涉及到 Java 虚拟机(JVM,特指 HotSpot 虚拟机)的内存分配和对象布局机制。了解子类对象的内存布局对于理解 Java 的内存管理、性能优化以及并发机制(如 synchronized 锁)至关重要。本文将详细解析 Java 子类对象的内存布局,包括对象头、实例数据和对齐填充,并结合示例代码和工具(如 JOL)进行说明。


1. Java 对象内存布局概述

在 HotSpot JVM 中,一个 Java 对象的内存布局通常分为以下三个部分:

  1. 对象头(Object Header):存储对象运行时元数据,如哈希码、锁状态、GC 分代年龄等。
  2. 实例数据(Instance Data):存储对象自身的字段数据,包括基本数据类型和引用类型。
  3. 对齐填充(Padding):确保对象大小是 8 字节的倍数,以满足 JVM 的内存对齐要求。

对于子类对象,其内存布局会继承父类的字段,同时添加自己的字段。以下将详细分析子类对象的内存布局。


2. 子类对象内存布局的组成

2.1 对象头(Object Header)

对象头包含以下两部分(对于普通对象,非数组对象):

  • Mark Word:用于存储对象的运行时数据,如:
  • 哈希码(Identity HashCode)
  • GC 分代年龄
  • 锁状态标志(无锁、偏向锁、轻量级锁、重量级锁)
  • 偏向线程 ID、偏向时间戳等
  • 在 32 位 JVM 中,Mark Word 占 4 字节;在 64 位 JVM 中,占 8 字节。
  • 类型指针(Class Pointer):指向对象的类元数据(instanceKlass),用于确定对象所属的类。
  • 在 32 位 JVM 中,类型指针占 4 字节。
  • 在 64 位 JVM 中,默认启用指针压缩(-XX:+UseCompressedOops),类型指针占 4 字节;未启用时占 8 字节。
  • 数组长度(Array Length,仅数组对象):对于数组对象,对象头还包含一个 4 字节的字段存储数组长度。普通对象和子类对象无此部分。

对象头大小: - 32 位 JVM:Mark Word (4 字节) + Class Pointer (4 字节) = 8 字节 - 64 位 JVM(开启指针压缩):Mark Word (8 字节) + Class Pointer (4 字节) = 12 字节 - 64 位 JVM(关闭指针压缩):Mark Word (8 字节) + Class Pointer (8 字节) = 16 字节

2.2 实例数据(Instance Data)

实例数据部分存储对象的字段,包括: - 父类的字段:子类对象会继承父类的所有实例字段(不包括静态字段)。 - 子类的字段:子类自身定义的实例字段。 - 字段排列规则: - JVM 会根据字段类型的大小进行重排序,通常按照“先长后短”原则(long/double → int/float → short/char → byte/boolean)以减少内存碎片和对齐填充。 - 基本数据类型大小: - byte: 1 字节 - short: 2 字节 - char: 2 字节 - int: 4 字节 - float: 4 字节 - long: 8 字节 - double: 8 字节 - boolean: 1 字节(实际存储可能因 JVM 实现而异) - 引用类型(如对象引用、数组引用): - 开启指针压缩:4 字节 - 关闭指针压缩:8 字节

2.3 对齐填充(Padding)

JVM 要求对象总大小必须是 8 字节的倍数(即 8 字节对齐)。如果对象头和实例数据的总大小不是 8 的倍数,JVM 会添加填充字节(padding)以补齐。


3. 子类对象内存布局示例

以下通过一个父类和子类的示例,结合 JOL(Java Object Layout)工具分析子类对象的内存布局。

3.1 示例代码

import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;

class Parent {
    long parentLong; // 8 字节
    int parentInt;   // 4 字节
}

class Child extends Parent {
    boolean childBoolean; // 1 字节
    int childInt;        // 4 字节
}

public class MemoryLayoutTest {
    public static void main(String[] args) {
        System.out.println(VM.current().details());
        Child child = new Child();
        System.out.println(ClassLayout.parseInstance(child).toPrintable());
    }
}

3.2 环境说明

  • 运行环境:64 位 HotSpot JVM,开启指针压缩(-XX:+UseCompressedOops)。
  • 依赖 JOL 工具:添加 Maven 依赖以查看内存布局:
    <dependency>
        <groupId>org.openjdk.jol</groupId>
        <artifactId>jol-core</artifactId>
        <version>0.16</version>
    </dependency>
    

3.3 内存布局分析

假设运行在 64 位 JVM,开启指针压缩。以下是 Child 类对象的内存布局:

  1. 对象头
  2. Mark Word: 8 字节
  3. Class Pointer: 4 字节(压缩后)
  4. 总计:12 字节

  5. 实例数据

  6. 父类字段:
    • parentLong: 8 字节
    • parentInt: 4 字节
  7. 子类字段:
    • childInt: 4 字节
    • childBoolean: 1 字节
  8. 字段重排序:JVM 可能按以下顺序排列字段以优化内存对齐:
    • parentLong (8 字节)
    • parentInt (4 字节)
    • childInt (4 字节)
    • childBoolean (1 字节)
  9. 实例数据总大小:8 + 4 + 4 + 1 = 17 字节

  10. 对齐填充

  11. 对象头 (12 字节) + 实例数据 (17 字节) = 29 字节
  12. 29 字节不是 8 的倍数,需填充 3 字节,使总大小为 32 字节(8 的倍数)。

内存布局图示

+--------------------+--------------- +------------+-----------+---------------+---------------+
| Object Header (12B)| parentLong (8B)| parentInt (4B) | childInt (4B) | childBoolean (1B) | Padding (3B) |
| 0             | 12            | 20         | 24       | 28            | 29           | 32

3.4 JOL 输出结果

运行上述代码,JOL 工具可能输出以下结果(具体值可能因 JVM 实现而异):

# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.

memory.Child object internals:
 OFFSET  SIZE   TYPE DESCRIPTION               VALUE
      0    12        (object header)           N/A
     12     8   long Parent.parentLong         N/A
     20     4    int Parent.parentInt          N/A
     24     4    int Child.childInt           N/A
     28     1 boolean Child.childBoolean       N/A
     29     3        (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

解释: - 对象头占 12 字节(Mark Word 8 字节 + Class Pointer 4 字节)。 - 实例数据按“先长后短”排列,依次为 parentLong (8 字节)、parentInt (4 字节)、childInt (4 字节)、childBoolean (1 字节)。 - 总大小 29 字节,填充 3 字节,最终对象大小为 32 字节。


4. 内存布局的影响因素

4.1 指针压缩

  • 开启指针压缩-XX:+UseCompressedOops):
  • 引用类型和类型指针占用 4 字节,对象头为 12 字节。
  • 适合堆内存小于 32 GB 的场景,可有效节省内存。
  • 关闭指针压缩-XX:-UseCompressedOops):
  • 引用类型和类型指针占用 8 字节,对象头为 16 字节。
  • 适用于大内存场景,但内存占用增加。

4.2 字段重排序

JVM 会根据字段大小重新排列,以减少内存碎片和对齐填充。例如,longdouble 优先排列,其次是 intfloat,最后是 shortcharboolean

4.3 继承的影响

  • 子类对象包含父类的所有实例字段,父类字段在内存布局中通常先于子类字段。
  • 静态字段(static)存储在方法区,不包含在对象实例的内存布局中。

4.4 并发与对象头

对象头的 Mark Word 在并发场景中至关重要。例如: - 偏向锁:存储偏向线程 ID 和时间戳。 - 轻量级锁:存储指向栈中锁记录的指针(ptr_to_lock_record)。 - 重量级锁:存储指向监视器(Monitor)的指针(ptr_to_heavyweight_monitor)。

这些信息直接影响 synchronized 关键字的实现。


5. 实际验证与工具

使用 JOL 工具可以直观查看对象的内存布局。以下是验证步骤:

  1. 添加 JOL 依赖到项目。
  2. 编写测试代码,创建子类对象并打印其内存布局。
  3. 运行时确保 JVM 参数正确(例如,是否开启指针压缩)。

示例扩展:如果子类包含引用类型字段(如 Object 或数组),引用本身占用 4 字节(压缩)或 8 字节(未压缩),但引用的对象(如数组元素)存储在堆中,需单独计算其内存布局。


6. 总结

Java 子类对象的内存布局由对象头、实例数据和对齐填充三部分组成: - 对象头:包含 Mark Word 和 Class Pointer,64 位 JVM 开启指针压缩时占 12 字节。 - 实例数据:包含父类和子类的实例字段,按“先长后短”排列。 - 对齐填充:确保对象大小为 8 字节的倍数。 - 子类继承父类字段,字段排列会影响内存使用效率。 - 指针压缩和字段重排序是 JVM 优化内存的常用手段。

通过 JOL 工具可以精确分析内存布局,推荐开发者在性能优化或并发编程时深入理解这些机制。

参考文献: - 《深入理解 Java 虚拟机:JVM 高级特性与最佳实践》 - OpenJDK JOL 工具文档:http://openjdk.java.net/projects/code-tools/jol/

graph TD
    A[堆内存 Heap] --> B[ChildClass 对象实例]
    B --> C[对象头 Object Header<br>12 字节]
    B --> D[实例数据 Instance Data]
    B --> E[对齐填充 Padding<br>0-7 字节]

    C --> C1[Mark Word<br>8 字节<br>哈希码、锁状态、GC年龄等]
    C --> C2[Class Pointer<br>4 字节<br>指向方法区中 ChildClass 元数据]

    D --> D1[父类字段 Parent Fields]
    D --> D2[子类字段 Child Fields]

    D1 --> D1_1[parentLong: long<br>8 字节]
    D1 --> D1_2[parentInt: int<br>4 字节]

    D2 --> D2_1[childInt: int<br>4 字节]
    D2 --> D2_2[childBoolean: boolean<br>1 字节]

    E --> E1[填充字节<br>3 字节<br>确保总大小为 8 的倍数]