对象内存布局

在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

下图展示了普通对象实例与数组对象实例的数据结构:

20170419212953720.png

1. 对象头(Header)

HotSpot 虚拟机的对象头包括两部分信息:

  1. MarkWord
    用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。

    • 在 32 位虚拟机中,这部分数据长度为 32bit(4 字节)。
    • 在 64 位虚拟机(未开启压缩指针)中,这部分数据长度为 64bit(8 字节)。
    • 官方称它为"MarkWord"。
  2. Klass Pointer(类型指针)
    对象头的一部分是 klass 类型指针,即对象指向它的类元数据的指针。虚拟机通过这个指针来确定这个对象是哪个类的实例。

    • 32 位系统:4 字节。
    • 64 位系统

      • 开启指针压缩(默认)或最大堆内存 < 32GB 时:4 字节。
      • 否则(未开启压缩):8 字节。
  3. 数组长度(仅数组对象有)
    如果对象是一个数组,那在对象头中还必须有一块数据用于记录数组长度。

    • 占用 4 字节
    • 注:int 最大值为 2^31,因此 Java 数组(包含字符串)最长约为 2GB。

2. 实例数据(Instance Data)

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。

基本数据类型(Primitive Type)所占内存如下:

Primitive TypeMemory Required (bytes)
boolean1
byte1
short2
char2
int4
float4
long8
double8

引用类型大小:

  • 32 位系统:每个引用占用 4 字节。
  • 64 位系统

    • 开启指针压缩(默认):每个引用占用 4 字节。
    • 未开启指针压缩:每个引用占用 8 字节。

3. 对齐填充(Padding)

第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。

由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说,就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或者 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

对象大小计算

计算对象大小时,需遵循以下要点:

  1. 32 位系统

    • Class 指针:4 字节。
    • MarkWord:4 字节。
    • 对象头合计:8 字节
  2. 64 位系统(未开启指针压缩)

    • Class 指针:8 字节。
    • MarkWord:8 字节。
    • 对象头合计:16 字节
  3. 64 位系统(开启指针压缩 或 JVM 堆最大值 < 32G)

    • Class 指针:4 字节。
    • MarkWord:8 字节。
    • 对象头合计:12 字节
    • 注:JDK 1.8 默认启用指针压缩。
  4. 数组对象头大小

    • 数组对象头 = 普通对象头 + 数组长度(4 字节)+ 对齐填充(如需)。
    • 开启压缩时:12 字节(头) + 4 字节(长度) = 16 字节。
    • 未开启压缩时:16 字节(头) + 4 字节(长度) = 20 字节(通常会对齐到 24 字节)。
  5. 计算公式

    • 普通对象 Shallow Size = 对象头 + 实例数据 + 填充。
    • 数组对象 Shallow Size = 数组对象头(12/16 字节) + 数组长度(4 字节) + (length * 引用指针大小 4/8 字节) + 填充。
    • 注:MarkWord 在 64 位系统中始终为 8 字节;Class Pointer 及 Object Ref Pointer 压缩时为 4 字节,不压缩为 8 字节。
  6. 静态属性不算在对象大小内。

HotSpot 对象模型(OOP-Klass)

HotSpot 中采用了 OOP-Klass 模型,它是描述 Java 对象实例的模型,分为两部分:

  • Klass:类被加载到内存时,就被封装成了 klass。Klass 包含类的元数据信息,像类的方法、常量池这些信息都是存在 klass 里的。你可以认为它是 Java 里面的 java.lang.Class 对象,记录了类的全部信息。
  • OOP(Ordinary Object Pointer):指的是普通对象指针,它包含 MarkWord 和元数据指针。

    • MarkWord:用来存储当前指针指向的对象运行时的一些状态数据。
    • 元数据指针:指向 klass,用来告诉你当前指针指向的对象是什么类型,也就是使用哪个类来创建出来的。

为何设计为一分为二的对象模型?
这是因为 HotSpot JVM 的设计者不想让每个对象中都含有一个 vtable(虚函数表)。所以就把对象模型拆成 klass 和 oop,其中 oop 中不含有任何虚函数,而 klass 就含有虚函数表,可以进行 method dispatch。

实践验证(JOL)

我们可以使用 JOL (Java Object Layout) 工具来查看对象的具体内存布局。

测试代码:

package com.daicy.jvm;

import org.openjdk.jol.info.ClassLayout;

/**
 * @author: create by daichangya
 * @version: v1.0
 * @description: com.daicy.jvm
 * @date: 12/28/20
 */
public class MarkdownMain {
    // 关闭指针压缩参数:-XX:-UseCompressedOops
    public static void main(String[] f) {
        System.out.println(ClassLayout.parseInstance(new Integer(2)).toPrintable());
        System.out.println(ClassLayout.parseInstance(new Long(2)).toPrintable());
        System.out.println(ClassLayout.parseInstance(new MyIntArray()).toPrintable());
        System.out.println(ClassLayout.parseInstance(new MyLong()).toPrintable());
        System.out.println(ClassLayout.parseInstance(new MyLong[]{new MyLong(), new MyLong(), new MyLong()}).toPrintable());
    }

    private static class MyIntArray {
        public int[] intArray;
    }

    private static class MyLong {
        public volatile long usefulVal;
        public volatile Long anotherVal;
        public MyRef myRef;
    }

    private static class MyRef {
        Integer integer = new Integer(15);
    }
}

Maven 依赖:

<dependency>
  <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.9</version>
</dependency>

1. 默认情况(JDK 1.8 开启指针压缩)

java.lang.Integer object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           ae 22 00 f8 (10101110 00100010 00000000 11111000) (-134208850)
     12     4    int Integer.value                             2
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

java.lang.Long object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           f5 22 00 f8 (11110101 00100010 00000000 11111000) (-134208779)
     12     4        (alignment/padding gap)                  
     16     8   long Long.value                                2
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

com.daicy.jvm.MarkdownMain$MyIntArray object internals:
 OFFSET  SIZE    TYPE DESCRIPTION                               VALUE
      0     4         (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4         (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4         (object header)                           5a f3 00 f8 (01011010 11110011 00000000 11111000) (-134155430)
     12     4   int[] MyIntArray.intArray                       null
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

com.daicy.jvm.MarkdownMain$MyLong object internals:
 OFFSET  SIZE                               TYPE DESCRIPTION                               VALUE
      0     4                                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                                    (object header)                           d8 f3 00 f8 (11011000 11110011 00000000 11111000) (-134155304)
     12     4                     java.lang.Long MyLong.anotherVal                         null
     16     8                               long MyLong.usefulVal                          0
     24     4   com.daicy.jvm.MarkdownMain.MyRef MyLong.myRef                              null
     28     4                                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

[Lcom.daicy.jvm.MarkdownMain$MyLong; object internals:
 OFFSET  SIZE                                TYPE DESCRIPTION                               VALUE
      0     4                                     (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                                     (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                                     (object header)                           18 f4 00 f8 (00011000 11110100 00000000 11111000) (-134155240)
     12     4                                     (object header)                           03 00 00 00 (00000011 00000000 00000000 00000000) (3)
     16    12   com.daicy.jvm.MarkdownMain$MyLong MarkdownMain$MyLong;.<elements>           N/A
     28     4                                     (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

分析:

  • 整个对象大小要是 8 的倍数,否则补全。
  • 64 位开启压缩:MarkWord 8 字节,Class Pointer 4 字节(对象头共 12 字节)。
  • 引用类型:4 字节。
  • 数组类型:存在【数组长度】个引用类型 + 数组长度 int(4 字节)。

2. 关闭指针压缩(-XX:-UseCompressedOops)

# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
java.lang.Integer object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e0 a9 7e 15 (11100000 10101001 01111110 00010101) (360622560)
     12     4        (object header)                           ea 7f 00 00 (11101010 01111111 00000000 00000000) (32746)
     16     4    int Integer.value                             2
     20     4        (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Long object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           18 e5 7e 15 (00011000 11100101 01111110 00010101) (360637720)
     12     4        (object header)                           ea 7f 00 00 (11101010 01111111 00000000 00000000) (32746)
     16     8   long Long.value                                2
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

com.daicy.jvm.MarkdownMain$MyIntArray object internals:
 OFFSET  SIZE    TYPE DESCRIPTION                               VALUE
      0     4         (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4         (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4         (object header)                           60 d1 84 0d (01100000 11010001 10000100 00001101) (226808160)
     12     4         (object header)                           ea 7f 00 00 (11101010 01111111 00000000 00000000) (32746)
     16     8   int[] MyIntArray.intArray                       null
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

com.daicy.jvm.MarkdownMain$MyLong object internals:
 OFFSET  SIZE                               TYPE DESCRIPTION                               VALUE
      0     4                                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                                    (object header)                           90 db 84 0d (10010000 11011011 10000100 00001101) (226810768)
     12     4                                    (object header)                           ea 7f 00 00 (11101010 01111111 00000000 00000000) (32746)
     16     8                               long MyLong.usefulVal                          0
     24     8                     java.lang.Long MyLong.anotherVal                         null
     32     8   com.daicy.jvm.MarkdownMain.MyRef MyLong.myRef                              null
Instance size: 40 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

[Lcom.daicy.jvm.MarkdownMain$MyLong; object internals:
 OFFSET  SIZE                                TYPE DESCRIPTION                               VALUE
      0     4                                     (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                                     (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                                     (object header)                           18 de 84 0d (00011000 11011110 10000100 00001101) (226811416)
     12     4                                     (object header)                           ea 7f 00 00 (11101010 01111111 00000000 00000000) (32746)
     16     4                                     (object header)                           03 00 00 00 (00000011 00000000 00000000 00000000) (3)
     20     4                                     (alignment/padding gap)                  
     24    24   com.daicy.jvm.MarkdownMain$MyLong MarkdownMain$MyLong;.<elements>           N/A
Instance size: 48 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

分析:

  • 64 位未开启压缩:MarkWord 8 字节,Class Pointer 8 字节(对象头共 16 字节)。
  • 引用类型:8 字节。
  • 数组对象头占用 24 bytes(16 字节头 + 4 字节长度 + 4 字节填充),启用压缩后占用 16 字节(12 字节头 + 4 字节长度)。
  • 比普通对象占用内存多是因为需要额外的空间存储数组的长度。
  • 基础数据类型数组占用的空间包括数组对象头以及基础数据类型数据占用的内存空间。
  • 由于对象数组中存放的是对象的引用,所以:

    • 数组对象 Shallow Size = 数组对象头(含数组长度 4 字节)+ length * 引用指针大小(4/8 字节)+ 填充。
    • Retained Size = Shallow Size + length * 每个元素的 Retained Size。
参考链接:https://www.jianshu.com/p/91e398d5d17c 中介绍了另一种看对象模型的方式,还可以看 Shallow Size 和 Retained Size。

补充:String 对象大小示例

因此,在关闭指针压缩时,一个 String 对象的大小为:

  • Shallow Size = 对象头大小 16 字节 + int 类型大小 4 字节 + 数组引用大小 8 字节 + padding 4 字节 = 32 字节
  • Retained Size = Shallow Size + char 数组的 Retained Size。

在开启指针压缩时,一个 String 对象的大小为:

  • Shallow Size = 对象头大小 12 字节 + int 类型大小 4 字节 + 数组引用大小 4 字节 + padding 4 字节 = 24 字节
  • Retained Size = Shallow Size + char 数组的 Retained Size。

有关 Shallow Size 和 Retained Size 请参考:
使用 MAT 时的 Shallow Size 和 Retained Size 的区别


说明:本文基于 HotSpot 虚拟机(JDK 1.8 为主)整理,默认开启指针压缩。不同 JDK 版本或不同虚拟机实现(如 OpenJ9)可能在对象头布局上存在差异。