¿Encabezado de objeto JVM para entender?

En el segundo capítulo de la tercera edición de "Comprensión profunda de la máquina virtual de Java", hay conocimiento sobre el diseño de la memoria de los objetos. Hoy hablaremos sobre los encabezados de los objetos en él, y crearemos objetos y los objetos. en los encabezados de los objetos Los cambios de información hacen un ejercicio práctico de procedimiento.

Disposición de la memoria del objeto

En primer lugar, necesitamos saber cómo se ve el diseño del objeto en la memoria. El diseño de la memoria de un objeto se puede dividir en tres partes:

  1. Encabezado de objeto: Es el foco de la charla de hoy.El encabezado de objeto se divide en dos partes, markWord (traducido como palabra de marca, pero generalmente se usa en inglés markWord) y classPoint (puntero de tipo). El puntero de tipo apunta a los metadatos del tipo de objeto y la máquina virtual Java utiliza este puntero para determinar qué tipo de instancia es el objeto. Si el objeto es una matriz Java, también hay un dato en el encabezado del objeto que se usa para marcar la longitud de la matriz. markWord almacena los datos de tiempo de ejecución del propio objeto, como hashCode, edad generacional, estado de bloqueo y otra información, que se analizará en detalle más adelante.
  2. Datos de instancia Datos de instancia: No hay nada que decir al respecto, es la información que realmente almacena el objeto.
  3. El relleno de alineación no existe necesariamente y puede no existir. Hotspot estipula que la dirección inicial del objeto debe ser un múltiplo entero de 8 bytes, es decir, el tamaño del objeto después de agregar relleno debe ser un múltiplo entero de 8 bytes. Es decir, si la suma del encabezado y los datos de instancia no es un múltiplo entero de 8, entonces se usará el relleno para completar el tamaño del objeto, haciendo que el objeto sea un múltiplo de 8 bytes.

marcaPalabra

Después de comprender el diseño de la memoria del objeto, comenzamos a hablar sobre la información almacenada en markWord. La longitud de markWord en los sistemas de 32 y 64 bits es de 32 y 64. Si el puntero comprimido está habilitado en el sistema de 64 bits , también es de 32 bits.

puntero comprimido

Ver los parámetros predeterminados de 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)
复制代码

可以看到带有压缩关键字的有两个参数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.大小端参考资料

Supongo que te gusta

Origin juejin.im/post/7166619439591489573
Recomendado
Clasificación