Verify the whole process of object header change during synchronized lock upgrade - springboot actual e-commerce project mall4j

Posted by tomhath on Thu, 10 Feb 2022 16:43:33 +0100

springboot e-commerce project mall4j( https://gitee.com/gz-yami/mall4j)

java open source mall system

Verify the whole process of object header change during synchronized lock upgrade

jdk version: 1.8

System: Windows 10 64 bit

jvm startup parameters: - XX:BiasedLockingStartupDelay=0 (cancel delayed load bias lock)

First, we need to know several concepts

  1. Memory structure of java non array objects (ordinary objects)

If it is an array object, it will occupy another length space (4 bytes) to record the length of the array.

  1. markword format of java object (on 64 bit virtual machine)

3.synchronized four lock upgrade process and when to upgrade (not detailed)

Check whether the lock flag bit changes correspondingly in these four lock states through the actual coding

Introducing jol Toolkit

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

Unlocked state

@Test
public void test01() throws Exception {
    Object o = new Object();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

results of enforcement

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 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 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

The object header in the first and second lines is the memory of markword, the object header in the third line is the content of class pointer, and the fourth line is alignment filling.

Just focus on the first and second lines.

Because the first and second lines are printed from low to high, they need to be viewed in reverse to correspond one by one with the lock status table above.

00000000 00000000 00000000.

In comparison with the last three digits of the table, focus on whether the bias lock is 1 and the lock flag is 01. Theoretically, the bias lock flag should be 0, which is caused by adding a startup parameter to cancel the delayed loading bias lock. If the startup parameter is removed, the bias lock flag is 0.

When the JVM starts, it will carry out a series of complex activities, such as loading configuration, system class initialization and so on. In this process, a large number of synchronized keywords will be used to lock objects, and most of these locks are not biased locks. In order to reduce the initialization time, the JVM delays loading the bias lock by default, and we disable it, so the bias lock flag bit becomes 1.

Even so, it can still be seen that the underlined part is the id storage location of the biased thread. At present, it is all 0, that is, the object is not biased to any thread, so the current object is still biased

That is, the current lock state of the object is unlocked.

Biased locked state

@Test
public void test2() throws Exception {
    Object o = new Object();
    synchronized (o) {}
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

results of enforcement

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 e8 d1 02 (00000101 11101000 11010001 00000010) (47310853)
      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

Similarly, first arrange the first two rows in the order in the table, as follows:

00000000 00000000 00000000 00000000 00000010 11010001 11101000 00000101

This time, we can see that the bias lock flag bit is 1, the lock flag bit is 01, and the thread id of the bias lock is also occupied, so it is obvious that the object thread is in the bias lock state.

At the same time, it can be seen from the code that O is printed only after o is synchronized and the lock is released. It can be concluded that the biased lock state will not be revoked actively, but will continue to retain its state.

Lightweight Locking

@Test
public void test3() throws Exception {
    Object o = new Object();
    synchronized (o) {}
    new Thread(() -> {
        synchronized (o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }).start();
    TimeUnit.SECONDS.sleep(10);
}

results of enforcement

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           00 ef a5 1e (00000000 11101111 10100101 00011110) (514191104)
      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

00000000 00000000 00000000 00000000 00011110 10100101 11101111 00000000

According to the comparison table, it is obvious that the O is in the lightweight lock state at this time. Because the main thread locks o first, O is in the bias lock, and then another thread locks O. before locking, the bias lock will expand into a lightweight lock.

To sum up, it is proved.

Heavyweight lock

@Test
public void test4() throws Exception {
    Object o = new Object();
    AtomicInteger index = new AtomicInteger(0);
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            synchronized (o) {
                index.getAndIncrement();
                if(index.get() == 9)
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        }).start();
    }
    TimeUnit.SECONDS.sleep(10);
}

results of enforcement

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           fa 05 d7 1c (11111010 00000101 11010111 00011100) (483853818)
      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

00000000 00000000 00000000 00000000 00011100 11010111 00000101 11111010

There is a lot of lock competition. Theoretically, o should be in the heavyweight lock state. It is obvious from the comparison table that o is in the heavyweight lock state, which is proved.

Let's look at the following two situations

  1. Calculate the hashcode of o before locking it. What state is o in when locking it

    @Test
    public void test5() throws Exception {
        Object o = new Object();
        o.hashCode();
        synchronized (o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
    
    

    Theoretically, it should still be biased towards locking, so look at the execution results.

    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           b8 e4 ac 02 (10111000 11100100 10101100 00000010) (44885176)
          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
    
    

    At this time, the lock flag bit is 00, which is a lightweight lock, not a biased lock.

    Conclusion: after locking, the hashcode calculated object will directly expand into a lightweight lock and skip the bias lock.

  2. After locking o, calculate the hashcode of O. what is the status of o at this time

    @Test
    public void test6() throws Exception {
        Object o = new Object();
        synchronized (o) {
            o.hashCode();
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
    
    

    In theory, o here should be biased lock. See the execution results:

    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           ba c7 52 1c (10111010 11000111 01010010 00011100) (475187130)
          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
    
    

    At this time, the lock flag bit is 10, which is a heavyweight lock, not a bias lock.

    Conclusion: as long as the hashcode is calculated, the object in the biased locking state will directly expand into a heavyweight lock.

expand:

Verify that the hashcode calculated in the code is consistent with the value in the corresponding record hashcode in markword

@Test
public void test7() throws Exception {
    Object o = new Object();
    System.out.println(o.hashCode());
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

Output result:

1174290147
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 e3 3e fe (00000001 11100011 00111110 11111110) (-29433087)
      4     4        (object header)                           45 00 00 00 (01000101 00000000 00000000 00000000) (69)
      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

Copy the markword, i.e. 00000000 00000000 01000101 11111110 00111110 11100011 00000001

Referring to the table at the beginning of the article, it can be seen that from the 26th bit to the 56th bit are the bits for recording hashcode. I.e. 1000101 11111111110 00111110 11100011, convert it to decimal, and the result is 1174290147. It is consistent with the result output in the code.

springboot e-commerce project mall4j( https://gitee.com/gz-yami/mall4j)

java open source mall system

Topics: jvm Spring Boot lock synchronized