zoukankan      html  css  js  c++  java
  • Java对象内存模型

    2 Java对象内存模型

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

    image

    在 JVM 中,Java对象保存在堆中时,由以下三部分组成:

    • 对象头(object header):包括了关于堆对象的布局、类型、GC状态、同步状态和标识哈希码的基本信息。Java对象和vm内部对象都有一个共同的对象头格式。
    • 实例数据(Instance Data):主要是存放类的数据信息,父类的信息,对象字段属性信息。
    • 对齐填充(Padding):为了字节对齐,填充的数据,不是必须的。凑齐8字节的倍数。

    2.1 对象头

    对象头包括两部分信息,第一部分用于存储对象自身的运行时数据(MarkWord), 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另外一部分是类型指针(Klass Pointer),即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

    // oopDesc hotspot对象头
    volatile markOop  _mark;
    union _metadata {
        wideKlassOop    _klass;
        narrowOop       _compressed_klass;
    } _metadata;
    

    Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。

    image

    image

    markword组成内容基本一致。

    • 锁标志位(lock):区分锁状态,11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。
    • biased_lock:是否偏向锁,由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。
    • 分代年龄(age):表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。
    • 对象的hashcode(hash):运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。当对象加锁后,计算的结果31位不够表示,在偏向锁,轻量锁,重量锁,hashcode会被转移到Monitor中
    • 偏向锁的线程ID(JavaThread):偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。
    • epoch:偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
    • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争的时,JVM使用原子操作而不是OS互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的markword中设置指向锁记录的指针。
    • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针。

    2.2 对象信息

    使用openjdk的jol工具打印对象信息。引入依赖

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

    1 无属性对象

    // 打印无属性对象
    System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
    // 对象信息
    java.lang.Object 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)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    

    对象信息分析

    image

    • OFFSET:偏移地址,单位字节;
    • SIZE:占用的内存大小,单位为字节;
    • TYPE DESCRIPTION:类型描述,其中object header为对象头;
    • VALUE:对应内存中当前存储的值,二进制32位;

    对于普通无属性对象,一共占用16字节,其中对象头markword占用8个字节,类型指针占用4个字节,剩余4个字节用于对齐填充,没有实例数据。

    JDK8版本默认开启指针压缩(-XX:+UseCompressedOops),如果关闭指针压缩,类型指针占用8个字节,不再需要对齐填充。

    2 数组对象

    // 打印数组
    System.out.println(ClassLayout.parseInstance(new int[]{}).toPrintable());
    // 对象信息
    [I 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)                           6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
         12     4        (object header)   // 数组长度              00 00 00 00 (00000000 00000000 00000000 00000000) (0)
         16     0    int [I.<elements>                             N/A
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
    

    对于数组对象,类型指针后有4个字节记录数组长度。因为虚拟机可以通过普通Java对象的元数据信息确定Java对象大小,如果数组长度不确定,则无法推断出数组对象大小。

    3 有属性对象

    // 打印有属性对象
    System.out.println(ClassLayout.parseInstance(new User()).toPrintable());
    class User {
        int id;			// 4B
        String name;	// 4B 未经类型压缩为8B
        byte b;			// 1B
        Object o;		// 4B 未经类型压缩为8B
    }
    // 对象信息
    com.lzp.java.jvm.memory.User object internals:
     OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
    // 对象头 12字节
         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)                           65 cc 00 f8 (01100101 11001100 00000000 11111000) (-134165403)
    // 实例数据
         12     4                int User.id                                   0
         16     1               byte User.b                                    0
         17     3                    (alignment/padding gap)   // 对齐               
         20     4   java.lang.String User.name                                 null
         24     4   java.lang.Object User.o                                    null
    // 对齐填充
         28     4                    (loss due to the next object alignment)
    Instance size: 32 bytes
    Space losses: 3 bytes internal + 4 bytes external = 7 bytes total
    

    通过这一节的分析,我们可以比较轻松地估计出一个对象的大小,即对象头+实例数据+对象填充,得到一个8字节倍数的值。

    2.3 其他

    1 JVM每次GC,对象分代年龄加1,当年龄增加到15时晋升到老年代。为什么是15?

    在Mark Word中可以发现标记对象分代年龄的分配的空间是4bit,而4bit能表示的最大数就是2^4-1 = 15。

    2 为什么要进行指针压缩?

    通过指针压缩,类型指针、对象引用等由8字节转为4个字节。降低对象占用的内存大小,顺便减轻GC压力;当指针移动时,减少带宽损耗。

    版权声明:本文为博主原创文章,未经博主允许不得转载。
  • 相关阅读:
    2019-9-2-一个好的程序员
    2019-9-2-一个好的程序员
    2018-2-13-wpf-GifBitmapDecoder-解析-gif-格式
    2018-2-13-wpf-GifBitmapDecoder-解析-gif-格式
    2019-5-31-SharpDx-进入全屏模式
    2019-5-31-SharpDx-进入全屏模式
    2019-8-31-dotnet-删除只读文件
    2019-8-31-dotnet-删除只读文件
    PHP mysqli_real_connect() 函数
    PHP mysqli_query() 函数
  • 原文地址:https://www.cnblogs.com/dtyy/p/15811755.html
Copyright © 2011-2022 走看看