Performance comparison of java object header information and three locks

Performance comparison of java object header information and three locks

Information analysis for java headers
Why should I study java object headers first?This intercepts a comment from the source of a hotspot

Replace this diagram with a readable table as follows

Object Header (128 bits)
Mark Word (64 bits) Klass Word (64 bits)
unused:25 identity_hashcode:31 unused:1 age:4 biased_lock:1 lock:2 OOP to metadata object No Lock
thread:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 OOP to metadata object Biased Lock
ptr_to_lock_record:62 lock:2 OOP to metadata object Lightweight lock
ptr_to_heavyweight_monitor:62 lock:2 OOP to metadata object Weight lock
lock:2 OOP to metadata object GC

This means that the header of an object in java can take on different forms in different states of the object. There are three main states: unlocked state, locked state and gc tag state.

So I can understand that taking a lock in java actually means locking an object, that is, changing the state of the object header and entering the synchronization code block if the lock succeeds.

However, there are many kinds of locks in java. From the figure above, we can see that there are three lock states: bias lock, lightweight lock and weight lock.

The efficiency of these three locks is completely different. The analysis of efficiency will be discussed below. We only have reasonable design code to make reasonable use of the locks. So what are the principles of these three locks? So we need to study this object head first.

Layout of java objects and object headers
Use JOL to analyze java's object layout and add dependencies

    

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

Test Class

public class B {

}

public class JOLExample1 {

static  B b = new B();
public static void main(String[] args) {
    //jvm information
    out.println(VM.current().details());
    out.println(ClassLayout.parseInstance(b).toPrintable());
}

}

Look at the results

Analysis result 1: There are 16B of the whole object, 12B of the Object header, and 4B of the aligned bytes (because the size of the object on a 64-bit virtual machine must be a multiple of 8).

Since there are no fields in this object, the instance data of the object is 0B?

Two questions

1. What is instance data called objects?

2. What exactly does 12B exist in the object head?

First of all, it's easy to understand what object's instance data is. We can add a boolean field to B. Everyone knows that the boolean field accounts for 1B. Then look at the results

The size of the entire object remains unchanged for a total of 16B, with the Object header 12B, the boolean field flag accounting for 1B, and the remaining 3B being the aligned bytes.

From this we can see that the layout of an object is roughly divided into three parts: Object header, object instance data, and byte alignment.

Next we will discuss the second question, why is the object header 12B?What is stored separately in this 12B?(Different bits of VM object header have different lengths, referring to 64bit VM here)

First refer to the explanation of the object header in the openjdk document

In the above reference, it is mentioned that a java object header contains two words, and it contains the layout, type, GC state, synchronization state, and identification hash code of the heap object. How can it be included?What two words are there?

mark word is the first word. According to the document, you can see that it contains lock information, hashcode, gc information, etc. What is the second word?

klass word is the second word in the object header that mainly points to the object's metadata.

Suppose we understand that an object header consists mainly of the two parts shown above (in addition to an array object, the object header of an array object contains an array length).

So how big is a java object header?We know from the source comment of the JVM that one mark word is 64bit, so what is the length of klass?

So we need to figure out how to get detailed information about the java object header, verify its size, and verify that it contains the correct information.

From the header information printed using JOL above, you can know that one header is 12B, of which 8B is mark word, the remaining 4B is klass word, and the lock-related is mark word.

Then we focus on the analysis that the first 56 bits of the information in mark word store the hashcode of the object without lock, so we can verify it.

First Code: Calculate HashCode manually

public class HashUtil {

public static void countHash(Object object) throws NoSuchFieldException, IllegalAccessException {
    // Manual calculation of HashCode
    Field field = Unsafe.class.getDeclaredField("theUnsafe");
    field.setAccessible(true);
    Unsafe unsafe = (Unsafe) field.get(null);
    long hashCode = 0;
    for (long index = 7; index > 0; index--) {
        // Calculate every Byte in Mark Word
        hashCode |= (unsafe.getByte(object, index) & 0xFF) << ((index - 1) * 8);
    }
    String code = Long.toHexString(hashCode);
    System.out.println("util-----------0x"+code);
}

}

public class JOLExample2 {

public static void main(String[] args) throws Exception {
    B b = new B();
    out.println("befor hash");
    //No object header before HASHCODE was calculated
    out.println(ClassLayout.parseInstance(b).toPrintable());
    //hashcode of JVM calculation
    out.println("jvm------------0x"+Integer.toHexString(b.hashCode()));
    HashUtil.countHash(b);
    //When hashcode is calculated, we can see the change of information in the object header
    out.println("after hash");
    out.println(ClassLayout.parseInstance(b).toPrintable());

}

}

Analysis result 3:

1--- There is no object header information before hashcode. You can see that 56bit has no value. It will be value after hashcode is printed. Why is it 1-7B, not 0-6B?Because it's small-end storage.

Two of these lines are the result of our hashcode method. The first line is the hashcode I calculated from 1-7B of information, so I can make sure that the last seven bytes in the mark work in the java object header store hashcode information.

Then the eight bits in the first byte are the zoning age, the biased lock information, and the state of the object. The information represented by the eight bits is shown in the following figure (in fact, the figure above also has information), which changes with the state of the object. The figure below is the unlocked state.

There are five states about object state, namely unlock, bias lock, lightweight lock, weight lock, GC tag.

So how can 2 bits represent five states (2 bits can only represent 4 states at most: 00,01,10,11), respectively?

The jvm does a good job of representing the skewed and unlocked states as the same state, and then identifying the unlocked or skewed states according to the skewed lock identification in the diagram.

What does that mean?Write a code analysis. Before writing code, we first remember the information 00000001 in unlocked state, then write an example of biased lock to see the results

public static void main(String[] args) throws Exception {

//-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
B b = new B();
out.println("befor lock");
out.println(ClassLayout.parseInstance(b).toPrintable());
synchronized (b){
    out.println("lock ing");
    out.println(ClassLayout.parseInstance(b).toPrintable());
}
out.println("after lock");
out.println(ClassLayout.parseInstance(b).toPrintable());

}

The program above has only one thread to call the sync method, so it makes sense to prefer locks, but at this point it is a lightweight lock

And you'll find that the final output (the first byte) is still the same as 00000001 and unlocked, because the virtual machine has a delay on bias locking at startup.

If you add the code for 5 seconds of sleep to the above code, the result will be different.

public static void main(String[] args) throws Exception {

    //-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
    Thread.sleep(5000);
    B b = new B();
    out.println("befor lock");
    out.println(ClassLayout.parseInstance(b).toPrintable());
    synchronized (b){
        out.println("lock ing");
        out.println(ClassLayout.parseInstance(b).toPrintable());
    }
    out.println("after lock");
    out.println(ClassLayout.parseInstance(b).toPrintable());
}

The result turns to 00000101. Of course, for testing purposes we can also disable latencies directly through the parameters of the JVM

-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

The result is the same as sleeping for 5 seconds.

Think about why the bias lock is delayed?Because when you start a program, jvm has many operations, including gc, etc. There are a lot of synchronization methods when jvm just runs, many of which are not biased locks.

It takes time and resources to upgrade a bias lock to a light/heavy lock, so the jvm delays by about four seconds to turn on the bias lock again.

So why did synchronization bias lock before? I guess it's jvm, and it's not clear yet.

Note that after lock, bias information is maintained after exit from synchronization

Then look at the object head of the lightweight lock

static A a;

public static void main(String[] args) throws Exception {
    a = new A();
    out.println("befre lock");
    out.println(ClassLayout.parseInstance(a).toPrintable());
    synchronized (a){
        out.println("lock ing");
        out.println(ClassLayout.parseInstance(a).toPrintable());
    }
    out.println("after lock");
    out.println(ClassLayout.parseInstance(a).toPrintable());
}

Look at the results:

About weight locks look first at the object head

static A a;

public static void main(String[] args) throws Exception {
    //Thread.sleep(5000);
    a = new A();
    out.println("befre lock");
    out.println(ClassLayout.parseInstance(a).toPrintable());//No Lock

    Thread t1= new Thread(){
        public void run() {
            synchronized (a){
                try {
                    Thread.sleep(5000);
                    System.out.println("t1 release");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    };
    t1.start();
    Thread.sleep(1000);
    out.println("t1 lock ing");
    out.println(ClassLayout.parseInstance(a).toPrintable());//Lightweight lock
    sync();
    out.println("after lock");
    out.println(ClassLayout.parseInstance(a).toPrintable());//Weight lock
    System.gc();
    out.println("after gc()");
    out.println(ClassLayout.parseInstance(a).toPrintable());//No Lock - gc
}

public  static  void sync() throws InterruptedException {
    synchronized (a){
        System.out.println("t1 main lock");
        out.println(ClassLayout.parseInstance(a).toPrintable());//Weight lock
    }
}

Look at the results

From the above experiments, the following figure can be summarized:

Performance comparison bias lock and lightweight lock:

public class A {

int i=0;

public synchronized void parse(){
    i++;
    
}
//JOLExample6.countDownLatch.countDown();

}

Perform 1000000000L++ operations

public class JOLExample4 {

public static void main(String[] args) throws Exception {
    A a = new A();
    long start = System.currentTimeMillis();
    //Call the synchronization method 1000000000L to calculate 1000000000L++, compare the performance of bias and lightweight locks
    //If nothing happens, the results are often obvious
    for(int i=0;i<1000000000L;i++){
        a.parse();
    }
    long end = System.currentTimeMillis();
    System.out.println(String.format("%sms", end - start));

}

}

At this point, according to the above test, it is a lightweight lock, see the results

About 16 seconds

Then we let the deflection lock start without delay, at start once

-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
Look at the results again

It only takes 2 seconds and it's much faster

Look at the time of the heavy lock again

static CountDownLatch countDownLatch = new CountDownLatch(1000000000);

public static void main(String[] args) throws Exception {
    final A a = new A();

    long start = System.currentTimeMillis();

    //Call the synchronization method 1000000000L to calculate 1000000000L++, compare the performance of bias and lightweight locks
    //If nothing happens, the results are often obvious
    for(int i=0;i<2;i++){
        new Thread(){
            @Override
            public void run() {
                while (countDownLatch.getCount() > 0) {
                    a.parse();
                }
            }
        }.start();
    }
    countDownLatch.await();
    long end = System.currentTimeMillis();
    System.out.println(String.format("%sms", end - start));

}

Looking at the results, about 31 seconds,

It can be seen that the consumption of the three locks is quite different, which is also the significance of synchronized optimization after 1.5

Note that if the object has a hashcode calculated, it cannot be biased

static A a;

public static void main(String[] args) throws Exception {
    Thread.sleep(5000);
    a= new A();
    a.hashCode();
    out.println("befor lock");
    out.println(ClassLayout.parseInstance(a).toPrintable());
    synchronized (a){
        out.println("lock ing");
        out.println(ClassLayout.parseInstance(a).toPrintable());
    }
    out.println("after lock");
    out.println(ClassLayout.parseInstance(a).toPrintable());
}

Look at the results

Original Address https://www.cnblogs.com/lusaisai/p/12748869.html

Keywords: Java jvm

Added by catalin.1975 on Thu, 23 Apr 2020 05:30:45 +0300