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

作者: tomas家的小拨浪鼓 | 来源:发表于2017-12-17 17:15 被阅读101次

对象内存结构

在 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

相关文章

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

    对象内存结构 在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为3块区域:① 对象头(Header)② ...

  • Synchronized原理

    1、认识JAVA对象的组成结构 对象的内存结构:在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐...

  • JVM-对象内存布局

    jvm-对象内存布局 对象内存结构概述 对象的创建过程: jvm将对象所在的class文件加载到方法区中 jvm读...

  • Java对象内存布局与加锁过程

    一、在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据、对其数据。如下图所示 实例数据:存放类的属性数据...

  • JAVA对象在JVM中内存分配

    如果你还不了解JVM内存模型的建议您先看下JVM内存模型 以一下代码为例,来分析下,java的实例对象在内存中的空...

  • 对象的内存布局

    对象的内存布局 在Hotspot虚拟机中,对象在内存中的布局可以分为三块区域: 对象头(Header)、实例数据(...

  • JAVA运行时—内存分配情况

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

  • Java 内存模型

    内存结构 内存结构简介 JVM的内存结构大概分为: 堆(heap):线程共享,所有的对象实例以及数组都要在堆上分配...

  • 对象的内存布局(JOL)和锁

    对象的内存布局 在 HotSpot 虚拟机中,对象在内存中的布局主要分为三部分:对象头(Header)、实例数据(...

  • Java对象的内存布局以及访问方式

    对象的内存布局 在HotSpot虚拟机中,对象的内存中的布局可以分为3块区域:对象头,实例数据和对齐填充。 对象头...

网友评论

    本文标题:JVM中 对象的内存布局 以及 实例分析

    本文链接:https://www.haomeiwen.com/subject/nzaywxtx.html