Java对象内存布局
JVM
将内存划分为程序计数器(Program Counter Register
)、虚拟机栈(VM Stack
)、本地方法栈(Native Method Stack
)、堆(Heap
)以及方法区(Method Area
)。作为开发者,我们最关注的是虚拟机栈以及堆这两块区域。虚拟机栈所需要的内存空间在编译期间即可明确,而堆内存所需要的空间需要在运行时才可确定。堆内存用于存放我们在程序中创建的对象,一旦没有足够的空间用于存放这些对象,即会抛出OutOfMemoryError
异常。在这种情况下,我们可以调整堆内存的大小,或者对程序进行优化。当我们采用后一种方式时,我们需要了解一个对象是如何占据堆内存空间的,或者说是了解一个对象是由哪些部分组成的。
对象的内存布局
在HotSpot
虚拟机中,对象在内存中的布局划分为3
个区域:对象头(Header
),实例数据(Instance Data
)以及对齐填充(Padding
)。
对象头
HotSpot
虚拟机对象的对象头一般包含两部分信息,第一部分用于存储对象自身的运行时数据,例如HashCode
、GC
分代年龄等信息。在32
位和64
位的JVM
中,这部分数据分别为32bit
和64bit
,官方称这部分数据为Mark Word
。
另一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM
通过这个指针确定对象是哪个类的实例。在32
位JVM
中,指针的长度为32bit
,在未开启压缩指针的64
位JVM
中,该指针的长度为64bit
,如果开启压缩指针,那么为32bit
。
之前提到对象头一般包含两部分信息,这是因为如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,并且这部分数据也随着JVM
位数的不同而不同:32
位的JVM
上,该区域的长度为32bit
,在64
位未开启压缩指针的JVM
中,这部分数据的长度为64bit
,否则为32bit
。
实例数据
实例数据部分是对象真正存储有效信息的区域,存储了代码中定义的各种字段的内容,包括从父类继承下来的字段和子类中定义的字段。
实例数据紧随对象头,为了提高存储空间的利用率,这部分数据的存储顺序会受到虚拟机分配策略参数和字段在Java
源码中定义顺序的影响。HotSpot
虚拟机默认的分配策略如下所示。
doubles & longs
ints & floats
shorts & chars
booleans & bytes
references
可以看出,相同宽度的字段总是被分配到一起,并且在满足这个条件的前提下,在父类中定义的字段会出现在子类字段之前。
对齐填充
对齐填充这部分不是必须存在的,这部分仅仅是起着占位符的作用。由于HotSpot
虚拟机的自动内存管理系统要求对象的起始地址必须是8
字节的整数倍,因此当对象实例部分数据没有对齐时,就需要对剩余的部分进行填充。
度量工具
从JDK 5
开始, Java
提供了Instrumentation API
,通过getObjectSize
方法来获取对象的大小,但是getObjectSize
方法存在如下两个缺陷,不能准确的计算对象的大小。
- 不能直接调用
getObjectSize
方法,而是需要通过-javaagent
参数指定一个特定的jar
文件(包含Instrumentation
代理)来启动Instrumentation
的代理程序。 - 如果一个对象中包含别的对象的引用,那么
getObjectSize
方法仅仅计算引用的大小,而不包括引用所指向的对象的大小。
由于上述两个缺陷,我们不能直接调用getObjectSize
方法来计算对象的大小,但是利用Java
的反射机制,我们可以完整的计算一个对象的大小。我们解析对象的每一个Field
(使用getDeclaredFields
),并遵从如下规则。
- 当
Field
是基本数据类型时,我们不再计算该Field
的大小,因为该Field
的大小已经被包含在getObjectSize
方法的返回值中。 - 当
Field
是静态数据或者是常量池中包含的数据,那么我们忽略这些数据,因为这些数据并不是属于对象的。 - 我们需要保存我们已经计算过的对象的引用,防止重复计算。
- 如果对象所属的类存在父类,还需要计算父类中成员变量的大小。
jvm-obj-size 是以上思想的具体实现,jvm-obj-size 实现了基本的获取对象本身的大小(sizeOf
,仅包含引用本身),以及获取对象真正的大小(fullSizeOf
,包含引用所指向的对象)的方法,具体用法以及测试代码详见README
文件。