JVM中 对象的内存布局 以及 实例分析

对象内存结构

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

对象头(Header)

HotSpot 虚拟机的对象头包括两部分信息:Mark Word 和 类型指针;如果是数组对象的话,还有第三部分(option)信息:数组长度
Mark Word
这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit 和64bit。Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
对象头信息是与对象定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的信息,它会根据对象的状态复用自己的存储空间。

存储内容 标志位 状态
对象哈希码、对象分代年龄 01 未锁定
指向锁记录的指针 00 轻量级锁定
指向重量级锁的指针 10 膨胀(重量级锁定)
空,不需要记录信息 11 GC标记
偏向线程ID、偏向时间戳、对象分代年龄 01 可偏向

👆标志位“01”就被复用了,根据不同的状态:“未锁定” or “可偏向” 来确定“01”存储所表示的内容。

类型指针(Class Pointer)
是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

数组长度(Length)[option]
如果对象时一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据。因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。

实例数据(Instance Data)

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

HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被分配到一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果CompactFields参数值为true(默认为true),那子类之中较窄的变量也可能会插入到父类变量的空隙之中。

对齐填充 (Padding)

对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。

对象占用内存大小

上面我们已经对对象在内存的布局有了一点你的了解,接下来我们来看看对象占用内存的大小。也就是对象内存结构的每个部分分别占用多少的内存。

对象头

普通对象占用内存情况:

  32 位系统 64 位系统(+UseCompressedOops) 64 位系统(-UseCompressedOops)
Mark Word 4 bytes 8 bytes 8 bytes
Class Pointer 4 bytes 4 bytes 8 bytes
对象头 8 bytes 12 bytes 16 bytes

数组对象占用内存情况:

  32 位系统 64 位系统(+UseCompressedOops) 64 位系统(-UseCompressedOops)
Mark Word 4 bytes 8 bytes 8 bytes
Class Pointer 4 bytes 4 bytes 8 bytes
Length 4 bytes 4 bytes 4 bytes
对象头 12 bytes 16 bytes 20 bytes
实例数据
Type 32 位系统 64 位系统(+UseCompressedOops) 64 位系统(-UseCompressedOops)
double 8 bytes 8 bytes 8 bytes
long 8 bytes 8 bytes 8 bytes
float 4 bytes 4 bytes 4 bytes
int 4 bytes 4 bytes 4 bytes
char 2 bytes 2 bytes 2 bytes
short 2 bytes 2 bytes 2 bytes
byte 1 bytes 1 bytes 1 bytes
boolean 1 bytes 1 bytes 1 bytes
oops(ordinary object pointers) 4 bytes 4 bytes 8 bytes


实例分析

环境

系统:macOS 10.12.5
JDK:jdk1.8.0_144

涉及JVM参数

-XX:+UseCompressedOops(JDK 8下默认为启用)

UseCompressedOops
Use 32-bit object references in 64-bit VM. lp64_product means flag is always constant in 32 bit VM
在64位系统中使用32位系统下引用的大小,也就是说,在64系统下回压缩普通对象的指针大小以节约内存占用的大小。



-XX:+CompactFields(JDK 8下默认为启用)

CompactFields
Allocate nonstatic fields in gaps between previous fields
分配一个非static的字段在前面字段缝隙中。这么做也是为了提高内存的利用率。



-XX:FieldsAllocationStyle=1 (JDK 8下默认值为‘1’)

FieldsAllocationStyle
0 - type based with oops first, 1 - with oops last, 2 - oops in super and sub classes are together
实例对象中有效信息的存储顺序:
0:先放入oops(普通对象引用指针),然后在放入基本变量类型(顺序:longs/doubles、ints、shorts/chars、bytes/booleans)
1:先放入基本变量类型(顺序:longs/doubles、ints、shorts/chars、bytes/booleans),然后放入oops(普通对象引用指针)
2:oops和基本变量类型交叉存储

关于上面的JVM选项含义,可以结合下面的实例分析,更便于理解。

实例

下文中无特殊说明,“对象占用内存大小”均指“对象自身占用内存大小”

实例一

/**
 * ① 将下载的 classmexer.jar 加入当前项目的classpath中
 * ② 启动Main是添加启动项:-javaagent:${classmexer_path}/classmexer.jar
 * -javaagent:/Users/linling/Documents/software/classmexer-0_03/classmexer.jar
 *
 * ③ JVM 参数:
 * -XX:+UseCompressedOops   (默认启用)
 * -XX:+CompactFields   (默认启用)
 * -XX:FieldsAllocationStyle=1      (默认为1)
 */
public class TheObjectMemory {

    private static Unsafe UNSAFE;

    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            UNSAFE = (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * ● 对象头:mark word(8 bytes) + class pointer(4 bytes) = 12 bytes
     * 因为在JDK 8 中"UseCompressedOops"选项是默认启用的,因此class pointer只占用了4个字节。
     * 同时,从属性'a'在内存中的偏移量为12也能说明,对象头仅占用了12bytes(属性a的分配紧跟在对象头后)
     *
     * ● 实例数据:int (4 bytes)
     *
     * ● 对齐填充:0 bytes
     * 因为'对象头' + '对齐填充' 已经满足为8的倍数,因此无需填充
     *
     * 对象占用内存大小:对象头(12) + 实例数据(4) + 对齐填充(0) = 16
     */
    int a;

    public static void main(String[] args) throws NoSuchFieldException {

        TheObjectMemory obj = new TheObjectMemory();

        // memoryUsage : 16
        System.out.println("memoryUsage : " + MemoryUtil.memoryUsageOf(obj));

        // a field offset : 12
        System.out.println("a field offset : " + 
              UNSAFE.objectFieldOffset(TheObjectMemory.class.getDeclaredField("a")));

    }
}

实例二

/**
 * ① 将下载的 classmexer.jar 加入当前项目的classpath中
 * ② 启动Main是添加启动项:-javaagent:${classmexer_path}/classmexer.jar
 * -javaagent:/Users/linling/Documents/software/classmexer-0_03/classmexer.jar
 *
 * ③ JVM 参数:
 * -XX:+UseCompressedOops   (默认启用)
 * -XX:+CompactFields   (默认启用)
 * -XX:FieldsAllocationStyle=1      (默认为1)
 */
public class TheObjectMemory {

    private static Unsafe UNSAFE;

    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            UNSAFE = (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * ● 对象头:mark word(8 bytes) + class pointer(4 bytes) = 12 bytes
     * 因为在JDK 8 中"UseCompressedOops"选项是默认启用的,因此class pointer只占用了4个字节。
     *
     * ● 实例数据:long (8 bytes) + long (8 bytes)
     *
     * ● 对齐填充:4 bytes
     *
     * 对象占用内存大小:对象头(12) + 实例数据(16) + 对齐填充(4) = 32
     * 这里请注意,padding的填充不是在最后面的,即,不是在实例数据分配完后填充了4个字
     * 节。而是在对象头分配完后填充了4个字节。这从属性'a'字段的偏移量为16,也能够说明填充的部分是对象头后的4个字节空间。
     *
     * 这是为什么了?
     * 是这样的,在64位系统中,CPU一次读操作可读取64bit(8 bytes)的数据。如果,你在对象头分配后就进行属性 long a字
     * 段的分配,也就是说从偏移量为12的地方分配8个字节,这将导致读取属性long a时需要执行两次读数据操作。因为第一次读取
     * 到的数据中前4字节是对象头的内存,后4字节是属性long a的高4位(Java 是大端模式),低4位的数据则需要通过第二次读取
     * 操作获得。
     */
    long a;
    long b;
    public static void main(String[] args) throws NoSuchFieldException {

        TheObjectMemory obj = new TheObjectMemory();

        // memoryUsage : 32
        System.out.println("memoryUsage : " + MemoryUtil.memoryUsageOf(obj));

        // a field offset : 16
        System.out.println("a field offset : " + 
              UNSAFE.objectFieldOffset(TheObjectMemory.class.getDeclaredField("a")));

        // b field offset : 24
        System.out.println("b field offset : " + 
              UNSAFE.objectFieldOffset(TheObjectMemory.class.getDeclaredField("b")));

    }

}

实例三

/**
 * ① 将下载的 classmexer.jar 加入当前项目的classpath中
 * ② 启动Main是添加启动项:-javaagent:${classmexer_path}/classmexer.jar
 * -javaagent:/Users/linling/Documents/software/classmexer-0_03/classmexer.jar
 *
 * ③ JVM 参数:
 * -XX:+UseCompressedOops   (默认启用)
 * -XX:+CompactFields   (默认启用)
 * -XX:FieldsAllocationStyle=1      (默认为1)
 */
public class TheObjectMemory {

    private static Unsafe UNSAFE;

    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            UNSAFE = (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * ● 对象头:mark word(8 bytes) + class pointer(4 bytes) = 12 bytes
     * 因为在JDK 8 中"UseCompressedOops"选项是默认启用的,因此class pointer只占用了4个字节。
     *
     * ● 实例数据:long (8 bytes) + int (4 bytes)
     *
     * ● 对齐填充:0 bytes
     *
     * 对象占用内存大小:对象头(12) + 实例数据(12) + 对齐填充(0) = 24
     *
     * 在前面的理论中,我们说过基本变量类型在内存中的存放顺序是从大到小的(顺序:longs/doubles、ints、
     * shorts/chars、bytes/booleans)。所以,按理来说,属性int b应该被分配到了属性long a的后面。但是,从属性位置
     * 偏移量的结果来看,我们却发现属性int b被分配到了属性long a的前面,这是为什么了?
     * 是这样的,因为JVM启用了'CompactFields'选项,该选项运行分配的非静态(non-static)字段被插入到前面字段的空隙
     * 中,以提供内存的利用率。
     * 从前面的实例中,我们已经知道,对象头占用了12个字节,并且再次之后分配的long类型字段不会紧跟在对象头后面分配,而是
     * 在新一个8字节偏移量位置处开始分配,因此对象头和属性long a直接存在了4字节的空隙,而这个4字节空隙的大小符合(即,
     * 大小足以用于)属性int b的内存分配。所以,属性int b就被插入到了对象头与属性long a之间了。
     */
    long a;
    int b;
    public static void main(String[] args) throws NoSuchFieldException {

        TheObjectMemory obj = new TheObjectMemory();

        // memoryUsage : 24
        System.out.println("memoryUsage : " + MemoryUtil.memoryUsageOf(obj));

        // a field offset : 16
        System.out.println("a field offset : " + 
              UNSAFE.objectFieldOffset(TheObjectMemory.class.getDeclaredField("a")));

        // b field offset : 12
        System.out.println("b field offset : " + 
              UNSAFE.objectFieldOffset(TheObjectMemory.class.getDeclaredField("b")));
    }

}

实例四

/**
 * ① 将下载的 classmexer.jar 加入当前项目的classpath中
 * ② 启动Main是添加启动项:-javaagent:${classmexer_path}/classmexer.jar
 * -javaagent:/Users/linling/Documents/software/classmexer-0_03/classmexer.jar
 *
 * ③ JVM 参数:
 * -XX:+UseCompressedOops   (默认启用)
 * -XX:+CompactFields   (默认启用)
 * -XX:FieldsAllocationStyle=1      (默认为1)
 */
public class TheObjectMemory {

    private static Unsafe UNSAFE;

    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            UNSAFE = (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * ● 对象头:mark word(8 bytes) + class pointer(4 bytes) = 12 bytes
     * 因为在JDK 8 中"UseCompressedOops"选项是默认启用的,因此class pointer只占用了4个字节。
     *
     * ● 实例数据:long (8 bytes) + int (4 bytes) + oops (4 bytes)
     *
     * ● 对齐填充:4 bytes
     *
     * 对象占用内存大小:对象头(12) + 实例数据(16) + 对齐填充(4) = 32
     *
     * 从属性 int a、long b,以及对象引用 str 的偏移量可以发现,对象引用是在基本变量分配完后才进行的分配的。这是通过
     * JVM选项'FieldsAllocationStyle=1'决定的,FieldsAllocationStyle的值为1,说明:先放入基本变量类型(顺序:
     * longs/doubles、ints、shorts/chars、bytes/booleans),然后放入oops(普通对象引用指针)
     *
     */
    int a;
    long b;
    String str;
    public static void main(String[] args) throws NoSuchFieldException {

        TheObjectMemory obj = new TheObjectMemory();

        // memoryUsage : 24
        System.out.println("memoryUsage : " + MemoryUtil.memoryUsageOf(obj));

        // a field offset : 12
        System.out.println("a field offset : " + 
              UNSAFE.objectFieldOffset(TheObjectMemory.class.getDeclaredField("a")));

        // str field offset : 16
        System.out.println("b field offset : " + 
              UNSAFE.objectFieldOffset(TheObjectMemory.class.getDeclaredField("b")));

        // str field offset : 24
        System.out.println("str field offset : " + 
              UNSAFE.objectFieldOffset(TheObjectMemory.class.getDeclaredField("str")));
    }

}

实例五

/**
 * ① 将下载的 classmexer.jar 加入当前项目的classpath中
 * ② 启动Main是添加启动项:-javaagent:${classmexer_path}/classmexer.jar
 * -javaagent:/Users/linling/Documents/software/classmexer-0_03/classmexer.jar
 *
 * ③ JVM 参数:
 * -XX:+UseCompressedOops   (默认启用)
 * -XX:+CompactFields   (默认启用)
 * -XX:FieldsAllocationStyle=1      (默认为1)
 */
public class TheObjectMemory {

    private static Unsafe UNSAFE;

    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            UNSAFE = (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

     /**
     * memoryUsageOf方法仅计算了对象本身的大小,并未包含引用对象的内存大小(注意,memoryUsageOf方法计算的是引用指针
     * 的对象,而非引用对象占用的内存大小)。
     * deepMemoryUsageOf方法则会将引用对象占用的内存大小也计算进来。
     *
     * 注意,deepMemoryUsageOf(Object obj)默认只会包含non-public的引用对象的大
     * 小。如果你想将public引用对象的大小也计算在内,可通过deepMemoryUsageOf重载方法
     * deepMemoryUsageOf(Object obj, VisibilityFilter referenceFilter),VisibilityFilter参数传入
     * 'VisibilityFilter.ALL'来实现。
     */
    static class TheInnerObject {
        int innerA;
    }
    TheInnerObject innerObject = new TheInnerObject();
    public static void main(String[] args) throws NoSuchFieldException {

        TheObjectMemory obj = new TheObjectMemory();

        // TheObjectMemory memoryUsage : 16
        System.out.println("TheObjectMemory memoryUsage : " + MemoryUtil.memoryUsageOf(obj));

        // TheInnerObject memoryUsage : 16
        TheInnerObject innerObj = new TheInnerObject();
        System.out.println("TheInnerObject memoryUsage : " + MemoryUtil.memoryUsageOf(innerObj));

        // TheObjectMemory deepMemoryUsageOf : 32
        System.out.println("TheObjectMemory deepMemoryUsageOf : " + 
              MemoryUtil.deepMemoryUsageOf(obj));

    }

}

实例六

/**
 * ① 将下载的 classmexer.jar 加入当前项目的classpath中
 * ② 启动Main是添加启动项:-javaagent:${classmexer_path}/classmexer.jar
 * -javaagent:/Users/linling/Documents/software/classmexer-0_03/classmexer.jar
 *
 * ③ JVM 参数:
 * -XX:+UseCompressedOops   (默认启用)
 * -XX:+CompactFields   (默认启用)
 * -XX:FieldsAllocationStyle=1      (默认为1)
 */
public class TheObjectMemory {

    private static Unsafe UNSAFE;

    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            UNSAFE = (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

     /**
     * 数组对象自身占用的内存大小 = 对象头 + 数组长度 * 元素引用指针/基本数据类型大小 + 对齐填充
     *
     * ● 对象头:mark word(8 bytes) + class pointer(4 bytes) + length(4 bytes) = 16 bytes
     * 因为在JDK 8 中"UseCompressedOops"选项是默认启用的,因此class pointer只占用了4个字节。
     *
     * ● 实例数据:数组长度(1) * 对象引用指针(4 bytes) = 4 bytes
     *
     * ● 对齐填充:4 bytes
     *
     * 对象占用内存大小:对象头(16) + 实例数据(4) + 对齐填充(4) = 24
     *
     * deepMemoryUsageOf = array memoryUsage + array_length(数组长度) * item_deepMemoryUsage (元素占用
     * 的全部内存)
     *
     * 注意,这里的数组是一个对象数组,因此memoryUsage中计算的是对象引用指针的大小。如果是一个基本数据类型的数组,如,
     * int[],则,memoryUsage计算的就是基本数据类型的大小了。也就是说,如果是基本数据类型的数组的话,memoryUsage
     * 的值是等于deepMemoryUsageOf的值的。
     *
     */
    int a;
    String str = "hello";
    public static void main(String[] args) throws NoSuchFieldException {
        TheObjectMemory[] objArray = new TheObjectMemory[1];
        TheObjectMemory obj = new TheObjectMemory();
        objArray[0] = obj;

        // memoryUsage : 24
        System.out.println("objArray memoryUsage : " + MemoryUtil.memoryUsageOf(objArray));

        // deepMemoryUsageOf : 104
        System.out.println("objArray deepMemoryUsageOf : " + MemoryUtil.deepMemoryUsageOf(objArray));

        // obj memoryUsage : 24
        System.out.println("obj memoryUsage : " + MemoryUtil.memoryUsageOf(obj));
        // obj deepMemoryUsageOf : 80
        System.out.println("obj deepMemoryUsageOf : " + MemoryUtil.deepMemoryUsageOf(obj));

        // first item offset(数组第一个元素的内存地址偏移量) : 16
        System.out.println("first item offset : " + UNSAFE.arrayBaseOffset(objArray.getClass()));
    }

}


后记

如果文章有错不吝指教 :)

参考

《深入理解Java虚拟机》
classmexer
object_memory_usage
jvm-options

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容