JVM-对象的内存布局和如何估算对象的大小

256 阅读5分钟

概述

在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例 数据(Instance Data)和对齐填充(Padding)。

1.对象头(Header)

其中对象头中一部分包含了该对象对应的类信息:Hotspot VM 使用对象头部的一个指针指向 Class 区域的方式来找到对象的 Class 描述,以及内部的方法、属性入口。如下图所示:

image.png

HotSpot 虚拟机的对象头包括两部分(非数组对象)信息,如下图所示: image.png

  • 第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳、对象分代年龄,这部分信息称为“Mark Word”;Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据自己的状态复用自己的存储空间。
  • 第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;
  • 如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据。因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是从数组的元数据中无法确定数组的大小。

这部分数据的长度在 32 位和 64 位的虚拟机(未开启压缩指针)中分别为 32bit 和 64bit。

例如,在 32 位的 HotSpot 虚拟机中,如果对象处于未被锁定的状态下,那么 Mark Word 的 32bit 空间中的 25bit 用于存储对象哈希码,4bit 用于存储对象分代年龄,2bit 用于存储锁标志位,1bit 固定为 0,如下表所示

image.png 在 32 位系统下,存放 Class 指针的空间大小是 4 字节,Mark Word 空间大小也是4字节,因此头部就是 8 字节,如果是数组就需要再加 4 字节表示数组的长度,如下表所示:

image.png 在 64 位系统及 64 位 JVM 下,开启指针压缩,那么头部存放 Class 指针的空间大小还是4字节,而 Mark Word 区域会变大,变成 8 字节,也就是头部最少为 12 字节,如下表所示:

image.png

压缩指针介绍参考另外一篇文章,待填坑 现在JVM在1.6之后,在64位操作系统下都是默认开启的。

2.实例数据(Instance Data)

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。 这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在 Java 源码中定义顺序的影响

3.对齐填充(Padding)

对齐填充不是必然存在的,没有特别的含义,它仅起到占位符的作用。
由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,也就是说对象的大小必须是 8 字节的整数倍。对象头部分是 8 字节的倍数,所以当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

那么为什么非要进行8字节对齐呢?这样岂不是浪费了空间资源?

Scott oaks在书上给出的理由是:
其实在JVM中(不管是32位的还是64位的),对象已经按8字节边界对齐了;对于大部分处理器,这种对齐方案都是最优的。所以使用压缩的oop并不会损失什么。如果JVM中的第一个对象保存到位置0,占用57字节,那下一个对象就要保存到位置64,浪费了7字节,无法再分配。这种内存取舍是值得的(而且不管是否使用压缩的oop,都是这样),因为在8字节对齐的位置,对象可以更快地访问。 不过这也是为什么JVM没有尝试模仿36位引用(可以访问64GB的内存)的原因。在那种情况下,对象就要在16字节的边界上对齐,在堆中保存压缩指针所节约的成本,就被为对齐对象而浪费的内存抵消了。

也就说,8字节对齐,是为了效率的提升,以空间换时间的一种方案。当然你还可以16字节对齐。但是8字节是最优选择。

4.如何估算对象的大小

1). 对象头的大小

以下暂不讨论数组对象

32位系统:
占用 8 字节(markWord 4字节 + kclass 4字节)
64位系统:
开启 UseCompressedOops(压缩指针)时,占用 12字节(markWord 8字节+kclass 4字节)
否则是16字节(markWord8字节+kclass8字节,开启时markWord8字节+kclass4字节)

2). 实例的大小

首先java中基本类型和一些固定的类型占用内存大小是固定的,参考 基础类型常用对象.

其中常用对象占用内存大小在是否开启指针压缩会不同,比如 Integer对象在开启压缩时占用16Byte,未开启时占用24ByteString对象占用大小:[比较复杂,待填坑]

image.png

其中refernce类型,
64位系统,当开启UseCompressedOops时 占用4Byte,否则占用8Byte.
32位系统,占用4Byte.

3). 对齐填充

如果对象头+实例数据的值不是8的倍数,那么会补上一些,补够8的倍数.

4). 举例

例1:

64位系统 jdk8 ,关闭压缩指针
jvm参数:-Xmx10m -XX:-UseCompressedOops

package com.yukuilab;

public class JHSDB_TestCase {

    private static class ObjectHolder{
        int value;
        //static Object obj = new Object();
    }
    public static void main(String[] args){
        ObjectHolder obj = new ObjectHolder();
        while(true){
            //让Jprofiler能抓去内存信息
        }
    }
}

使用Jprofiler抓取JHSDB_TestCase$ObjectHolder内存占用大小信息如下

image.png

根据计算符合预期
8 Byte(markWord )+ 8 Byte(Klass field)+ 4 Byte(int) + 4 Byte(padding) = 24 字节

开启指针压缩后: image.png 8 Byte(markWord )+ 4 Byte(Klass field)+ 4 Byte(int) = 16 字节

//static Object obj = new Object();取消注释重新执行程序,会发现JHSDB_TestCase$ObjectHolder内存占用不变还是16字节,这也从侧面证明了Static变量是Class的一部分,并不占用实例对象的内存大小,而是和其他该Class的信息一起存在jvm 中的方法区中.