JVM-object header to understand?

In the second chapter of the third edition of "In-depth Understanding of the Java Virtual Machine", there is knowledge about the memory layout of objects. Today we will talk about the object headers in it, and we will create objects and the objects in the object headers. Information changes do a hands-on procedural exercise.

Object's memory layout

First of all, we need to know what the layout of the object looks like in memory? The memory layout of an object can be divided into three parts:

  1. Object header: It is the focus of today's talk. The object header is divided into two parts, markWord (translated as a mark word, but generally used in English markWord) and classPoint (type pointer). The type pointer points to the metadata of the object type, and the java virtual machine uses this pointer to determine what kind of instance the object is. If the object is a java array, there is also a piece of data in the object header used to mark the length of the array. markWord stores the runtime data of the object itself, such as hashCode, generational age, lock status and other information, which will be discussed in detail later.
  2. Instance data instance Data: There is nothing to say about this, it is the information that the object actually stores.
  3. Alignment padding does not necessarily exist, and may not exist. Hotspot stipulates that the starting address of the object must be an integer multiple of 8 bytes, that is to say, the size of the object after adding padding must be an integer multiple of 8 bytes. That is to say, if the sum of header and instanceData is not an integer multiple of 8, then padding will be used to complete the size of the object, making the object a multiple of 8 bytes.

MarkWord

After understanding the memory layout of the object, we start to talk about the information stored in markWord. The length of markWord in 32-bit and 64-bit systems is 32 and 64. If the compressed pointer is enabled in the 64-bit system, it is also 32 bits.

compressed pointer

View jvm default parameters: 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)
复制代码

可以看到带有压缩关键字的有两个参数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)
复制代码

本来打算整理成一个中文的表格,但是一想其实这个注释版本的已经很清楚了,而且平时也一般不会用到这个,没必要死记, 遇到具体问题的时候把这个表格拿出来一看,对照一下就很明白了。只是要稍微注意下,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   // 偏向锁
复制代码

枚举的值翻译为二进制就是对应的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());
 }

复制代码

可以看到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());
}
复制代码

这里我们从现象可以看到,虽然我们关闭的是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());
}
复制代码

可以看到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());
   }
复制代码

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")
复制代码

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());
  }
复制代码

可以看到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());
    }
}
复制代码

可以看到是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());
      }
  }
复制代码

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

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();
   }
复制代码

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

验证的代码地址

参考资料:

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

Guess you like

Origin juejin.im/post/7166619439591489573