文章目录
由JAVA对象布局到锁的原理
前言
本文基于64位虚拟机做测试
对象的内存布局
hotspot的虚拟机中,对象的内存布局包括三个部分,对象头,实例数据,对齐填充
对象头 ObjectHeader(12Byte)
下面英文摘录自hotspot
Common structure at the beginning of every GC-managed heap object.
Includes fundamental information about the heap objects layout, type,
GC state, synchronization state, and identity hash code.
Consists of two words. In arrays it is immediately followed by a length field.
Note that both Java objects and VM-internal objects have a common object header format.
GC管理的对象的公共的数据结构
包含对象布局,类型,GC状态,锁状态,hashcode
由两部分组成第一部分Mark Word,第二部分klass pointer
Mark word(8Byte)
The first word of every object header.
Usually a set of bitfields including synchronization state and identity hash code.
May also be a pointer to synchronization related information.
During GC, may contain GC state bits.
对象头的第一部分
包含锁状态,hashcode
也可以是指向同步相关信息的指针
可能包含gc状态
下面是openJDK的源码markOop.hpp对于64位虚拟机Mark Word描述的片段
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)
详细一点的Mark Word
无锁 unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2
偏向 thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2
轻量 ptr_to_lock_record:62 | lock:2
重量 ptr_to_heavyweight_moniter:62 | lock:2
GC 空 | lock:2
我们着重需要了解下面8bits
unused:1 age:4 biased_lock:1 lock:2
age占四位是因为晋升老年代的临界年龄是15岁
偏向标记1代表偏向锁,0代表不是偏向锁
对象状态
MarkWord存储内容 偏向标记 对象状态 状态
hashcode,age 0 01 无锁
偏向线程id,age 1 01 偏向锁
指向锁记录的指针 00 轻量锁
指向重量级锁的指针 10 重量锁
空 11 GC
重量锁:使用操作系统mutex_lock来实现的传统锁(有竞争,多个线程同时请求进入临界区)
轻量锁:没有资源竞争的时候使用CAS消除同步使用的互斥量(无竞争,多个线程交替进入临界区,轻量级锁退出同步块后变成无锁)
偏向锁:没有资源竞争的时候把整个同步消除,连CAS也不做(无竞争,只有一个线程进入临界区,偏向锁退出同步块还是偏向锁)
klass pointer(4Byte)
The second word of every object header.
Points to another object (a metaobject)
which describes the layout and behavior
of the original object.
对象头的第二部分
类型指针指向类元数据
JVM通过这个指针确定该对象是哪个类的实例
实例数据
对象真正存储起来的有效信息
对齐填充
为了保证对象大小是8的整数倍
测试对象内存布局
添加依赖,没用的话就用jar包
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol‐core</artifactId>
<version>0.8</version>
</dependency>
A.class
public class A {
long i;
}
测试
public static void main(String[] args) throws Exception {
A a = new A();//猜一猜对象a占多少字节(对象头+实例数据+填充)
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
输出结果
com.lry.basic.sync.A 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) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
12 4 (alignment/padding gap)
16 8 long A.i 0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
结果分析
对象头=4*3=12字节
实例数据=8字节
填充=4字节
a=24字节
第一行和第二行的对象头是Mark Word,第三行是Klass pointer
我们着重于分析unused:1 age:4 biased_lock:1 lock:2
由于我电脑系统是小端模式,数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,
所以这8个字节实际上是00000001,而不是对象头第二行的最后一个字节00000000
由00000001可知我们对象状态是01,偏向标记=0,处于无锁状态。
轻量级锁与偏向锁的性能对比
先看轻量级锁
A
public class A {
long i;
public synchronized void add(){
++i;
}
}
public static void main(String[] args) throws Exception {
A a = new A();
long start = System.currentTimeMillis();
//没有竞争,偏向锁开启有4s左右延迟,所以这里是轻量级锁
//如果启用偏向锁的话,偏向锁会有几秒的延迟,这是为了在启动JVM时候降低资源消耗的一种措施
for(int i=0;i<1000000000;i++)
a.add();
long end = System.currentTimeMillis();
System.out.println(String.format("%sms", end-start));
}
结果是17747ms
再看偏向锁
public static void main(String[] args) throws Exception {
Thread.sleep(5000);//等待5s 度过4s偏向锁的延迟
A a = new A();
long start = System.currentTimeMillis();
//没有竞争,偏向锁已经开启,所以这里是偏向锁
for(int i=0;i<1000000000;i++)
a.add();
long end = System.currentTimeMillis();
System.out.println(String.format("%sms", end-start));
}
结果是1336ms
还有一种方法可以不睡眠5s,加入-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0参数效果一样。
我偶然的看到了openJDK关于偏向锁的源码biasedLocking.cpp
void BiasedLocking::init() {
if (UseBiasedLocking) {
if (BiasedLockingStartupDelay > 0) {
EnableBiasedLockingTask* task = new EnableBiasedLockingTask(BiasedLockingStartupDelay);
task->enroll();
} else {
VM_EnableBiasedLocking op(false);
VMThread::execute(&op);
}
}
}
重量级锁的性能
A
public class A {
long i;
public synchronized void add(){
++i;
Sync1.cdl.countDown();
}
}
Sync1
public class Sync1 {
final static CountDownLatch cdl = new CountDownLatch(1000000000);
public static void main(String[] args) throws Exception {
final A a = new A();
long start = System.currentTimeMillis();
for(int i=0;i<2;i++)//资源竞争
new Thread(new Runnable() {
@Override
public void run() {
while(cdl.getCount()>0)
a.add();
}
}).start();
cdl.await();
long end = System.currentTimeMillis();
System.out.println(String.format("%sms", end-start));
}
}
结果是37040ms
计算hash后不可偏向
我们先来看看无锁状态计算hash后的对象布局
public static void main(String[] args) throws Exception {
A a = new A();
System.out.println("jvm‐‐‐‐‐‐‐‐‐‐‐‐"+Integer.toHexString(a.hashCode()));
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
结果如下
jvm‐‐‐‐‐‐‐‐‐‐‐‐1540e19d
com.lry.basic.sync.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 9d e1 40 (00000001 10011101 11100001 01000000) (1088527617)
4 4 (object header) 15 00 00 00 (00010101 00000000 00000000 00000000) (21)
8 4 (object header) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
12 4 (alignment/padding gap)
16 8 long A.i 0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
对象状态是无锁状态
hashcode的16进制是1540e19d,从对象头第二行的VALUE第一个往前数正好是1540e19d.
启用偏向锁
public static void main(String[] args) throws Exception {
Thread.sleep(5000);
A a = new A();
// System.out.println("jvm‐‐‐‐‐‐‐‐‐‐‐‐"+Integer.toHexString(a.hashCode()));
synchronized (a){}
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
结果如下
com.lry.basic.sync.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 28 26 03 (00000101 00101000 00100110 00000011) (52832261)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
12 4 (alignment/padding gap)
16 8 long A.i 0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
可以看出来确实是偏向锁,并且对象头还存储了偏向线程id。
但是把计算hash的代码打开后却发现是无锁状态,因为对象头要存储hashcode。
wait后变成重量级锁
JVM参数:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
public static void main(String[] args) throws Exception {
final A a = new A();
System.out.println("before lock:"+ClassLayout.parseInstance(a).toPrintable());//偏,无偏向线程id
Thread t = new Thread(){
@Override
public void run() {
synchronized (a){
System.out.println("before wait:"+ClassLayout.parseInstance(a).toPrintable());//偏,有偏向线程id
try {
a.wait();//释放锁a
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after wait:"+ClassLayout.parseInstance(a).toPrintable());//重锁
}
}
};
t.start();
t.join(1000);//等待t线程执行完或者一秒后
synchronized (a){
a.notifyAll();
}
Thread.sleep(1000);//这里改成t.join() 发现竟然还是重锁,why?有人知道可以评论里面教下我
System.out.println("after lock:"+ClassLayout.parseInstance(a).toPrintable());//无锁
//批量偏向 阈值20
}
运行结果和注释一样
批量重偏向
批量偏向阈值20
JVM参数:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
以class为单位,为每个class维护一个偏向锁撤销器,每当该class的对象发生偏向锁撤销操作,该计数器+1,当这个值到20,就会发生批量重偏向。
public static void main(String[] args) throws Exception {
final List<A> list = new ArrayList<>();
Thread t = new Thread(){
@Override
public void run() {
for(int i=0;i<25;i++){
A a = new A();
synchronized (a){//偏向锁,偏心于t这个线程
list.add(a);
}
}
}
};
t.start();
t.join();
System.out.println("偏锁:"+ClassLayout.parseInstance(list.get(0)).toPrintable());//偏
//<20 无竞争,线程交替执行 ,轻锁
//>=20 批量偏向
new Thread(){
@Override
public void run() {
for(int i=0;i<list.size();i++){
A a = list.get(i);
synchronized (a){
if(i<19)//偏向锁撤销20次
System.out.println("轻锁:"+ClassLayout.parseInstance(a).toPrintable());
if(i>=19)
System.out.println("批量重偏向:"+ClassLayout.parseInstance(a).toPrintable());
}
}
}
}.start();
}