JVM源码分析 -- 偏向锁

JVM源码分析 – 偏向锁

前言

JAVA在内部提供了许多种锁,在虚拟机内部,又会根据虚拟机配置和场景来使用不同种类的锁,比如偏向轻量级以及重量级等等,这篇文章根据1.8的源码,来看一下JAVA内部实现的锁。提前说明,笔者并非专业的C++开发人员,只了解一些基本的语法,只能通过方法命名、注释来大体了解一下代码的执行过程以及效果。

我们从字节码开始,我们使用synchronized(Object)包裹代码块的时候,字节码中会用如下方式包裹:

1
2
3
monitorenter; lock object in local ...
...
monitorexit; finished with object in local ...

解释器执行monitorenter的时候,会进入InterpreterRuntime.cpp,我们直接看一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//这里这个thread,就是java中的currentThread
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
if (PrintBiasedLockingStatistics) {
Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
}
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()), "must be NULL or an object");

//这里这个UseBiasedLocking,是一个全局的运行时配置,标识虚拟机是否开启偏向锁,开启用fast,否则slow
if (UseBiasedLocking) {
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
assert(Universe::heap()->is_in_reserved_or_null(elem->obj()), "must be NULL or an object");
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END

有点不明白的地方,在Google上找了一下BasicObjectLock的定义,这就是一个包装类:

1
2
3
4
5
class BasicObjectLock {
BasicLock _lock;
// object holds the lock;
oop _obj;
}

持有一个BasicLock和一个对象。这个BasicLock对象找了一顿定义,没有找到,但是从他的方法来看,主要是跟一些地址偏移相关,同样Google一下,找到了其定义:

1
2
3
4
class BasicLock {
//主要用来保存_obj指向Object对象的对象头数据
volatile markOop _displaced_header;
}

这个Handle类可以理解成一个“句柄”,他被用来在垃圾回收的时候,对象可能移动(地址变化),通过handle访问对象可以让调用方不去关心地址的变化,垃圾回收的细节。

偏向锁

我们接着看上面的代码,我们发现虚拟机发现开启偏向锁,会优先使用偏向锁,偏向锁只依赖一次CAS操作来置换ThreadId。默认开启了偏向锁,我们可以用-XX:-UseBiasedLocking关闭。

我们看一下上面的fast_enter方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
if (UseBiasedLocking) {
if (!SafepointSynchronize::is_at_safepoint()) {
BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
return;
}
} else {
assert(!attempt_rebias, "can not rebias toward VM thread");
BiasedLocking::revoke_at_safepoint(obj);
}
assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
}

slow_enter (obj, lock, THREAD) ;
}

这里又判断了一次,是怕运行时更新了配置?

我们这里看到了一个新的名词叫SafePoint,这里是个新东西,在Hotspot的垃圾回首时,这个SafePoint也会出现,我们先不考虑,只是发现,如果不在安全点上,我们就进入偏向。我们从revoke_and_rebias这个名字可以看出,偏向锁就是在没有多线程竞争的情况下,减少不必要的锁执行路径,只是更新了一下线程Id(撤销一个,偏向另外一个)

####revoke_and_rebias

这个方法连带注释,有好多好多逻辑,我们只看一下比较主要的,了解一下偏向锁的实现逻辑,先看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
BiasedLocking::Condition BiasedLocking::revoke_and_rebias(Handle obj, bool attempt_rebias, TRAPS) {
assert(!SafepointSynchronize::is_at_safepoint(), "must not be called while at safepoint");

// We can revoke the biases of anonymously-biased objects
// efficiently enough that we should not cause these revocations to
// update the heuristics because doing so may cause unwanted bulk
// revocations (which are expensive) to occur.
//获取对象头的Mark Word
markOop mark = obj->mark();
//判断mark是否为可偏向状态
if (mark->is_biased_anonymously() && !attempt_rebias) {
// We are probably trying to revoke the bias of this object due to
// an identity hash code computation. Try to revoke the bias
// without a safepoint. This is possible if we can successfully
// compare-and-exchange an unbiased header into the mark word of
// the object, meaning that no other thread has raced to acquire
// the bias of the object.
markOop biased_value = mark;
markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj->mark_addr(), mark);
if (res_mark == biased_value) {
return BIAS_REVOKED;
}
} else if (mark->has_bias_pattern()) {
Klass* k = obj->klass();
markOop prototype_header = k->prototype_header();
if (!prototype_header->has_bias_pattern()) {
// This object has a stale bias from before the bulk revocation
// for this data type occurred. It's pointless to update the
// heuristics at this point so simply update the header with a
// CAS. If we fail this race, the object's bias has been revoked
// by another thread so we simply return and let the caller deal
// with it.
markOop biased_value = mark;
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(prototype_header, obj->mark_addr(), mark);
assert(!(*(obj->mark_addr()))->has_bias_pattern(), "even if we raced, should still be revoked");
return BIAS_REVOKED;
} else if (prototype_header->bias_epoch() != mark->bias_epoch()) {
// The epoch of this biasing has expired indicating that the
// object is effectively unbiased. Depending on whether we need
// to rebias or revoke the bias of this object we can do it
// efficiently enough with a CAS that we shouldn't update the
// heuristics. This is normally done in the assembly code but we
// can reach this point due to various points in the runtime
// needing to revoke biases.
if (attempt_rebias) {
assert(THREAD->is_Java_thread(), "");
markOop biased_value = mark;
markOop rebiased_prototype = markOopDesc::encode((JavaThread*) THREAD, mark->age(), prototype_header->bias_epoch());
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(rebiased_prototype, obj->mark_addr(), mark);
if (res_mark == biased_value) {
return BIAS_REVOKED_AND_REBIASED;
}
} else {
markOop biased_value = mark;
markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj->mark_addr(), mark);
if (res_mark == biased_value) {
return BIAS_REVOKED;
}
}
}
}

HeuristicsResult heuristics = update_heuristics(obj(), attempt_rebias);
if (heuristics == HR_NOT_BIASED) {
return NOT_BIASED;
} else if (heuristics == HR_SINGLE_REVOKE) {
Klass *k = obj->klass();
markOop prototype_header = k->prototype_header();
if (mark->biased_locker() == THREAD &&
prototype_header->bias_epoch() == mark->bias_epoch()) {
// A thread is trying to revoke the bias of an object biased
// toward it, again likely due to an identity hash code
// computation. We can again avoid a safepoint in this case
// since we are only going to walk our own stack. There are no
// races with revocations occurring in other threads because we
// reach no safepoints in the revocation path.
// Also check the epoch because even if threads match, another thread
// can come in with a CAS to steal the bias of an object that has a
// stale epoch.
ResourceMark rm;
if (TraceBiasedLocking) {
tty->print_cr("Revoking bias by walking my own stack:");
}
BiasedLocking::Condition cond = revoke_bias(obj(), false, false, (JavaThread*) THREAD);
((JavaThread*) THREAD)->set_cached_monitor_info(NULL);
assert(cond == BIAS_REVOKED, "why not?");
return cond;
} else {
VM_RevokeBias revoke(&obj, (JavaThread*) THREAD);
VMThread::execute(&revoke);
return revoke.status_code();
}
}

assert((heuristics == HR_BULK_REVOKE) ||
(heuristics == HR_BULK_REBIAS), "?");
VM_BulkRevokeBias bulk_revoke(&obj, (JavaThread*) THREAD,
(heuristics == HR_BULK_REBIAS),
attempt_rebias);
VMThread::execute(&bulk_revoke);
return bulk_revoke.status_code();
}

第一段注释我大概理解了一下,应该是在说为什么需要偏向锁,因为竞争少,避免批量的竞争,减少开销。

判断的时候我们可以看到调用了is_biased_anonymously,这个方法最后会调用如下方法:

1
2
3
4
bool has_bias_pattern() const {
//这里biased_lock_pattern值是5,二级制101
return (mask_bits(value(), biased_lock_mask_in_place) == biased_lock_pattern);
}

那我们就知道了,is_biased_anonymously方法判断是否为可偏向状态,而mark的偏向锁标志位为1,锁标识位为01。

第二段注释,我理解了一下,大概是说,使用对象的hashCode计算,如果可以成功的将没有偏向锁的对象头与对象标记进行比较和交换,意味着没有其他线程在获取偏向锁。

说白了,如果没有竞争状态,我们就把mark中的JavaThread设置为当前线程Id,执行成功则执行代码块。

我们看一下,如果这个对象只是有一个偏向锁的头,会怎么样:

1
2
3
4
5
6
7
8
9
10
11
12
if (!prototype_header->has_bias_pattern()) {
// This object has a stale bias from before the bulk revocation
// for this data type occurred. It's pointless to update the
// heuristics at this point so simply update the header with a
// CAS. If we fail this race, the object's bias has been revoked
// by another thread so we simply return and let the caller deal
// with it.
markOop biased_value = mark;
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(prototype_header, obj->mark_addr(), mark);
assert(!(*(obj->mark_addr()))->has_bias_pattern(), "even if we raced, should still be revoked");
return BIAS_REVOKED;
}

这里是说如果撤销之前有一个过时的偏向锁,更新是没有意义的,会返回并交还给另外的线程去执行。

后面JVM会执行一段代码:HeuristicsResult heuristics = update_heuristics(obj(), attempt_rebias);,这段代码看不懂,只能再次去查了一下,笨神给出了比较详尽的解释:

update_heuristics的方法真正作用主要是当cas失败达到一定次数之后来决定是否批量偏向或者去偏向。在我们这段代码中,执行到这里有以下几种情况:

  1. 对象不是偏向模式
  2. 对象是偏向模式但是epoch过期了,如下两种情况下才会执行到这里
  • 打算重偏向,但是偏向了另外的线程
  • 打算撤销偏向

说白了,我们可能会已经一个偏向锁过期的问题,还可能存在synchronized(object)中这个object被回收的问题!这就很神奇了,按照我的理解,这个object应该不会被回收才对。也有可能是加锁过程中回收了。我找了一下Hotspot的的文档中,关于锁的部分,看到其中有这么一句话:

An epoch value in the class acts as a timestamp that indicates the validity of the bias. This value is copied into the header word upon object allocation. Bulk rebiasing can then efficiently be implemented as an increment of the epoch in the appropriate class. The next time an instance of this class is going to be locked, the code detects a different value in the header word and rebiases the object towards the current thread.

上面这段话解释了,对象存在过期的情况,标识符就是epoch。文章最后会附上一份MarkWord的存储状态图,在markOop.cpp中,有兴趣的可以看一下。

再下面的代码就不看了,我们可以在后面看到线程执行了(VMThread::execute(&bulk_revoke);),我们重新回到fast_enter中,如果我们执行失败了会怎么办,这里会撤销偏向锁,执行方法:BiasedLocking::revoke_at_safepoint(obj);,再往下,我们执行会进入到slow_enter中,进入之后我们的锁升级为轻量级锁。

偏向锁的撤销

1
2
3
4
5
6
7
8
9
10
11
12
void BiasedLocking::revoke_at_safepoint(Handle h_obj) {
assert(SafepointSynchronize::is_at_safepoint(), "must only be called while at safepoint");
oop obj = h_obj();
HeuristicsResult heuristics = update_heuristics(obj, false);
if (heuristics == HR_SINGLE_REVOKE) {
revoke_bias(obj, false, false, NULL);
} else if ((heuristics == HR_BULK_REBIAS) ||
(heuristics == HR_BULK_REVOKE)) {
bulk_revoke_or_rebias_at_safepoint(obj, (heuristics == HR_BULK_REBIAS), false, NULL);
}
clean_up_cached_monitor_info();
}

撤销偏向锁我们又看到了SafePoint,我们的撤销动作,需要等待一个安全点。这是我们需要暂停一下拥有偏向锁的线程,判断对象是否处于被锁定的状态,然后撤销锁,恢复到无锁或轻量级锁的状态(bulk_revoke_or_rebias_at_safepoint)。

小Tips:

偏向锁在Java 1.6之后是默认启用的,但在应用程序启动几秒钟之后才激活,可以使用-XX:BiasedLockingStartupDelay=0参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争状态,可以通过XX:-UseBiasedLocking=false参数关闭偏向锁,我们根据上面的代码可以知道,关闭偏向锁可以节省一部分开销,绕过很多操作直接进入到轻量级锁处理中。

感谢笨神和小狼大神的分享和解答。

附:
1
2
3
4
5
6
7
8
// Bit-format of an object header (most significant first, big endian layout below):  
//
// 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)