Java对象的内存布局

一个对象在内存中究竟是怎样进行布局的,如何依据代码去确定对象占据的大小,本文将进行粗略地探讨。

对象在内存中的布局,主要有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分代年龄、锁状态标志、持有锁的线程、偏向线程Id、偏向时间戳等。在32位系统上,Mark Word为32位,在64位系统上,为64位,即8个字节。Mark Word在不同的锁标志(lock)下,结构也不尽相同。当然,lock相同时,比如lock=01,这时候需要借助偏向锁标记(biased_lock)来具体确定对象是否存在偏向锁

Klass Word

该区域存储对象的类型指针,该指针指向对象类元数据,虚拟机能够通过这个指针,来确定该对象到底是哪个类的实例。在32位系统上,该区域占用32位,在64位系统上,占用64位,但是!当64位机器设置最大堆内存为32G以下时,将会默认开启指针压缩,将8字节的指针压缩为4字节。当然也可以使用+UseCompressedOops直接开启指针压缩。

Array Length

如果对象是一个数组,那么对象头会增加一个额外的区域,用来记录数组的长度。在32位系统上,该区域占用32位,在64位系统上,占用64位,同样的,如果开启指针压缩,则会压缩到32位。

可以看得出来,一个非数组的对象的对象头占用12个字节,即Mark Word(8)+Klass Word(4)

实例数据

基本数据类型占用的长度如下:
在这里插入图片描述
对于引用变量占用的长度,同样视系统位数而定。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.14</version>
	<scope>provided</scope>
</dependency>

然后在代码中这样调用:

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字节。

4)对象中包含数组

在这里插入图片描述

指针压缩概要

64位平台上默认打开
1)使用-XX:+UseCompressedOops压缩对象指针 "oops"指的是普通对象指针(“ordinary” object pointers)。 Java堆中对象指针会被压缩成32位。 使用堆基地址(如果堆在低26G内存中的话,基地址为0)
2)使用-XX:+UseCompressedClassPointers选项来压缩类指针
3)对象中指向类元数据的指针会被压缩成32位
4)类指针压缩空间会有一个基地址

元空间和类指针压缩空间的区别
1)类指针压缩空间只包含类的元数据,比如InstanceKlass, ArrayKlass 仅当打开了UseCompressedClassPointers选项才生效 为了提高性能,Java中的虚方法表也存放到这里 这里到底存放哪些元数据的类型,目前仍在减少
2)元空间包含类的其它比较大的元数据,比如方法,字节码,常量池等。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值