子类的内存创建布局
# Java 子类创建的内存布局详解
在 Java 中,子类的创建涉及到 Java 虚拟机(JVM,特指 HotSpot 虚拟机)的内存分配和对象布局机制。了解子类对象的内存布局对于理解 Java 的内存管理、性能优化以及并发机制(如 synchronized 锁)至关重要。本文将详细解析 Java 子类对象的内存布局,包括对象头、实例数据和对齐填充,并结合示例代码和工具(如 JOL)进行说明。
1. Java 对象内存布局概述¶
在 HotSpot JVM 中,一个 Java 对象的内存布局通常分为以下三个部分:
- 对象头(Object Header):存储对象运行时元数据,如哈希码、锁状态、GC 分代年龄等。
- 实例数据(Instance Data):存储对象自身的字段数据,包括基本数据类型和引用类型。
- 对齐填充(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 依赖以查看内存布局:
3.3 内存布局分析¶
假设运行在 64 位 JVM,开启指针压缩。以下是 Child 类对象的内存布局:
- 对象头:
- Mark Word: 8 字节
- Class Pointer: 4 字节(压缩后)
-
总计:12 字节
-
实例数据:
- 父类字段:
parentLong: 8 字节parentInt: 4 字节
- 子类字段:
childInt: 4 字节childBoolean: 1 字节
- 字段重排序:JVM 可能按以下顺序排列字段以优化内存对齐:
parentLong(8 字节)parentInt(4 字节)childInt(4 字节)childBoolean(1 字节)
-
实例数据总大小:8 + 4 + 4 + 1 = 17 字节
-
对齐填充:
- 对象头 (12 字节) + 实例数据 (17 字节) = 29 字节
- 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 会根据字段大小重新排列,以减少内存碎片和对齐填充。例如,long 和 double 优先排列,其次是 int 和 float,最后是 short、char 和 boolean。
4.3 继承的影响¶
- 子类对象包含父类的所有实例字段,父类字段在内存布局中通常先于子类字段。
- 静态字段(
static)存储在方法区,不包含在对象实例的内存布局中。
4.4 并发与对象头¶
对象头的 Mark Word 在并发场景中至关重要。例如:
- 偏向锁:存储偏向线程 ID 和时间戳。
- 轻量级锁:存储指向栈中锁记录的指针(ptr_to_lock_record)。
- 重量级锁:存储指向监视器(Monitor)的指针(ptr_to_heavyweight_monitor)。
这些信息直接影响 synchronized 关键字的实现。
5. 实际验证与工具¶
使用 JOL 工具可以直观查看对象的内存布局。以下是验证步骤:
- 添加 JOL 依赖到项目。
- 编写测试代码,创建子类对象并打印其内存布局。
- 运行时确保 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 的倍数]