探究Java对象的大小

对象的内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

本文不讨论对象头中里面有什么,在32位系统中,对象头占8字节;在64位系统中(未开启指针压缩),对象头占16字节。

第二部分实例数据就是对象真正存储的有效数据,也就是咱们在类中定义的变量。之前在这篇 为什么short、byte会被提升为int,及基本类型的真实大小 文章中讨论过,short、byte、boolean 等类型在栈中实际也都是占用4字节,但是在堆中不必哦,例如 byte 数组,每个元素就占一字节,在其它对象中,byte也只占一字节。

第三部分对齐填充并不一定是必需存在的,仅仅是占位符的作用。由于虚拟机都要求对象起始地址必须是8字节的整数倍,所以对象的大小也必须是8字节的整数倍。当对象实例数据部分没有对齐时,就需要对齐填充来补全。(64位开启指针压缩后,对象头只有12字节,也可能会对齐填充,哪不齐就填哪,不是一定在末尾)

JOL工具

本文使用 JOL 工具测量对象的大小,JOL全称为Java Object Layout,是分析JVM中对象布局的工具,添加依赖:

1
implementation("org.openjdk.jol:jol-core:0.14")

使用方式,可以传 类 或者 实例对象。

1
2
3
System.out.println(ClassLayout.parseInstance(yourObject).toPrintable());
// or
System.out.println(ClassLayout.parseClass(Test.class).toPrintable());

影响对象大小的启动参数

这节会讨论一些会影响对象大小的启动参数,所有启动参数的解释都可以在源码中找到: http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/tip/src/share/vm/runtime/globals.hpp?utm_source=ld246.com

CompactFields

-XX:+CompactFields :在前一个字段之间的空白处分配非静态字段,默认开启。

开启该参数后,比较窄的变量可能会打破原有顺序,插入到更前的空隙位置中。

1
2
3
4
5
6
// 测试类
class A {
private boolean a;
private char b;
private long e;
}

开启CompactFields

关闭CompactFields

UseCompressedOops

由于在32位系统中,最大获取的空间地址为 4GB,对于有些应用而言不够,因此需要使用64位系统。

之前说了,在64位系统中,对象头所占空间更大,有16字节,不仅如此,原本占4字节的对象指针在64位系统中(未开启指针压缩)也膨胀了一倍,变为8字节,因此在64位系统下,会着实浪费不少内存空间。

此时,我们就可以开启指针压缩了。
-XX:+UseCompressedOops:开启指针压缩,只对 64 位系统有效,默认开启

以下是测试类A

1
2
3
4
5
6
7
8
9
10
class A {
private short a;
private char b;
private long e;
private Object f;

public static void main(String[] args) {
System.out.println(ClassLayout.parseInstance(new A()).toPrintable());
}
}

类A 不开启指针压缩

类A 开启指针压缩

再看看数组对象的压缩情况:

1
2
3
public static void main(String[] args) {
System.out.println(ClassLayout.parseInstance(new Object[10]).toPrintable());
}

数组对象不开启指针压缩

数组对象开启指针压缩
发现开启压缩后,对象头的大小由20字节(比普通对象大是因为要多存储数组大小)变为16字节,且少了一个4字节的对齐填充。

64位系统下开启指正压缩后,有以下变化:

  • 对象头由 16 字节变为 12 字节。
  • 对象引用由 8 字节变为 4 字节。
  • 数组对象的对象头由20字节变为16字节。(由于还少了一次对齐填充,能压缩8字节对象头)