Java单个对象内存布局.md
我们在如何获取一个Java对象所占内存大小的文章中写了一个获取Java对象所占内存大小的工具类(ObjectSizeFetcher
),那么接下来,我们使用这个工具类来看一下Java中各种类型的对象所占内存的大小
基本类型
基本类型的内存占用情况如下表:
基本类型 | 内存大小(单位:字节) |
---|---|
boolean | 1 |
byte | 1 |
short | 2 |
char | 2 |
int | 4 |
float | 4 |
long | 8 |
double | 8 |
以上基本类型所占内存大小是Java规定的,引用类型所占内存大小就不是确定的了,接下来我们看下引用类型所占内存大小,我们先从Java单个对象内存布局开始
Java单个对象内存布局
对象头
在Java中,每一个对象都包含对象头,对象头包含两类数据:存储对象自身的运行时数据和类型指针数据
- 存储对象自身的运行时数据,Mark Word(在32位和64位操作系统上长度分别为4字节和8字节),包含如下信息:
- 对象hashCode
- 对象GC分代年龄
- 锁状态标志(轻量级锁、重量级锁)
- 线程持有的锁(轻量级锁、重量级锁)
- 偏向锁相关
- 类型指针:对象指向类元数据的指针(32位操作系统-->4字节,64位操作系统-->8字节(未开启压缩指针),4字节(开启压缩指针))
- JVM通过这个指针来确定这个对象是哪个类的实例(根据对象确定其Class的指针)
所以在32位操作系统上,一个Java对象的对象头所占内存大小为8字节
而在64位操作系统上:
- 如果未开启压缩指针,那么对象头的大小为
16字节
- 如果开启压缩指针,那么对象头的大小为
12字节
JVM通过参数UseCompressedOops
来控制是否开启压缩指针的功能,默认是开启,我们来看一下这个参数。
我们在如何获取一个Java对象所占内存大小的最后留下了一个问题,那就是new Point()
这个对象所占内存大小为什么是24字节
呢?以下是Point
的代码:
public class Point { private int x; // 4字节 private int y; // 4字节 public static void main(String [] args) { System.out.println(ObjectSizeFetcher.sizeOf(new Point())); } }
这个Point
类有两个属性x
和y
,都是int
类型的,而int类型所占内存大小是4字节,那么两个int类型的属性所占大小为8字节,那么24 - 8 = 16字节
是什么所占的内存呢?
不开启指针压缩功能
当我们执行下面的命令的时候:
## 不开启指针压缩功能
java -XX:-UseCompressedOops -javaagent:ObjectSizeFetcherAgent-1.0-SNAPSHOT.jar com.twq.Point
new Point()
这个对象的大小是24字节
因为没有开启指针压缩功能,所以这个时候的对象头的大小是16字节
(注意:我的电脑是64位操作系统)。那么24 - 8 = 16字节
就是对象头所占的内存大小
开启指针压缩功能
当我们执行下面的命令的时候:
## 开启指针压缩功能 java -XX:+UseCompressedOops -javaagent:ObjectSizeFetcherAgent-1.0-SNAPSHOT.jar com.twq.Point
new Point()
这个对象的大小还是24字节
因为开启了指针压缩功能,所以这个时候的对象头的大小是12字节
。那么24 - 8 = 16字节
中的16字节
除了包含了12字节
的对象头,还有4字节
多,这个4字节
就是对齐填充(padding)的。
对齐填充:JVM要求对象的大小必须是8的整数倍,若不是,需要补位对齐。
在开启指针压缩功能的时候,对象头大小是12字节
+ 2个int类型属性大小8字节
= 20字节
,因为20
不是8的倍数,所以需要对齐填充4个字节,即24字节
Java单个对象内存布局总结:
- Java单个对象所占内存大小等于:对象头所占内存大小 + 对象实例属性数据所占内存大小 + 对齐填充所占内存大小
- 对象头包含存储对象自身的运行时数据和类型指针数据两类数据。在64位操作系统中,如果开启指针压缩功能的话,对象头所占内存大小为12字节;如果没有开启指针压缩功能的话,对象头所占内存大小为16字节
- 对齐填充:JVM要求对象的大小必须是8的整数倍,若不是,需要补位对齐。
static修饰的属性
我们在Point
中增加一个static修饰的变量,如下代码:
public class Point { private int x; private int y; public static long id = 3000L; // 增加一个static修饰的变量 public static void main(String [] args) { System.out.println(ObjectSizeFetcher.sizeOf(new Point())); } }
当我们执行下面的命令的时候:
## 不开启指针压缩功能 java -XX:-UseCompressedOops -javaagent:ObjectSizeFetcherAgent-1.0-SNAPSHOT.jar com.twq.Point ## 开启指针压缩功能 java -XX:+UseCompressedOops -javaagent:ObjectSizeFetcherAgent-1.0-SNAPSHOT.jar com.twq.Point
得到的结果如下:
new Point()
这个对象所占的内存大小还是24字节
,证明static变量属于类,不属于实例,存放在全局数据段,普通变量才纳入Java对象占用空间的计算。
引用类型
引用类型在32位操作系统上每个占用4字节
在64位操作系统上:
- 没有开启指针压缩功能的话占用8字节
- 开启指针压缩功能的话占用4字节
我们写一个名为RefTypeSizer
的类,其内容为:
class Person { } public class RefTypeSizer { // 这个是引用类型 private Person person; public static void main(String[] args) throws IllegalAccessException { System.out.println("对象new RefTypeSizer()所占内存大小:" + ObjectSizeFetcher.sizeOf(new RefTypeSizer()) + "字节"); } }
然后,我们重新打包,然后先执行下面的命令:
## 不开启指针压缩功能 java -XX:-UseCompressedOops -javaagent:ObjectSizeFetcherAgent-1.0-SNAPSHOT.jar com.twq.RefTypeSizer
得到的对象new RefTypeSizer()
所占内存大小为24字节
因为这个时候没有开启指针压缩功能,所以对象头大小为16字节
,引用类型Person person
所占内存为8字节
,所以加起来大小为16 + 8 = 24字节
现在,我们再来打开指针压缩功能,如下命令:
## 开启指针压缩功能 java -XX:+UseCompressedOops -javaagent:ObjectSizeFetcherAgent-1.0-SNAPSHOT.jar com.twq.RefTypeSizer
得到的对象new RefTypeSizer()
所占内存大小为16字节
因为这个时候开启了指针压缩功能,所以对象头大小为12字节
,引用类型Person person
所占内存为4字节
,所以加起来大小为12 + 4 = 16字节
数组
在64位操作系统上,数组对象的对象头占用24字节,启用指针压缩功能后占用16字节,之所以比普通对象占用内存多是因为数组需要额外的空间存储数组的长度。
我们看如下计算数组长度的代码:
public class ArraySizer { public static void main(String[] args) { System.out.println("new Integer[0]所占内存大小为:" + ObjectSizeFetcher.sizeOf(new Integer[0]) + "字节"); System.out.println("new Integer[1]所占内存大小为:" + ObjectSizeFetcher.sizeOf(new Integer[1]) + "字节"); System.out.println("new Integer[2]所占内存大小为:" + ObjectSizeFetcher.sizeOf(new Integer[2]) + "字节"); System.out.println("new Integer[3]所占内存大小为:" + ObjectSizeFetcher.sizeOf(new Integer[3]) + "字节"); System.out.println("new Integer[4]所占内存大小为:" + ObjectSizeFetcher.sizeOf(new Integer[4]) + "字节"); } }
然后,我们重新打包,然后先执行下面的命令:
## 不开启指针压缩功能 java -XX:-UseCompressedOops -javaagent:ObjectSizeFetcherAgent-1.0-SNAPSHOT.jar com.twq.ArraySizer
得到结果如下:
我们可以看到new Integer[0]
的大小为24字节
,因为数组长度为0,所以这个数组的大小就是数组对象头的大小,又因为没有开启指针压缩功能,所以数组对象头大小为24字节
,其他长度数组所占内存解释:
new Integer[1]
的大小为:对象头24字节 + 1个引用类型大小8字节 = 32字节new Integer[2]
的大小为:对象头24字节 + 2个引用类型大小16字节 = 40字节new Integer[3]
的大小为:对象头24字节 + 3个引用类型大小24字节 = 48字节new Integer[4]
的大小为:对象头24字节 + 4个引用类型大小32字节 = 56字节new Integer[]{2, 3, 4, 5}
的大小位:new Integer[4]
的大小56字节 + 4 * (Integer对象头16字节 + Integer中int类型属性大小4字节 + 对齐填充4字节) = 152字节
接着我们开启指针压缩功能,执行如下命令:
## 开启指针压缩功能 java -XX:+UseCompressedOops -javaagent:ObjectSizeFetcherAgent-1.0-SNAPSHOT.jar com.twq.ArraySizer
得到结果如下:
我们可以看到new Integer[0]
的大小为16字节
,因为数组长度为0,所以这个数组的大小就是数组对象头的大小,又因为开启了指针压缩功能,所以数组对象头大小为16字节
,其他长度数组所占内存解释:
new Integer[1]
的大小为:对象头16字节 + 1个引用类型大小4字节 + 对齐补充4字节 = 24字节new Integer[2]
的大小为:对象头16字节 + 2个引用类型大小8字节 = 24字节new Integer[3]
的大小为:对象头16字节 + 3个引用类型大小12字节 + 对齐补充4字节 = 32字节new Integer[4]
的大小为:对象头16字节 + 4个引用类型大小16字节 = 32字节new Integer[]{2, 3, 4, 5}
的大小位:new Integer[4]
的大小32字节 + 4 * (Integer对象头12字节 + Integer中int类型属性大小4字节) = 96字节
总结
Java单个对象内存布局总结:
- Java单个对象所占内存大小等于:对象头所占内存大小 + 对象实例属性数据所占内存大小 + 对齐填充所占内存大小
- 对象头包含存储对象自身的运行时数据和类型指针数据两类数据。在64位操作系统中,如果开启指针压缩功能的话,对象头所占内存大小为12字节;如果没有开启指针压缩功能的话,对象头所占内存大小为16字节
- 对齐填充:JVM要求对象的大小必须是8的整数倍,若不是,需要补位对齐。
- static变量属于类,不属于实例,存放在全局数据段,普通变量才纳入Java对象占用空间的计算
- 引用类型在32位操作系统上每个占用4字节,在64位操作系统上:
- 没有开启指针压缩功能的话占用8字节
- 开启指针压缩功能的话占用4字节
- 在64位操作系统上,数组对象的对象头占用24字节,启用指针压缩功能后占用16字节,之所以比普通对象占用内存多是因为数组需要额外的空间存储数组的长度。
以上是单个简单的Java对象所占内存的大小的计算,对于复杂的Java对象所占内存的大小的计