利用jol查看64位系统java对象(空对象),默认开启指针压缩,总大小显示16字节,前12字节为对象头
关闭指针压缩后,对象头为16字节:-XX:-UseCompressedOops
S.java中增加一个boolean属性
public class S { public boolean sign = true; }
使用指针压缩:boolean占1个字节,java对象整体占16字节(会向2的指数倍自动对齐)
关闭指针压缩后,java对象整体占24字节
对象头内部信息:
资料显示对象头包含两块:mark word(锁的信息,hashcode,gc信息等)和klass pointer(指向对象的元数据----指针压缩就是压缩的这部分)
并且hashcode只会再计算之后才会存入markword中
public class Main { public static void main(String[] args) { S s = new S(); System.out.println(ClassLayout.parseInstance(s).toPrintable()); System.out.println("---------------------hash---------------------"); int hashCode = s.hashCode(); System.out.println(Integer.toHexString(hashCode)); System.out.println("-------------------hash end---------------------"); System.out.println(ClassLayout.parseInstance(s).toPrintable()); } }
可以看到hashcode存储在对象头的1-7B,占据56位,并且存储的hashcode和打印的hashcode正好相反(根据硬件设备采用小端存储和大端存储来决定)
第0个bit存的是什么呢?根据资料显示存的是分带年龄、偏向锁信息,和对象状态
对象状态一共分为五种状态,分别是无锁、偏向锁、轻量锁、重量锁、gc回收标记
结论:前八位-----1未用 4:年龄 1:偏向锁标记位 2:锁状态位
x | x | x | x | x | 0 | 0 | 1 | 无锁 |
x | x | x | x | x | 1 | 0 | 1 | 偏向锁 |
x | x | x | x | x | x | 0 | 0 | 轻量级锁 |
x | x | x | x | x | x | 1 | 0 | 重量级锁 |
x | x | x | x | x | x | 1 | 1 | gc |
验证之前:jvm有偏向锁延迟机制(默认延迟4s),可以通过-XX:BiasedLockingStartupDelay=0来关闭偏向锁延迟
偏向锁验证:
public class Main { public static void main(String[] args) throws InterruptedException { S s = new S(); System.out.println(ClassLayout.parseInstance(s).toPrintable()); System.out.println("--------------------上锁前---------------------"); synFunction(); synchronized (s){ System.out.println("12345667"); System.out.println(ClassLayout.parseInstance(s).toPrintable()); } System.out.println("--------------------结束---------------------"); System.out.println(ClassLayout.parseInstance(s).toPrintable()); } public static void synFunction(){ System.out.println("synFunctioning"); } }
运行结果和学习之前想法不一致,学习之前的想法是 -------加锁之前应为无锁状态(001) 然后可以变为偏向锁状态(101) 接着释放为无锁状态(001)------------------这种认识是错误的,
实际上JVM从第一个结果可以看出,锁的状态前后都显示为偏向锁状态(101),但偏向锁状态后面几位,在经过加锁前后值不一样,经过学习后了解到偏向锁和hash计算不能共存(即经过hash计算了hashcode就不能偏向了)
这里的值实际上是偏向的线程id,并且在正常情况下,加锁的对象不可再重新偏向另一个线程;
上面打印的结果这里显示,第一个输出中,实际上并没有标记偏向的线程id,是偏向锁的无偏向状态(若当前是无锁状态,则会直接升级为轻量级锁),第二个输出则表示同步过程中的偏向线程,第三个表示结束同步以后,线程也无法重新偏向另一个线程(除了特殊情况——批量重偏向)
补充一点:无锁状态直接升级为轻量级锁
代码:
package com.learn.mytest; import org.openjdk.jol.info.ClassLayout; public class Main { private static S s; public static void main(String[] args) throws InterruptedException { s = new S(); Thread.sleep(5000); System.out.println(ClassLayout.parseInstance(s).toPrintable()); System.out.println("--------------------上锁前---------------------"); synchronized (s){ System.out.println("12345667"); System.out.println(ClassLayout.parseInstance(s).toPrintable()); } System.out.println("--------------------结束---------------------"); System.out.println(ClassLayout.parseInstance(s).toPrintable()); } }
轻量级锁验证:
package com.learn.mytest; import org.openjdk.jol.info.ClassLayout; public class Main { private static S s; public static void main(String[] args) throws InterruptedException { s = new S(); System.out.println(ClassLayout.parseInstance(s).toPrintable()); System.out.println("--------------------上锁前---------------------"); Thread t1 = new Thread(){ @Override public void run() { synchronized (s){ System.out.println("12345667"); System.out.println(ClassLayout.parseInstance(s).toPrintable()); } } }; t1.start(); t1.join(); System.out.println("--------------------t1线程结束---------------------"); synchronized (s){ System.out.println("12345667"); System.out.println(ClassLayout.parseInstance(s).toPrintable()); } System.out.println("------------------主线程使用结束---------------------"); System.out.println(ClassLayout.parseInstance(s).toPrintable()); } }
输出结果:
可以看到输出第一个1234567时,t1上锁,此时锁状态为101,是偏向锁,当t1线程使用结束,主线程再使用锁时,状态位时00,此刻为轻量级锁,并且锁后面的线程id也随之发生了变化
当主线程使用结束时,因为此时为轻量级锁,所以会释放锁,打印出无锁状态001
重量级锁:
package com.learn.mytest;
import org.openjdk.jol.info.ClassLayout;
public class Main {
private static S s;
public static void main(String[] args) throws InterruptedException {
s = new S();
System.out.println(ClassLayout.parseInstance(s).toPrintable());
System.out.println("--------------------上锁前---------------------");
Thread t1 = new Thread(){
@Override
public void run() {
synchronized (s){
System.out.println("-----------------t1线程进入------------------");
System.out.println(ClassLayout.parseInstance(s).toPrintable());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("-----------------t1线程开始------------------");
System.out.println(ClassLayout.parseInstance(s).toPrintable());
System.out.println("--------------------t1线程结束---------------------");
}
}
};
t1.start();
// t1.join();
Thread.sleep(2000);
synchronized (s){
System.out.println("12345667");
System.out.println(ClassLayout.parseInstance(s).toPrintable());
}
System.out.println("------------------主线程使用结束---------------------");
System.out.println(ClassLayout.parseInstance(s).toPrintable());
System.gc();
System.out.println("------------------gc结束---------------------");
System.out.println(ClassLayout.parseInstance(s).toPrintable());
}
}
结果:
t1线程开启后,主线程睡眠2s,t1线程拿到s的锁,开始往下执行,可以看到t1刚进入时还是偏向锁101,t1线程睡眠5s并没有释放锁,主线程睡眠2s醒来后继续执行,想要获取s的锁,后发现产生了线程争用,可以看
到此时t1和主线程持有的锁s,升级成了重量级锁10,直至系统gc以后,才释放掉锁
笔记:
单个重偏向为什么没有意义? a线程对锁s(obj)进行cas操作,在对象头记录下a线程的id,a线程再次使用时,只需要比较锁(对象头)上记录的id是否和自身线程id是否一致即可
当a线程销毁,b线程在使用锁s时,如果再次进行cas操作,记录下自身的线程id,对于资源的消耗并没有降低,还不如直接升级为轻量级锁