引言
下面代码块是很常用的两种加锁方式。
public class Test {
public static void main(String[] args) {
A a = new A();
//使用synchronized
synchronized (a){
//同步代码块
}
//使用ReentrantLock
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();
try {
//同步代码块
}finally {
reentrantLock.unlock();
}
}
}
class A{}
我在ReentrantLock源码解析--加锁过程中有说过,ReentrantLock通过state关键字,记录锁重入的次数,还会记录当前持有锁的线程和使用双向链表来记录排队线程。那么synchronized是如何实现加锁过程呢?他是否有类似state的关键字?上面代码中我使用a当作锁,来同步一个代码块。很明显这个class A{}中并没有类似state的域对象。那么synchronized到底锁的是什么。 在java编程思想第二章 一切都是对象中有这样的一些关于对象的解释。
Java语言尽管将一切都视为对象,但实际上操控着程序的是对象的一个引用(reference)。
如果你想操纵一个词或句子,则可以创建一个String的引用:String s;但这里创建的只是引用并不是对象。
一旦创建了一个引用,就希望它能够与一个新的对象相关联。通常用new操作符来实现这一目的。
那么这里synchronizd(a){}锁的是引用还是对象呢。于是修改上面的代码。
A a = null;
synchronized (a){
//同步代码块
}
这样a表示一个引用,但是运行抛出了空指针异常,说明synchronized锁的并不是引用,而是a指向的对象,也就是new A()。但是既然锁的是对象a,而对象a中又没有类似state的结构,那我我们就需要探究一下这个对象的本质,也就是java中的对象到底是由什么组成的。
java对象的组成结构
我们可以在pom里面添加这个以来用于打印类的信息。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
public static void main(String[] args) {
A a = new A();
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
输出结果:
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 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
在这里我们可以清楚的看见,我们的对象a,大小是16byte,由两个部分组成一个是object header(对象头),一个是loss due to the next object alignment(也就是对齐填充字节)。如果你的类中还有域对象(实例数据)的话,还会由具体的实例数据组成。这里演示的是64位虚拟机打印的数据。
例如我在class A 中添加了一个boolean对象,于是域数据(实例数据)就占了1字节,填充数据就占了3字节。是否有填充数据是根据你的数据类型来决定的。由于HotSpot的对齐方式为8字节对齐,所以对象的大小必须是8字节的倍数。所以一个对象最小为16个字节。由于对象头占了12字节,如果你的数据正好是4个字节的,如int,那么就不会有填充数据。顺便说下,如果是数组的话object header会多一行表示数组长度。
//对象A中有一个boolean类型的域对象(实例数据)
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 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 1 boolean A.flag false
13 3 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
//对象A中有一个int类型的域对象(实例数据)
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 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int A.flag 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
实例数据和对齐填充字节都很好理解,那么什么是对象头(object header)呢。 在HotSpot Glossary of Terms中有关于对象头的描述
Common structure at the beginning of every GC-managed heap object. (Every oop points to an object header.) Includes fundamental information about the heap object's 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托管堆对象开头的公共结构。(每个oop都指向一个对象头。)包括有关堆对象的布局、类型、GC状态、同步状态和标识哈希代码的基本信息。 由两个词组成。在数组中,紧接着是一个长度字段。注意Java对象和VM内部对象都有一个通用的对象头格式。
也就是说对象头由两个“词”组成,可以理解位两部分组成。对象头中保存了对象的布局、类型、GC状态、同步状态和标识哈希代码的基本信息。这其中就有我们需要的同步状态(synchronization state)。
两个“词”是指什么呢?
mark word The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits.
klass pointer The second word of every object header. Points to another object (a metaobject) which describes the layout and behavior of the original object. For Java objects, the "klass" contains a C++ style "vtable".
到这里我们可以知道,对象头包括两个词组成.
- 第一个词是mark word,存放了 同步状态和标识哈希码。也可以是指向同步相关信息的指针(具有特征性的低位编码)。在GC期间,可以包含GC状态位。
- 第二个词是klass pointer 指向另一个对象(元对象),该对象描述原始对象的布局和行为。
接着我们可以查阅openjdk源码中关于对象头的部分对象头中关于mark word 的部分markOop.hpp
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
// 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)
//
// unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
// unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
从代码注释中我们可以的看出mark word在64位的虚拟机中占了64个bit,也就是8个byte,在32位虚拟机中占了32个bit。而且在不同的状态下,存储的数据也是不同的。
由于mark word是第一个词,大小为8byte,也就是前两行。但是似乎和注释所说的有所区别。上面写对象在(normal object)状态时,前25位是未使用,接着紧跟着是31位的hashcode。但是这里前八位00000001明显是有值的,之后全是0明显未被使用。不是存放着hashcode和年龄信息吗?
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)
这是由于hashcode尚未被初始化。那么我们试着打印一下hashcode,让其初始化再看看结果。
//打印十六进制的hashcode
public static void main(String[] args) {
A a = new A();
System.out.println(Integer.toHexString(a.hashCode()));
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
74a14482
A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 82 44 a1 (00000001 10000010 01000100 10100001) (-1589345791)
4 4 (object header) 74 00 00 00 (01110100 00000000 00000000 00000000) (116)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int A.a 0
16 1 boolean A.flag false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
现在我们可以发现对象头的信息和之前不同了,而且我们输出的hashcode和对象头里面显示的是相同的,不过是倒着的。这是由于windows操作系统采用的是小端存储模式,数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。所以我们应该从第二行的末尾八位当作数据的开头。这样就和注释的内容对上了。 00000000 00000000 00000000 0这25位是未被使用的字节,1110100 10100001 01000100 10000010存储的是hashcode,紧接着0是未使用的,对象年龄age为0000,偏向标识为0,锁状态为01(无锁)。这里我们着重讨论最后的三位偏向标识+锁状态。
前面说过mark word存放了对象的同步状态,那么同步状态有哪些呢,同样的我们查阅官方文档HotSpot Runtime Overview中描述了同步的相关状态。
Neutral: Unlocked
Biased: Locked/Unlocked + Unshared
Stack-Locked: Locked + Shared but uncontended
The mark points to displaced mark word on the owner thread's stack.
Inflated: Locked/Unlocked + Shared and contended
Threads are blocked in monitorenter or wait().
The mark points to heavy-weight "objectmonitor" structure.[8]
未加锁(Neutral),偏向锁(Biased),轻量级锁(Stack-Locked),和重量级锁(Inflated)四个状态。 如果对象被GC标记准备回收,还会有一个待回收状态。这个状态在下文的引用中有说明。
无锁状态很好理解的。那么什么是偏向锁呢?
the biased lock pattern is used to bias a lock toward a given thread. When this pattern is set in the low three bits, the lock is either biased toward a given thread or "anonymously" biased,indicating that it is possible for it to be biased. When the lock is biased toward a given thread, locking and unlocking can be performed by that thread without using atomic operations.
偏向锁模式用于将锁偏向给定线程。当这个模式设置为低3位时,锁要么偏向于给定的线程,要么“匿名”偏向,这表明它可能有偏向。当锁偏向于给定的线程时,该线程可以执行锁定和解锁,而无需使用原子操作。
也就是说偏向锁的设置是至少三位的(偏向一位,锁状态两位),如果mark word里面有线程ID,那么就表示偏向了具体的线程。如果没有存放ID,则表示匿名偏向。
Note that we are overloading the meaning of the "unlocked" state of the header. Because we steal a bit from the age we can guarantee that the bias pattern will never be seen for a truly unlocked object.
注意,我们重载了头的“unlocked”状态的含义。因为我们从年龄上偷了一点,所以我们可以保证,对于一个真正解锁的对象,永远不会看到偏向模式。
真正解锁的对象至少是轻量锁锁了,而锁的膨胀是不可逆的,所以就不能在偏向了。
Note also that the biased state contains the age bits normally contained in the object header. Large increases in scavenge times were seen when these bits were absent and an arbitrary age assigned to all biased objects, because they tended to consume a significant fraction of the eden semispaces and were not promoted promptly, causing an increase in the amount of copying performed. The runtime system aligns all JavaThread* pointers to a very large value (currently 128 bytes (32bVM) or 256 bytes (64bVM)) to make room for the age bits & the epoch bits (used in support of biased locking), and for the CMS "freeness" bit in the 64bVM (+COOPs).
还要注意,偏置状态通常包含在对象头中的年龄位。当这些位不存在,并且为所有有偏置的对象指定了任意的年龄时,可以看到清除时间的大幅增加,因为它们往往消耗了大量的eden空间,并且没有得到及时的提升,从而导致执行的复制量增加。运行时系统将所有JavaThread*指针对齐到一个非常大的值(当前为128字节(32bVM)或256字节(64bVM)),以便为age位和epoch位(用于支持偏置锁定)以及64bVM(+COOPs)中的CMS“freeness”位腾出空间。
[JavaThread* | epoch | age | 1 | 01] lock is biased toward given thread //锁偏向于给定的线程
[0 | epoch | age | 1 | 01] lock is anonymously biased //锁是匿名偏向的
- the two lock bits are used to describe three states: locked/unlocked and monitor. //这两个锁定位用于描述三种状态:锁定/解锁和监视。
[ptr | 00] locked ptr points to real header on stack //ptr指向堆栈上的实际头
[header | 0 | 01] unlocked regular object header //常规对象头
[ptr | 10] monitor inflated lock (header is wapped out) //膨胀锁(头部脱离了)
[ptr | 11] marked used by markSweep to mark an object //由markSweep标记用于标记对象(被GC标记清理)
结合这些引用和翻译我们可以得出结论。
- 偏向锁的标示需要三位。在baised_lock占了1 bit,其中1表示偏向某个线程或者匿名偏向(是否偏向取决于mark word 中是否保存了线程的ID),0表示不偏向,或者解锁。接着紧接的两个bit表示lock位,01用来标识这是一个偏向锁,也能表示已解锁。(例如最后三位101表示偏向锁,001表示解锁)
- 如果该对象的标志是一个偏向锁,则一般会包含年龄信息(4bit 的age),如果表示年龄位不存在但是被指定了任意年龄时,线程ID会对齐到很大的值,64位虚拟机为256字节,这会消耗大量的eden空间从而增加垃圾回收成本。
- 当锁偏向于给定的线程时,该线程可以执行锁定和解锁,而无需使用原子操作。
- 如果对象被解锁,那么就不会再次进入偏向模式(至少是轻量级锁)。
- 如果lock位为00(轻量级锁),这时对象的头部会存储在堆栈上,这里只保存一个地址。
- 如果lock位为10(重量级锁),对象的头部脱离了(wapped out),这里保存的是指向重量级锁的指针。
注:由于age的存储只有4位,所以对象最多经过15次垃圾回收后就进入老年代。 -XX:+PrintGCDetails 可以用来打印垃圾回收的信息 -XX:MaxTenuringThreshold=15 用来设置回收多少次后进入老年代。读者可自行打印对象头查看age变化。
关于对象头存储的内容与lock标志位的关系可以简化为下面这张表
| 存储内容 | 标志位 | 状态 |
|---|---|---|
| 对象hashcode,对象年龄,偏向标志 | 01 | 未加锁 |
| 对象线程ID,偏向时间戳,对象年龄,偏向标志 | 01 | 偏向某个线程 |
| 锁记录的指针 | 00 | 轻量级锁 |
| 重量级锁的指针 | 10 | 重量级锁 |
| 空 | 11 | GC回收 |