• JVM-对象头了解一下?


    在《深入Java虚拟机》第三版的第二章里面有讲到关于对象的内存布局的知识,今天我们就来聊一聊这里面的对象头,并且会对
    创建对象,对象头里面的信息变化做一个实践的程序练习。

    对象的内存布局

    首先我们要知道对象在内存的布局是什么样子?
    对象的内存布局可以分为三块:

    1. 对象头 header:
      是今天要讲的重点,对象头又分为两部分,markWord(翻译为标记字,但一般还是用英文markWord在称呼使用)和classPoint(类型指针)。
      类型指针,指向的是这个对象类型的元数据,java虚拟机用这个指针来确定该对象是什么类的实例。如果对象是java数组,那么对象头中还有一块
      用于标记数组长度的的数据。markWord里面储存了对象自身的运行时数据,比如hashCode,分代年龄,锁状态等信息,等会后面会详细讲。
    2. 实例数据 instance Data:
      这个没什么好说的,就是对象真正存储的信息。
    3. 对齐填充 padding
      不是必然存在的,可能会没有。Hotspot里面规定对象的起始地址必须是8字节的整数倍,
      也就是说加上padding后的对象大小必须是8字节的整数倍。也就是说如果header和instanceData加起来还不是8的整数倍,
      那么padding就会用来补全对象的大小,让对象凑成8字节的倍数。

    MarkWord

    了解了对象的内存布局之后,我们开始讲markWord储存的信息,markWord在32位和64位系统中的长度是32和64,如果64位系统开启了压缩
    指针的话,那也是32位。

    压缩指针

    查看jvm默认参数:java -XX:+PrintCommandLineFlags -version

    -XX:InitialHeapSize=132313536
    -XX:MaxHeapSize=2117016576
    -XX:+PrintCommandLineFlags
    -XX:+UseCompressedClassPointers
    -XX:+UseCompressedOops
    -XX:-UseLargePagesIndividualAllocation
    -XX:+UseParallelGC
    java version "1.8.0_162"
    Java(TM) SE Runtime Environment (build 1.8.0_162-b12)
    Java HotSpot(TM) 64-Bit Server VM (build 25.162-b12, mixed mode)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    可以看到带有压缩关键字的有两个参数UseCompressedClassPointers,UseCompressedOops。而我们说的压缩指针指的是
    oop(ordinary object pointer)普通对象指针。另外一个也是对象头里面的指针,就是之前说过的classPoint类型指针,
    这两块内容都是可以压缩的,也都是默认开启的,因为指针越长寻址也就越慢,性能会有损耗,所以默认开启压缩指针。
    压缩指针参数:启用指针压缩:-XX:+UseCompressedOops(默认开启),禁止指针压缩:-XX:-UseCompressedOops

    MarkWord信息

    openJdk的源码里面是这么注释的:

    //  32 bits:
    //  --------
    //  hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
    //  JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
    //  size:32 ------------------------------------------>| (CMS free block)
    //  PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
    //
    //  64 bits:
    //  --------
    //  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
    //  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
    //  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
    //  size:64 ----------------------------------------------------->| (CMS free block)
    //
    //  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
    //  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
    //  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
    //  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    本来打算整理成一个中文的表格,但是一想其实这个注释版本的已经很清楚了,而且平时也一般不会用到这个,没必要死记,
    遇到具体问题的时候把这个表格拿出来一看,对照一下就很明白了。只是要稍微注意下,64位的有两部分,上面的是未开启压缩指针的,
    下面的是开启了压缩指针的,一看到注释里面这个COOPS,联想到上面讲的压缩指针就很容易理解了吧。

    我们主要把这个锁的状态拿出来讲一下,等会会举几个例子,使用JOL(java object layout)打印下对象头的信息,这里要注意一点就是表格里面虽然是从左往右写的,
    但是实际上我们JOL打印出来是反的哦。因为java是BigEndian的,低字节保存到高位中,所以JOL打印出来的内容,我们看最前面的8位就好了,
    可以看上面的注释表格,都是8位中的后3位保存的锁的状态信息。(平时我们写java代码的时候也不关心这个问题,因为java已经帮我们处理了,屏蔽了
    这个底层的细节)。

    参考源码中的解释(源码地址 src/share/vm/oops/markOop.hpp):

    //    [JavaThread* | epoch | age | 1 | 01]       lock is biased toward given thread
    //    [0           | epoch | age | 1 | 01]       lock is anonymously biased
    //
    //  - the two lock bits are used to describe three states: locked/unlocked and monitor.
    //
    //    [ptr             | 00]  locked             ptr points to real header on stack
    //    [header      | 0 | 01]  unlocked           regular object header
    //    [ptr             | 10]  monitor            inflated lock (header is wapped out)
    //    [ptr             | 11]  marked             used by markSweep to mark an object
    //                                               not valid at any other time
    
    enum { locked_value             = 0,  // 轻量级锁
           unlocked_value           = 1,  // 无锁,普通对象
           monitor_value            = 2,  // 重量级锁
           marked_value             = 3,  // GC标记
           biased_lock_pattern      = 5   // 偏向锁
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    枚举的值翻译为二进制就是对应的000,001,010,011,101,我们结合源码的注释和这个枚举可以很容易的知道有5种状态(五个枚举),我把
    中文注释也加上去了,然后可以表示6种情况,看注释可以知道是偏向锁的时候,有JavaThread指针的偏向,和无指针的偏向(又叫匿名偏向)。

    验证对象头信息

    验证对象头的信息我们要用到JOL包(上面也提到过,它是openjdk提供的分析工具),接下来我们对照这5中情况一个个来验证,但是GC标记这个没法
    测试(至少我没想到办法也没找到类似的资料),下面的测试我都加了一个 -XX:+PrintCommandLineFlags,是为了方便大家看到目前有哪些jvm参数。

    1.普通对象
    /**
      * -XX:InitialHeapSize=132313536 -XX:MaxHeapSize=2117016576 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
      * 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 20 (11100101 00000001 00000000 00100000) (536871397)
      *      12     4        (loss due to the next object alignment)
      * Instance size: 16 bytes
      * Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
      */
     @Test
     public  void test1() {
         Object object = new Object();
         System.out.println(ClassLayout.parseInstance(object).toPrintable());
     }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    可以看到object header占了12个字节,前两个是markword,后一个是classPoint,因为classPoint默认是压缩的,所以它只有4字节。
    然后object header加上classPoint是12个字节对吧,不是8的整数倍,所以padding就出来了自动补全加了4字节,因此整个Object对象是
    占用了16字节。

    2.不使用oops压缩的普通对象

    /**
     * 不使用压缩oops vm参数:-XX:-UseCompressedOops
     *
     * -XX:InitialHeapSize=132313536 -XX:MaxHeapSize=2117016576 -XX:+PrintCommandLineFlags -XX:-UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
     * 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)                           00 1c 54 17 (00000000 00011100 01010100 00010111) (391388160)
     *      12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     * Instance size: 16 bytes
     * Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
     */
    @Test
    public  void test2() {
        Object object = new Object();
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    这里我们从现象可以看到,虽然我们关闭的是oops的指针压缩,但是却发现类型指针的压缩竟然也是被关闭了,16个字节全部都是object header。
    这里推断是这两个参数基本上是联动的,修改其中一个,另一个也会跟着开启或者关闭之类的,后续等我看了hotspot源码后再来验证这个问题,
    今天我们先继续往下看,不耽误其他例子的验证

    3. 不使用类型指针压缩

    /**
     * 不使用类型指针压缩 -XX:-UseCompressedClassPointers
     *
     * 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)                           00 1c 5f 17 (00000000 00011100 01011111 00010111) (392109056)
     *      12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     * Instance size: 16 bytes
     * Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
     */
    @Test
    public  void test3() {
        Object object = new Object();
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    可以看到test2,test3在类型指针压缩和对象指针分别开启关闭的情况下,都会是不生效的,都会是16字节,必须两个都是开启状态,objectHeader才会是占用12字节,8字节的markWord,4字节的classPointer。
    而且上面的三个测试都是markword结尾都是001,无锁状态,符合我们的预期。

    4. 偏向锁状态

    /**
        * 偏向锁状态测试 -XX:BiasedLockingStartupDelay=0
        *
        * java.lang.Object 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)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
        *      12     4        (loss due to the next object alignment)
        * Instance size: 16 bytes
        * Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
        */
       @Test
       public  void test4() {
           Object object = new Object();
           System.out.println(ClassLayout.parseInstance(object).toPrintable());
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    jdk8默认是jvm启动4秒后开启偏向锁,所以我们直接关闭偏向锁的延迟就可以看到markWord变化了,101-偏向锁,符合上面的介绍,另外我们也在测试一下默认
    偏向锁延迟真的是4秒吗,因为我们test1已经是001-无锁,所以我们先让程序sleep大于4秒,在打印markWord的状态看看,虽然不是很严谨的证明延迟是4秒,但是也
    大致验证了这个偏向锁延迟生效确实是存在的。要继续探究的话可以看openJdk源码
    openJdk源码中biasedLocking.cpp和globals.hpp结合起来看,确实延迟是4秒。

    http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/runtime/biasedLocking.cpp#l89
    
    void BiasedLocking::init() {
      // If biased locking is enabled, schedule a task to fire a few
      // seconds into the run which turns on biased locking for all
      // currently loaded classes as well as future ones. This is a
      // workaround for startup time regressions due to a large number of
      // safepoints being taken during VM startup for bias revocation.
      // Ideally we would have a lower cost for individual bias revocation
      // and not need a mechanism like this.
      if (UseBiasedLocking) {
        if (BiasedLockingStartupDelay > 0) {
          EnableBiasedLockingTask* task = new EnableBiasedLockingTask(BiasedLockingStartupDelay);
          task->enroll();
        } else {
          VM_EnableBiasedLocking op(false);
          VMThread::execute(&op);
        }
      }
    }
    
    https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/runtime/globals.hpp
    
    product(intx, BiasedLockingStartupDelay, 4000,
          "Number of milliseconds to wait before enabling biased locking")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    5.验证sleep4100毫秒后的偏向状态

    /**
       * 偏向锁状态测试  验证sleep 4100ms后markword的状态
       * java.lang.Object 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)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
       *      12     4        (loss due to the next object alignment)
       * Instance size: 16 bytes
       * Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
       */
      @Test
      public  void test5() throws InterruptedException {
          Thread.sleep(4100);
          Object object = new Object();
          System.out.println(ClassLayout.parseInstance(object).toPrintable());
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    可以看到sleep4秒多后,确实也是101-偏向锁的状态

    6.验证轻量级锁的状态

    /**
     * 轻量级锁测试:默认开启 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops
     *   java.lang.Object object internals:
     *   OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
     *   0     4        (object header)                           18 df 38 03 (00011000 11011111 00111000 00000011) (54058776)
     *   4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     *   8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     *   12     4        (loss due to the next object alignment)
     *   Instance size: 16 bytes
     *   Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
     * @throws InterruptedException
     */
    @Test
    public  void test6() throws InterruptedException {
        Object object = new Object();
        synchronized (object) {
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    可以看到是00011000,以0结尾的是轻量级锁

    7.偏向锁和轻量级锁一起的情况

    /**
       * 轻量级锁测试:默认开启 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops
       * 并且去掉偏向锁的延迟: -XX:BiasedLockingStartupDelay=0
       *  java.lang.Object object internals:
       *  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
       *  0     4        (object header)                           08 e0 e2 02 (00001000 11100000 11100010 00000010) (48422920)
       *  4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
       *  8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
       *  12     4        (loss due to the next object alignment)
       *  Instance size: 16 bytes
       *  Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
       * @throws InterruptedException
       */
      @Test
      public  void test7() throws InterruptedException {
          Object object = new Object();
          synchronized (object) {
              System.out.println(ClassLayout.parseInstance(object).toPrintable());
          }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    偏向锁开启的情况下,如果涉及到同步锁,就会变成轻量级锁。

    8.重量级锁验证

    /**
        * 重量级锁测试:-XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:BiasedLockingStartupDelay=0
        * 当前线程:线程2java.lang.Object object internals:
        * OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
        * 0     4        (object header)                           8a fe 4c 1a (10001010 11111110 01001100 00011010) (441253514)
        * 4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
        * 8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
        * 12     4        (loss due to the next object alignment)
        * Instance size: 16 bytes
        * Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
        * 当前线程:线程1java.lang.Object object internals:
        * OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
        * 0     4        (object header)                           8a fe 4c 1a (10001010 11111110 01001100 00011010) (441253514)
        * 4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
        * 8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
        * 12     4        (loss due to the next object alignment)
        * Instance size: 16 bytes
        * Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
        *
        * @throws InterruptedException
        */
       @Test
       public void test8() throws InterruptedException {
           Object object = new Object();
           Thread t1 = new Thread(() -> {
               synchronized (object) {
                   System.out.println("当前线程:" + Thread.currentThread().getName() + ClassLayout.parseInstance(object).toPrintable());
               }
           }, "线程1"
           );
    
           Thread t2 = new Thread(() -> {
               synchronized (object) {
                   System.out.println("当前线程:" + Thread.currentThread().getName() + ClassLayout.parseInstance(object).toPrintable());
               }
           }, "线程2"
           );
    
    
           t1.start();
           t2.start();
    
           t1.join();
           t2.join();
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    我们发现当有两个线程开始争抢资源的时候,test7里面的轻量级锁升级成了重量级锁,显示的是10001010,尾数是10,重量级锁(monitor)。
    到此为止,对象头上的状态除了GC没想到办法监测之外,基本上都验证过了,也见识了偏向锁->轻量级锁->重量级锁的升级过程,完结撒花~
    验证的代码地址

    参考资料:
    1.压缩指针
    2.大小端参考资料

  • 相关阅读:
    学习“基于深度学习的故障诊断”开源
    vue3路由跳转params传参接收不到
    03 基础配置webpack3打包vue文件
    怎样恢复误删和损坏磁盘上的文件
    老梗新玩「GitHub 热点速览 v.22.34」
    更换Mac硬盘后如何将数据恢复到新驱动器?
    Swagger概述
    从中间表取数更新TW付款单数据(不含表体)
    一次明白 JDBC,ORM,JPA,SpringDataJPA 之间的关系
    Linux 安装5.7版本MySQL
  • 原文地址:https://blog.csdn.net/sc9018181134/article/details/127894866