【JVM】对象的内存布局,怎样确定对象的大小

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

对象在内存中的布局,主要有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字节。当然也可以使用+UseCompressedOops直接开启指针压缩。

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

原创文章 193 获赞 651 访问量 122万+

猜你喜欢

转载自blog.csdn.net/qq_33591903/article/details/106077584