转载

Java对象头的内存布局探究

  本章节主要来验证下是否开启压缩指针对对象头内存布局的影响,因为在其他地方看到的结论不一样,还是得眼见为实。先看下JVM版本以及默认启动参数:

~❯ java -XX:+PrintCommandLineFlags -version
-XX:G1ConcRefinementThreads=4 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC
java version "11.0.1" 2018-10-16 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.1+13-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.1+13-LTS, mixed mode)

-XX:+UseCompressedClassPointers 可见,默认是开启压缩指针的。下面先引入一个依赖,用于查看对象内存布局:

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

开启压缩指针(默认)情况

public class User{
    int i;
}

public class CompressedLayout{
    public static void main(String[] args){
        User o = new User();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           48 02 06 00 (01001000 00000010 00000110 00000000) (393800)
     12     4    int User.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

  由结果可以看出,对象一共占用16个字节,头部占12个字节。组成结构如下:

  • Header:8字节Mark World + 4字节Class类型指针
  • 实例数据:4字节的int类型成员变量

未开启压缩指针情况

/**
 * vm args: -XX:-UseCompressedClassPointers
 *
 */
public class UnCompressedLayout{
    public static void main(String[] args){
        User o = new User();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

cn.didadu.sample.jvm.objLayout.User object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           28 99 20 26 (00101000 10011001 00100000 00100110) (639670568)
     12     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
     16     4    int User.i                                    0
     20     4        (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

  由结果可以看出,对象一共占用24个字节,头部占16字节。组成结构如下:

  • Header:8字节Mark World + 8字节Class类型指针
  • 实例数据:4字节的int类型成员变量
  • 对其填充:4字节,由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。所以对象内存布局中有了对其填充这一项。

由上面两个结果可以看出是否开启压缩指针,直接影响的是对象头部的大小,更具体的说是指向Class的类型指针那一部分。开启压缩时占4字节,未开启压缩时占8字节(64位的虚拟机)。

Mark World布局

  顺便来看下Header的前8个字节Mark World区域,这8个字节存储了对象哈希码、对象分代年龄、锁状态等。根据最后一个字节的标志位,Mark World一共有5种表现形式,下面以64位的形式来看下这5种的布局情况:

  • 正常对象,无锁,这是新建对象的Mark World

    | 25位未使用 | 31位HashCode(调用对象的hashCode后写入) | 1位未使用 | 4位age(所以age最大值为15) | 1位偏向锁标记(此处为0,表示不是偏向锁) | 2位锁状态(此处为01,结合前面1位表示未锁定) |
    
  • 偏向锁对象的Mark World

    | 54位线程ID | 2位epoch(偏向的时间戳) | 1位未使用 | 4位age(所以age最大值为15) | 1位偏向锁标记(此处为1,表示是偏向锁) | 2位锁状态(此处为01,结合前面1位表示可偏向) |
    
  • 轻量锁的Mark World

    | 62位指针,指向线程栈帧中的锁记录 | 2位锁标识(此处为00,表示轻量级锁定) |
    
  • 重量级锁的Mark World

    | 62位指针,指向关联的监视器对象 | 2位锁标识(此处为10,表示膨胀为重量级锁) |
    
  • 被GC标记过的Mark World

    | 62位空 | 2位状态标识(此处为11,表示被GC标记) |
    

本章节简单地验证了下对象头的内存布局,就到这儿了。

原文  http://bboyjing.github.io/2020/04/24/Java对象头的内存布局探究/
正文到此结束
Loading...