Java对象在内存中的布局

Java对象在内存中到底长什么样?通过 new Object() 实例化的对象到底有多大? 对象在内存中是如何布局的?

对象的创建

从源代码到被JVM装载执行,要经过一系列的步骤

编译 -> 加载 -> 链接 -> 初始化 -> 创建对象

每个部分都有很多细节和底层原理可以拆分成很多篇文章,本篇文章不进行细说,先挖个坑后面再来填。

简单来说,创建一个对象,实际上就是为对象分配一块内存地址空间。
比如如下代码段:

1
Object obj = new Object();

new Object() 在堆中开辟了一块内存空间,并且把内存地址返回给变量 obj
对象创建后就可以通过变量 obj去使用对象了。

对象在内存中的布局结构

OpenJDK 提供了一个工具:JOL(Java Object Layout)
JOL通过JVMTI和SA来解码对象在内存中的实际布局信息。
引入依赖:

1
2
3
dependencies {
implementation 'org.openjdk.jol:jol-core:0.10'
}

使用JOL提供的API获取并输出对象实例信息:

1
2
3
4
5
6
7
8
9
10
package com.lanshiqin;

import org.openjdk.jol.info.ClassLayout;

public class ObjectLayoutSample {
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}

运行程序,输出Object对象实例的信息如下。

1
2
3
4
5
6
7
8
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 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

通过输出的信息可以看到,一个Object对象实例占用内存16个字节。
OFFSET 内存偏移
SIZE 占用大小(单位:字节 byte)
TYPE 数据类型
DESCRIPTION 描述
VALUE 内存中的值
对象头(object header)中的信息包含了对象的hashcode,锁标志位,gc标记和分代年龄等信息。
一个完整的对象应该包含三个部分:对象头、实例数据、对齐填充。

对象的组成

对象在内存中可以分为三个部分,分别是对象头、实例数据、对齐填充。

对象头(Object Header)

对象头中包含Mark WorkClass Pointer
如果对象是数组类型,在对象头中有一块用于记录数组长度的数据结构。
因为通过元数据无法确定数组大小。对象头中还会多存一个Length字段用来记录数组长度。

Mark Work

存储对象自身的运行时信息,包含:hashcode,gc分代年龄、锁状态标志,偏向锁的线程id。
为了节省内存空间,对象头中的不同信息没有采用结构化的数据结构字段进行存储,而是直接复用了二进制位。

Class Pointer

存储当前对象对应的类信息,用来标识是由哪个类产生的对象。
开启指针压缩:

1
-XX:+UseCompressedClassPointers

JVM默认开启指针压缩的情况下,对象头只占用12个字节。

关闭指针压缩:

1
-XX:-UseCompressedClassPointers

关闭指针压缩,对象头占用16个字节。
可以通过JOL工具查看对象内存布局。

实例数据

存储对象实例的数据,比如对象的成员属性值。
自定义类TypeSample

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.lanshiqin;

/**
* 数据类型
*/
public class TypeSample {
// 基础类型
private byte aByte;
private char aChar;
private short aShort;
private int anInt;
private float aFloat;
private double aDouble;
private long aLong;
private boolean aBoolean;
// 包装类型
private Byte theByte;
private Character theCharacter;
private Short theShort;
private Integer theInteger;
private Float theFloat;
private Double theDouble;
private Long theLong;
private Boolean theBoolean;
}

实例化对象并输出对象的内存布局:

1
System.out.println(ClassLayout.parseInstance(new TypeSample()).toPrintable());

默认开启指针压缩,每个引用类型的变量都固定占用4个字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
com.lanshiqin.TypeSample 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) 43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509)
12 4 int TypeSample.anInt 0
16 8 double TypeSample.aDouble 0.0
24 8 long TypeSample.aLong 0
32 4 float TypeSample.aFloat 0.0
36 2 char TypeSample.aChar
38 2 short TypeSample.aShort 0
40 1 byte TypeSample.aByte 0
41 1 boolean TypeSample.aBoolean false
42 2 (alignment/padding gap)
44 4 java.lang.Byte TypeSample.theByte null
48 4 java.lang.Character TypeSample.theCharacter null
52 4 java.lang.Short TypeSample.theShort null
56 4 java.lang.Integer TypeSample.theInteger null
60 4 java.lang.Float TypeSample.theFloat null
64 4 java.lang.Double TypeSample.theDouble null
68 4 java.lang.Long TypeSample.theLong null
72 4 java.lang.Boolean TypeSample.theBoolean null
76 4 (loss due to the next object alignment)
Instance size: 80 bytes
Space losses: 2 bytes internal + 4 bytes external = 6 bytes total

每个基础类型都占用了各自都内存空间。
int类型32位,占用了4个字节。所以SIZE为4 byte。
long类型64位,占用了8个字节。所以SIZE为8 byte。

引用类型的变量,对应的对象都在堆内存中。(不考虑逃逸分析的情况下)
由于JVM默认开启了指针压缩,所以引用类型的空间统一都只占用4个字节。

开启指针压缩:

1
-XX:+UseCompressedOops

关闭指针压缩:

1
-XX:-UseCompressedOops

关闭指针压缩后,引用类型的变量地址空间将会占用8个字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
com.lanshiqin.TypeSample 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) e8 36 80 9f (11101000 00110110 10000000 10011111) (-1618987288)
12 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
16 8 double TypeSample.aDouble 0.0
24 8 long TypeSample.aLong 0
32 4 int TypeSample.anInt 0
36 4 float TypeSample.aFloat 0.0
40 2 char TypeSample.aChar
42 2 short TypeSample.aShort 0
44 1 byte TypeSample.aByte 0
45 1 boolean TypeSample.aBoolean false
46 2 (alignment/padding gap)
48 8 java.lang.Byte TypeSample.theByte null
56 8 java.lang.Character TypeSample.theCharacter null
64 8 java.lang.Short TypeSample.theShort null
72 8 java.lang.Integer TypeSample.theInteger null
80 8 java.lang.Float TypeSample.theFloat null
88 8 java.lang.Double TypeSample.theDouble null
96 8 java.lang.Long TypeSample.theLong null
104 8 java.lang.Boolean TypeSample.theBoolean null
Instance size: 112 bytes
Space losses: 2 bytes internal + 0 bytes external = 2 bytes total

对齐填充

HotSpot虚拟机的自动内存管理机制要求,对象起始地址必须是8字节的整数倍。
如果对象实例数据部分没有对齐,就需要通过对齐填充来补全。
这么做是为了更好的GC,规避引发的安全问题提升GC性能的角度进行考虑的。
这部分需要深入JVM的C++源码实现,后续单独文章进行分析。

0%