Java对象在内存中到底长什么样?通过 new Object()
实例化的对象到底有多大? 对象在内存中是如何布局的?
对象的创建
从源代码到被JVM装载执行,要经过一系列的步骤
编译 -> 加载 -> 链接 -> 初始化 -> 创建对象
每个部分都有很多细节和底层原理可以拆分成很多篇文章,本篇文章不进行细说,先挖个坑后面再来填。
简单来说,创建一个对象,实际上就是为对象分配一块内存地址空间。
比如如下代码段:1
Object obj = new Object();
new Object()
在堆中开辟了一块内存空间,并且把内存地址返回给变量 obj
。
对象创建后就可以通过变量 obj
去使用对象了。
对象在内存中的布局结构
OpenJDK 提供了一个工具:JOL(Java Object Layout)
JOL通过JVMTI和SA来解码对象在内存中的实际布局信息。
引入依赖:1
2
3dependencies {
implementation 'org.openjdk.jol:jol-core:0.10'
}
使用JOL提供的API获取并输出对象实例信息:1
2
3
4
5
6
7
8
9
10package 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
8java.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 Work
和 Class Pointer
。
如果对象是数组类型,在对象头中有一块用于记录数组长度的数据结构。
因为通过元数据无法确定数组大小。对象头中还会多存一个Length字段用来记录数组长度。
Mark Work
存储对象自身的运行时信息,包含:hashcode,gc分代年龄、锁状态标志,偏向锁的线程id。
为了节省内存空间,对象头中的不同信息没有采用结构化的数据结构字段进行存储,而是直接复用了二进制位。
Class Pointer
存储当前对象对应的类信息,用来标识是由哪个类产生的对象。
开启指针压缩:1
-XX:+UseCompressedClassPointers
JVM默认开启指针压缩的情况下,对象头只占用12个字节。
关闭指针压缩:1
-XX:-UseCompressedClassPointers
关闭指针压缩,对象头占用16个字节。
可以通过JOL工具查看对象内存布局。
实例数据
存储对象实例的数据,比如对象的成员属性值。
自定义类TypeSample
:
1 | package com.lanshiqin; |
实例化对象并输出对象的内存布局: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
25com.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
25com.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++源码实现,后续单独文章进行分析。