Java对象的内存结构
对象内存结构
在64位操作系统下,
MarkWord(下图_mark)占64位
KlassWord(下图_klass)占32位 64位系统的Klass Word不是32位,默认64位,开启指针压缩后为32(感谢评论老哥的指出)
64位系统的Klass Word不是32位,默认64位,开启指针压缩后为32
_lengh(只有数据对象才有,不考虑)
实例数据(下图instance data)看参数的类型,int就占32位(4byte)
补齐(padding)是JVM规定java对象内存必须是8byte的倍数,如果实例数据占2byte,那么(64bit的Markword+32bit的Klassword+实例数据32bit)=128bit=16byte是8byte的倍数,所以padding部分为0。
查看对象内存结构
JDK8
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
public class SynchronizedDemo {
public static void main(String[] args) {
Dog dog = new Dog();
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
}
}
class Dog {
char age;
}
如上图所示,对象头中的MarkWord占8byte,KlassWord占4个byte,实例属性age是char类型占2个byte,那么此时加起来为14byte,为了满足是8的倍数,要补充2个byte。
下图是当Dog对象里的age变为int时打印的结果,请自行对比。
对象头
下图是引自《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版) 周志明》中的一个图,下图是32操作系统下的对象头中的Mark Word(32位),Klass Word(32位),一共是64位。
64操作系统下,Mark Word的长度是64,在加Klass Word(32位),一共是96位,其实对象头长什么样其实不是本文的重点,本文的重点是验证锁升级的过程,所以我们只需要关注对象头中Mark Word的最后3位即可,如下图中的后3位。
锁升级的过程
锁状态
25bit
4bit
1bit
2bit
23bit
2bit
是否偏向锁
锁标志位
1
无锁
对象的HashCode
分代年龄
0
01
2
无锁
对象的HashCode
分代年龄
1
01
3
偏向锁
线程ID
Epoch
分代年龄
1
01
4
轻量级锁
指向栈中锁记录的指针
00
5
重量级锁
指向重量级锁的指针
10
6
GC标记
空
11
前提
由于大小端引起的问题,使得这里展示的高低位相反,如下图所示,所以我要关注的就是⑧位置的最后3位足矣。
无锁状态
public class SynchronizedDemo {
public static void main(String[] args) {
Dog dog = new Dog();
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
}
}
如下图所示,001表示的无锁状态并且不允许偏向 (其实默认是开启偏向的,只不过虚拟机后在运行后几秒才开启偏向锁)
使用下面的参数,如下图所示 ,会发现状态为101,表示无锁状态
-XX:BiasedLockingStartupDelay=0
由无锁状态---->偏向锁状态
单线程访问锁的时候,锁由无锁状态变为偏向锁状态。
// -XX:BiasedLockingStartupDelay=0
public class SynchronizedDemo {
public static void main(String[] args) {
Dog dog = new Dog();
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
//上锁
synchronized (dog){
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
}
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
}
}
class Dog {
int age;
}
如上图所示,开始状态为101,为可偏向,无锁状态
上锁后状态是101,为可偏向,有锁状态
解锁后:状态为101,为可偏向,有锁状态
区别为:当线程给无锁状态的lock加锁时,会把线程ID存储到MarkWord中,即锁偏向于该ID的线程,偏向锁不会自动释放。
上面表格中2->3的过程。
偏向锁状态---->轻量级锁状态
多线程使用锁(不竞争,错开时间访问),锁由偏向锁状态变为轻量级锁状态
// -XX:BiasedLockingStartupDelay=0
public class SynchronizedDemo {
public static void main(String[] args) {
Dog dog = new Dog();
System.out.println("初始状态:");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
new Thread(
() -> {
synchronized (dog) {
System.out.println("hello world");
}
},
"t1")
.start();
System.out.println("线程1释放锁后:");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
}
new Thread(
() -> {
synchronized (dog) {
System.out.println("线程2上锁:");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
}
System.out.println("线程2释放锁:");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
},
"t2")
.start();
}
}
class Dog {
int age;
}
初始状态为101,为可偏向,并且为无锁状态
线程1释放锁后,状态为101,并且存储了线程ID,为偏向锁状态,偏向于线程1
线程2上锁,上锁后,状态为00,轻量级锁状态
线程2释放锁后,状态为001,此时为不可偏向的无锁状态。
重量级锁状态
// -XX:BiasedLockingStartupDelay=0
public class SynchronizedDemo {
public static void main(String[] args) {
Dog dog = new Dog();
System.out.println("初始状态:");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
new Thread(
() -> {
synchronized (dog) {
System.out.println("");
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
}
}
},
"t1")
.start();
new Thread(
() -> {
synchronized (dog) {
System.out.println("线程2上锁");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
}
}
System.out.println("线程2释放锁");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
},
"t2")
.start();
}
}
class Dog {
int age;
}
如上图所示,锁初始状态为101,可偏向无锁状态
当线程1在使用锁,而线程2去上锁的时候,状态已经变为010,不可偏向重量级锁。
总结
单线程使用锁的时候为偏向锁。
多线程无竞争(错峰使用锁)的时候为轻量级锁。
有竞争的时候为重量级锁。
参考
这可能是B站上最深入解析的synchronized底层原理解析_哔哩哔哩_bilibili
视频去哪了呢?_哔哩哔哩_bilibili