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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏韩伟的专栏

框架设计原则和规范(完)

祝大家圣诞节快乐!有事没事别出门,外面太!挤!了! 此文是《.NET:框架设计原则、规范》的读书笔记,本文内容较多,共分九章,今天推送最后一章。 1. 什么是好...

2644
来自专栏lonelydawn的前端猿区

打造专属插件之Easy Slider Bar

引用 <link rel="stylesheet" type="text/css" href="./index.css"> <div id="slider"><...

2825
来自专栏GreenLeaves

C# 引用类型和值类型

1、引用类型 FCL(Framework)中的大多数类型都是引用类型,引用类型总是在托管堆中分配的,C#的new操作符会返回对象的内存地址,也就是指对象数据的内...

1877
来自专栏Java学习123

原 Java中计算程序运行耗时的方法对比

1713
来自专栏武培轩的专栏

瓜子面经汇总

HashMap是常用的Java集合之一,是基于哈希表的Map接口的实现。不支持同步和允许null作为key和value。

1103
来自专栏Android 研究

Android JNI学习(四)——JNI的常用方法的中文API

本文主要是结合JNI的常用接口文档进行的翻译主要是帮助我们更好的理解JNI中常用的API。具体如下:

591
来自专栏java、Spring、技术分享

Netty NioEventLoop源码解读

  NioEventLoop中维护了一个线程,线程启动时会调用NioEventLoop的run方法,执行I/O任务和非I/O任务:I/O任务:即selectio...

883
来自专栏技术专栏

慕课网Flask构建可扩展的RESTful API-4. 理解WTForms并灵活改造她

之前的代码,修改完成之后,已经修复了之前的缺陷,但是这样爆出了两个问题: 1.代码太啰嗦了,每个试图函数里,都需要这么写 2.ClientTypeError...

681
来自专栏一个会写诗的程序员的博客

《Kotin 极简教程》第15章 Kotlin 文件IO操作、正则表达式与多线程第15章 Kotlin 文件IO操作与多线程《Kotlin极简教程》正式上架:

我们在使用 Groovy 的文件 IO 操作的时候,感觉非常便利。同样的Kotlin也有好用的文件 IO 操作的 API。同样的在 Kotlin 中对 Java...

1072
来自专栏木木玲

堆外内存 之 DirectByteBuffer 详解

2097

扫码关注云+社区