在计算机发展的早期阶段,硬件的发展速度慢,容量小,所以软件开发人员写起代码里对 byte、bit 都是「斤斤计较」,这才使用写出来的应用能在我们今天看起来那么小的,配置那么低的硬件中运行良好,同时效果惊人。
那么计算机发展到今天,硬件看似配置越来越高,但依然架不住你随意写,搞不好应用就挂了。另外像游戏等一些行业还是「锱铢必较」,让应用能 稳定的运行。在Java 应用里,要想精确计算,需要对于对象的占用大小做到心里有数。那这篇文章一起来看看, 在 Java 的世界里,一个对象的大小究竟是多少呢?有哪些方式能够计算对象大小。
要看一个对象的大小,首先需要看Java 运行的平台是 32位还是 64位的 ,其次还要看对象内有多少属性(field)。最后还有一些 JVM 自身需要 在对象里记录的信息,比如说有GC的状态、同步状态、数组长度等等多种信息。这些项汇总求和,基本就是一个对象的大小了。不过VM为了效率,会采用固定长度,比如 8 位的整数倍来统一存储。这种情况下,如果原对象大小不足时,就会扩展对齐来存储。
好的,下面来看下 JVM 里,一个Java 对象大小占用多少。
由于现在基本操作系统基本都是64位,咱们后面都以64位 JVM 来说明。
首先来看两个例子:
- public class App {
- }
- public class App {
- private byte a;
- private int b;
例子1里, App 这个对象占用内存大小是多少 byte 呢?答案是 16。例子2 又是多少呢?答案是 24。
这里是怎么计算的呢?
计算方式
和开头的文字描述类似,在Java 里一个对象大小,是由这些内容组成
一个对象Object 大小 = Header + Primitive Fields + Reference Fields + Alignment & Padding`
其中的 Header 部分,就是 JVM 用于记录特定信息的,一版也叫做 object header,在 OpenJDK 的汇总页里,描述是这样的:
Common structure at the beginning of every GC-managed heap object. (Every oop points to an object header.) Includes fundamental information about the heap object's layout, type, GC state, synchronization state, and identity hash code. Consists of two words. In arrays it is immediately followed by a length field. Note that both Java objects and VM-internal objects have a common object header format.
我们看到 object header 由这些部分组成:
- mark word
- klass pointer
- (Optinal) 如果是数组,会记录数组的长度
mark word
The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits.
klass pointer
The second word of every object header. Points to another object (a metaobject) which describes the layout and behavior of the original object. For Java objects, the "klass" contains a C++ style "vtable".
总结一下,对象头里,基本是 GC的状态、同步状态、identity hash code,数组长度,以及 class 元信息的指针。
header 的长度由两个 word 组成。mark word 在 64位VM里,长度是 8 bytes。klass pointer 的长度64位VM下受参数-XX:+UseCompressedOops配置控制, 可能是 4 bytes,也可能是8 bytes。
例子1,我们看关闭情况-XX:-UseCompressedOops
- 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) 38 c4 6a 97 (00111000 11000100 01101010 10010111) (-1754610632)
- 12 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
- Instance size: 16 bytes
- Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
16 bytes 全都是对象头。
如果打开,则 object header 占 12 个bytes, 另外4个会补齐。
- 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) 05 c0 00 f8 (00000101 11000000 00000000 11111000) (-134168571)
- 12 4 (loss due to the next object alignment)
- Instance size: 16 bytes
- Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
例子2,输出是这样:
- 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) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
- 12 4 int App.b 0
- 16 1 byte App.a 0
- 17 7 (loss due to the next object alignment)
- Instance size: 24 bytes
- Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
增加了 field 的占用, 这里的占用大小,就是咱们常说的 基本数据类型 的大小。如果有对象引用类型,就再加上这些的大小即可。
工具
下面的这些工具及方式,可用来计算对象的大小。
1.JOL
JOL 是 OpenJDK 提供的一个工具,在项目中添加依赖后可直接使用。
- <dependency>
- <groupId>org.openjdk.jol</groupId>
- <artifactId>jol-core</artifactId>
- <version>0.9</version>
- </dependency>
然后代码里直接输出即可。
- public class App {
- private byte a;
- private int b;
- public static void main(String[] args) {
- // 这里就会输出和上面类似的内容
- System.out.println(ClassLayout.parseClass(new App()).toPrintable());
- }
- }
2.Instrument
在 Instrument 包的 Instrumentation类内,有一个可以直接获取对象大小的方法,注释如下:
- /**
- * Returns an implementation-specific approximation of the amount of storage consumed by
- * the specified object. The result may include some or all of the object's overhead,
- * and thus is useful for comparison within an implementation but not between implementations.
- *
- * The estimate may change during a single invocation of the JVM.
- *
- * @param objectToSize the object to size
- * @return an implementation-specific approximation of the amount of storage consumed by the specified object
- * @throws java.lang.NullPointerException if the supplied Object is <code>null</code>.
- */
那怎样拿到 Instrument,从而调用这个方法,一般是通过 Agent attach 到 VM上来得到,一种是Agent 实现premain方法,一种是agentMain方法,区别在于attach的时机。
比如你自己定义了一个Agent 的 jar
- import java.lang.instrument.Instrumentation;
- public class ObjectSizeFetcher {
- private static Instrumentation instrumentation;
- public static void premain(String args, Instrumentation inst) {
- instrumentation = inst;
- }
- public static long getObjectSize(Object o) {
- return instrumentation.getObjectSize(o);
- }
- }
后面咱们在代码里直接静态调用 getObjectSize 方法就行了。
3.SA
我之前的文章(Java虚拟机的显微镜 Serviceability Agent)里介绍过 SA(Serviceablity Agent), 通过 SA,可以做到许多剖析 JVM 的事情。观察对象的组成和大小当然也不在话下。
通过SA来观察上面例子2,效果是这样的:
咱们能看到,整个对象先是_mark(mark word), 之后是 compressed klass(klass pointer ), 再后面是实例包含的属性。 如果红框所示,在 klass pointer 里面包含一个 _layout_helper,显示的就是该对象的大小。
此外,在SA里的 Console 里,可以像命令行一样交互,也可以显示对象的大小。
PS: 对于对象的大小,JVM在初始化分配的时候,会对 field 有个「重排序」,给 field 的分配排序,从而节省空间。比如先分配byte 或者 boolean 之后,再分配 int 这些,那 byte 之后可能需要增加 3 bytes 进行 padding,重排序可以减小空间占用。
比如有这些属性:
- public class App {
- private byte a;
- private int b;
- private boolean c;
- private float d;
- private char e = 'a';
- }
- 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) 61 c1 00 f8 (01100001 11000001 00000000 11111000) (-134168223)
- 12 4 int App.b 0
- 16 4 float App.d 0.0
- 20 2 char App.e a
- 22 1 byte App.a 0
- 23 1 boolean App.c false
- Instance size: 24 bytes
- Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
在输出对象内容,我们发现,并不是按属性的声明顺序来分配的,这样只占用24 bytes,如果按声明顺序,那应该先分配byte, 之后再分配 int 这样为了对齐,会额外增加 3 个 bytes的(alignment/padding gap)最后可能会占用到32bytes。
为了减少空间浪费,一般情况下,field分配的优先依次顺序是:
double > long > int > float > char > short > byte > boolean > object reference。
这里有个基本的原则是:尽可能先分配占用空间大的类型。