日常使用Java进行业务开发时,我们基本不关心一个Java对象的大小,所以经常因为错误的估算导致大量的内存空间在无形之间被浪费了,所以今天笔者就基于这篇文章来聊聊一个Java对象的大小,希望对读者日常堆内存评估有所帮助。
一、Java对象构成详解
1.整体构成概述
我们这里就以Hotspot虚拟机来探讨Java对象的构成,如下所示,可以看到Java对象的整体构成分为:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
2.对象头
(1) Mark World
而对象头是由两部分组成的,第一部分用于存储对象自身的数据,也就是我们常说的Mark World,它记录着一个对象的如下信息:
- 哈希码(hashCode)
- GC分代年龄
- 锁状态标志
- 线程持有锁
- 偏向锁id
- 偏向时间戳
(2) 类型指针
再来说说类型指针,它记录着当前对象的元数据的地址,虚拟机可通过这个指针确定当前对象属于哪个类的实例,也就是说如果我们希望获得这个对象的元数据信息是可以通过类型指针定位到。 需要注意的是,在JDK8版本默认情况下,Mark World默认开启了指针压缩,这使得这一部分在64位的操作系统中的情况下,长度由原来的8个字节(64位)变为4个字节(32位)。
(3) 数组长度
最后一部分就是数组长度,如果当前对象是基本类型的数组,那么这4位则是记录数组的长度,为什么说是基本类型呢?原因很简单,普通Java对象的的大小是可以通过元数据信息计算获得,而基本类型的数组却却无法从元数据信息中计算获得,所以我们就需要通过4个字节记录一下数组的长度以便计算。
3.实例数据
这一点就不多说了,这就是对象真正存储的有效信息,这些实例数据可以是从父类继承也可以是自定义字段,因为实例数据可能存在多个,Hotspot虚拟机定义了实例对象内存分配的先后顺序:
- long/double(8字节)
- int(4字节)
- short/char(2字节)
- byte/boolean(1字节)
- oops(Ordinary Object Pointers 普通对象指针)
4.对齐填充
Hotspot虚拟机为了保证在指针压缩的情况下,32字节的空间仍然表示32G的内存空间地址,用到了8位对齐填充的思想,既保证了缓存命中率可以记录更多的对象,又能记录更多的对象地址。 因为指针压缩涉及的知识点比较多,笔者后续会单独开一个篇幅进行补充,这里我们有先说一下对其填充,假设我们现在有这样一个Java对象,可以看到在实例数据部分,它有8字节的long变量和4字节的int变量,合起来是12字节:
public
class
Obj
{
private
long id;
private
int age;
}
而8位对齐填充的意思就是实例数据部分的和要能够被16整除,所以对于这个对象的实例部分,我们还需要补充4个字节做到8位的对齐填充:
二、基于JOL了解Java对象的构成
1.前置步骤
了解了Java对象的组成之后,我们不妨通过JOL(Java Object Layout)来印证一下笔者的观点,所以我们需要在项目中引入下面这个依赖开始本次的实验:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
2.空对象
首先是一个空对象EmptyObj ,可以看到这个对象没有任何成员变量:
class
EmptyObj
{
}
我们都知道默认情况下,JDK8是开启指针压缩的,可以看到object header总共12字节,其中Mark World占了前8字节(4+4),类型指针占了4字节,加起来是12字节,而Java对象要求16位对齐,所以需要补齐4位,总的结果是16字节:
com.sharkChili.webTemplate.EmptyObj 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 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
我们再来看看关闭指针的压缩的结果,首先我们设置JVM参数将指针压缩关闭:
-XX:-UseCompressedClassPointers
此时我们就发现指针由原来是object header多了4位,原本被压缩的指针占用空间被还原了(offset为8-12的部分),总的计算结果为16字节,无需对齐填充:
com.sharkChili.webTemplate.EmptyObj 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) c0 34 b8 1c (11000000 00110100 10111000 00011100) (481834176)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
3.数组对象
我们再来看看数组对象,在默认开启指针压缩的情况下,我们创建了一个长度为3的数组:
com.sharkChili.webTemplate.EmptyObj 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) c0 34 b8 1c (11000000 00110100 10111000 00011100) (481834176)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
可以看到:
- Mark World占了8字节
- 指针4字节(offfset为8这一部分)
- offset为12这一部分也有了4字节的空间,记录了一个值3即数组长度
所以8+4+4=16,对象头刚刚好8位对齐,故无需对齐填充。
再看看实例数据部分(offset为16)这一部分,因为数组中有3个整形所以长度size为12,需要补充4字节达到8位对齐,最终这个数组对象的长度为16(对象头)+16(实例数据部分)=32字节:
[I 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) 6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
12 4 (object header) 03 00 00 00 (00000011 00000000 00000000 00000000) (3)
16 12 int [I.<elements> N/A
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
我们再来看看关闭指针压缩的结果,可以看到mark word和指针都占了8位,加上数组长度的4位,最终对象头为20位,8位对齐后为24位。 同理实例部分还是12字节的数组元素大小加4字节的8对齐字节,关闭指针压缩后的对象大小为40字节:
[I 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) 68 0b 85 1c (01101000 00001011 10000101 00011100) (478481256)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
16 4 (object header) 03 00 00 00 (00000011 00000000 00000000 00000000) (3)
20 4 (alignment/padding gap)
24 12 int [I.<elements> N/A
36 4 (loss due to the next object alignment)
Instance size: 40 bytes
Space losses: 4 bytes internal + 4 bytes external = 8 bytes total
4.带有成员变量的对象
我们再来说说带有成员变量的Java对象,也就是我们日常使用的普通Java对象:
class
NormalObject
{
int a;
short b;
byte c;
}
默认开启指针压缩的情况下,对象头为8+4=12字节,而实例数据部分,参考上文的实例数据顺序,我们的NormalObject的实例数据内存分配顺序为int、short、byte。 虚拟机为了更好的利用内存空间,看到对象头还差4字节才能保证对象头8位对齐填充,故将实例数据int作为对齐填充移动至对象头。
所以实例数据部分长度是2+1+5(对齐填充),最终在指针压缩的情况下,当前对象长度为24字节。
com.sharkChili.webTemplate.NormalObject 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 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int NormalObject.a 0
16 2 short NormalObject.b 0
18 1 byte NormalObject.c 0
19 5 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 5 bytes external = 5 bytes total
同理,关闭指针压缩,相比读者现在也知道如何计算了,笔者这里就不多赘述了,答案是是对象头8+8,实例数据4+2+1+1(对齐填充),即关闭指针压缩情况下,当前普通对象大小为24字节:
com.sharkChili.webTemplate.NormalObject 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) 10 35 0b 1d (00010000 00110101 00001011 00011101) (487273744)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
16 4 int NormalObject.a 0
20 2 short NormalObject.b 0
22 1 byte NormalObject.c 0
23 1 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 1 bytes external = 1 bytes total
5.带有数组的对象
最后我们再来看看带有数组的对象:
class NormalObject {
int a;
short b;
byte c;
int[] arr = new int[3];
}
先来看看开启指针压缩8+4+int变量作为对齐填充即16字节,注意很多读者会认为此时还需要计算数组长度,实际上数组长度记录的是当前对象为数组情况下的数组的长度,而非成员变量的数组长度,所以我们的对象头总的大小就是16。
然后实例数据部分4+2+1+1(对齐填充),最后就是数组引用4+4(对齐填充),最终结果为16+8+8即32:
com.sharkChili.webTemplate.NormalObject 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 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int NormalObject.a 0
16 2 short NormalObject.b 0
18 1 byte NormalObject.c 0
19 1 (alignment/padding gap)
20 4 int[] NormalObject.arr [0, 0, 0]
Instance size: 24 bytes
Space losses: 1 bytes internal + 0 bytes external = 1 bytes total
关闭指针压缩情况下,对象头8+8。实例数据4+2+1+1(对齐填充),再加上数组引用的4字节+4字对齐填充,最终计算结果为32字节。
com.sharkChili.webTemplate.NormalObject 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) 48 35 f8 1c (01001000 00110101 11111000 00011100) (486028616)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
16 4 int NormalObject.a 0
20 2 short NormalObject.b 0
22 1 byte NormalObject.c 0
23 1 (alignment/padding gap)
24 4 int[] NormalObject.arr [0, 0, 0]
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 1 bytes internal + 4 bytes external = 5 bytes total
小结
总的来说要想获取Java对象的大小,我们只需按照如下步骤即可精确计算:
- mark world 8位。
- 确认是否开启指针压缩,以计算类型指针大小。
- 是否是数组,若是则增加4字节数组长度位。
- 计算对象头总和进行8位填充。
- 实例数据按照顺序排列并计算总和,并进行8位填充。
- 引用数据计算总和,并进行8位填充。
- 综合上述计算结果。