一个对象在内存中究竟是怎样进行布局的,如何依据代码去确定对象占据的大小,本文将进行粗略地探讨。
对象在内存中的布局,主要有3个组成部分,包括对象头,实例数据与对齐填充。确定对象的大小,也是从这3个组成部分的入手。
对象头
其中对象头中又包括Mark Word与Klass Word。当该对象是一个数组时,对象头还会增加一块区域,用来保存数组的长度。以64位系统为例,对象头存储内容如下图所示:
|---------------------------------------------------------------------------------------------------------------| | Object Header (128 bits) | |---------------------------------------------------------------------------------------------------------------| | Mark Word (64 bits) | Klass Word (64 bits) | |---------------------------------------------------------------------------------------------------------------| | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:0 | lock:01 | OOP to metadata object | 无锁 |----------------------------------------------------------------------|---------|------------------------------| | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:01 | OOP to metadata object | 偏向锁 |----------------------------------------------------------------------|---------|------------------------------| | ptr_to_lock_record:62 | lock:00 | OOP to metadata object | 轻量锁 |----------------------------------------------------------------------|---------|------------------------------| | ptr_to_heavyweight_monitor:62 | lock:10 | OOP to metadata object | 重量锁 |----------------------------------------------------------------------|---------|------------------------------| | | lock:11 | OOP to metadata object | GC |---------------------------------------------------------------------------------------------------------------|
Mark Word
该区域主要存储hashcode、gc年龄、锁标志等。在32位系统上,Mark Word为32位,在64位系统上,为64位,即8个字节。Mark Word在不同的锁标志(lock)下,结构也不尽相同。当然,lock相同时,比如lock=01,这时候需要借助偏向锁标记(biased_lock)来具体确定对象是否存在偏向锁。
关于结合Mark Word讲锁的升级,可能要另外篇幅。不过,可以先看我的另外一篇文章Synchronized的优化,大致了解一下锁的优化。
Klass Word
该区域存储对象的类型指针,该指正指向对象类元数据(类元数据都在方法区中,对方法区不熟悉的同学,可以先参考我的另外一篇文章灵性一问——为什么用元空间替换永久代?),虚拟机能够通过这个指针,来确定该对象到底是哪个类的实例。在32位系统上,该区域占用32位,在64位系统上,占用64位,但是!当64位机器设置最大堆内存为32G以下时,将会默认开启指针压缩,将8字节的指针压缩为4字节。当然也可以使用+U***pressedOops直接开启指针压缩。
Array Length
前面说过,如果对象是一个数组,那么对象头会增加一个额外的区域,用来记录数组的长度。在32位系统上,该区域占用32位,在64位系统上,占用64位,同样的,如果开启指针压缩,则会压缩到32位。
可以看得出来,一个非数组的对象的对象头占用12个字节,即Mark Word(8)+Klass Word(4)。
实例数据
基本数据类型占用的长度如下:
类型 | 占用字节长度 |
byte | 1 |
short | 2 |
int | 4 |
long | 8 |
float | 4 |
double | 8 |
char | 2 |
boolean | 1 |
对于引用变量占用的长度,同样视系统位数而定。32位系统占用4字节,64位系统8字节,开启指针压缩那就占用4字节。
实例数据部分只会存放对象的实例数据,并不会存放静态数据。此外,子对象的实例数据部分会继承父类所有实例数据,包括私有类型,这里可以理解为子类拥有父类所有类型的成员变量,但在子类中无法直接访问这些私有实例变量。
对齐填充
这里的对齐填充有两方面:
(1)HotSpot虚拟机规定对象的起始地址必须是8的整数倍,也就是要求对象的大小必须是8的整数倍。因此如果一个对象的对象头+实例数据占用的总内存没有达到8的倍数时,会进行对齐填充,将总大小填充到最近的8的倍数上。
(2)字段与字段之前也需要对齐,字段对齐的最小单位是4个字节。
可以这样理解,虚拟机每次会为字段发放一个最近的4倍数的一个盒子。比如,有个类的字段有一个boolean和一个int,这时候先为boolean发放第一个大小为4字节的盒子,将boolean放入其中,占用1个字节,浪费3个字节,因为int占用4个字节,根本放不下,需要虚拟机再分配一个大小为4的盒子。
虚拟机不会按照字段声明的顺序去给字段分配盒子,而是会进行重排序,使得物尽其用。比如一个类有以下变量:char、int、boolean、byte。如果按照声明顺序去分配盒子的话,则需要为char分配一个盒子,浪费2个字节。再为int分配一个盒子,这个盒子正好满了,没有浪费。接着为boolean分配一个盒子,浪费3个字节。最后为byte分配一个盒子,又浪费3个字节。
在进行重排序后,此时可以按照int(4)、char(2)+boolean(1)+byte(1)的顺序,虚拟机可以只分配2个盒子,大大减少内存浪费。但是引用类型的字段必定在最后才分配。
例子
例子位于64位机器上,其都开启指针压缩。
(1)实例化一个没有任何属性的空对象,那么这个空对象占用的内存大小为多少呢?
很简单,对象头占用12字节,还会利用4字节进行填充,一共占用16字节。
(2)实例一个具有四个不同属性的对象
class Test { public char charP; public int intP; public boolean booleanP; public byte byteP; }
这就是对齐填充部分举的例子,对象头占用12字节,实例数据占用8字节,此时一共20字节,则对象填充需要占用4字节,一共占用24字节。
我们使用一个jol(Java Object Layout)工具来分析Test对象占据的内存大小。只要在maven项目中引入这个依赖就好:
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.8</version> </dependency>
然后在代码中这样调用:
package com.yang; import org.openjdk.jol.info.ClassLayout; public class Main { public static void main(String[] args) { Test test = new Test(); System.out.println(ClassLayout.parseInstance(test).toPrintable()); } }
输出如下:
看的出来,总大小确实为24字节。
(3)实例化一个具有父类的子类
class Father { public boolean publicFlag; private boolean privateFlag; public static boolean staticFlag; } public class Test extends Father { public boolean publicFlag; private int b; protected double c; Long d; }
猜猜看,实例化一个Test对象后,这个对象占据的内存大小是多少呢?
这里可能会有几个问题:
【1】子类的实例数据部分会排除掉父类的私有实例属性privateFlag吗?
【2】子类的实例数据部分会覆盖掉父类的同名实例属性吗?
带着这些疑问,我们直接使用jol查看对象内存大小:
可以看到,子类对象中包含了父类所有的实例变量,且首先分配父类实例变量,再分配子类实例变量。对象头还是占用12字节,父类实例变量占用4字节(包括2个字节的字段填充),子类实例变量占用20字节,对象填充占用4字节,一共占用40字节。