從字節(jié)碼角度解析synchronized和反射實(shí)現(xiàn)原理
引言
前幾天,關(guān)于字節(jié)碼技術(shù),我們講了字節(jié)碼的基礎(chǔ), 常見(jiàn)的字節(jié)碼框架以及在軟件破解和APM鏈路監(jiān)控方面的一些應(yīng)用.
今天我們回到Java本身, 看下我們常用的synchronized關(guān)鍵字和反射在字節(jié)碼層面是如何實(shí)現(xiàn)的.
synchronized
代碼塊級(jí)別的 synchronized
如下方法的內(nèi)部使用了synchronized關(guān)鍵字
private Object lock = new Object();
public void foo() {
synchronized (lock) {
bar();
}
}
public void bar() { }編譯成字節(jié)碼如下
public void foo();
Code:
0: aload_0
1: getfield #3 // Field lock:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: aload_0
8: invokevirtual #4 // Method bar:()V
11: aload_1
12: monitorexit
13: goto 21
16: astore_2
17: aload_1
18: monitorexit
19: aload_2
20: athrow
21: return
Exception table:
from to target type
7 13 16 any
16 19 16 anyJava 虛擬機(jī)中代碼塊的同步是通過(guò) monitorenter 和 monitorexit 兩個(gè)支持 synchronized 關(guān)鍵字語(yǔ)意的。比如上面的字節(jié)碼
- 0 ~ 5:將 lock 對(duì)象入棧,使用 dup 指令復(fù)制棧頂元素,并將它存入局部變量表位置 1 的地方,現(xiàn)在棧上還剩下一個(gè) lock 對(duì)象
- 6:以棧頂元素 lock 做為鎖,使用 monitorenter 開(kāi)始同步
- 7 ~ 8:調(diào)用 bar() 方法
- 11 ~ 12:將 lock 對(duì)象入棧,調(diào)用 monitorexit 釋放鎖
monitorenter 對(duì)操作數(shù)棧的影響如下

- 16 ~ 20:執(zhí)行異常處理,我們代碼中本來(lái)沒(méi)有 try-catch 的代碼,為什么字節(jié)碼會(huì)幫忙加上這段邏輯呢?
因?yàn)榫幾g器必須保證,無(wú)論同步代碼塊中的代碼以何種方式結(jié)束(正常 return 或者異常退出),代碼中每次調(diào)用 monitorenter 必須執(zhí)行對(duì)應(yīng)的 monitorexit 指令。為了保證這一點(diǎn),編譯器會(huì)自動(dòng)生成一個(gè)異常處理器,這個(gè)異常處理器的目的就是為了同步代碼塊拋出異常時(shí)能執(zhí)行 monitorexit。這也是字節(jié)碼中,只有一個(gè) monitorenter 卻有兩個(gè) monitorexit 的原因
可理解為這樣的一段 Java 代碼
public void _foo() throws Throwable {
monitorenter(lock);
try {
bar();
} finally {
monitorexit(lock);
}
}根據(jù)我們之前介紹的 try-catch-finally 的字節(jié)碼實(shí)現(xiàn)原理,復(fù)制 finally 語(yǔ)句塊到所有可能函數(shù)退出的地方,上面的代碼等價(jià)于
public void _foo() throws Throwable {
monitorenter(lock);
try {
bar();
monitorexit(lock);
} catch (Throwable e) {
monitorexit(lock);
throw e;
}
}方法級(jí)的 synchronized
方法級(jí)的同步與上述有所不同,它是由常量池中方法的 ACC_SYNCHRONIZED 標(biāo)志來(lái)隱式實(shí)現(xiàn)的。
synchronized public void testMe() {
}
對(duì)應(yīng)字節(jié)碼
public synchronized void testMe();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZEDJVM 不會(huì)使用特殊的字節(jié)碼來(lái)調(diào)用同步方法,當(dāng) JVM 解析方法的符號(hào)引用時(shí),它會(huì)判斷方法是不是同步的(檢查方法 ACC_SYNCHRONIZED 是否被設(shè)置)。如果是,執(zhí)行線(xiàn)程會(huì)先嘗試獲取鎖。如果是實(shí)例方法,JVM 會(huì)嘗試獲取實(shí)例對(duì)象的鎖,如果是類(lèi)方法,JVM 會(huì)嘗試獲取類(lèi)鎖。在同步方法完成以后,不管是正常返回還是異常返回,都會(huì)釋放鎖.
反射
在 Java 中反射隨處可見(jiàn),它底層的原也比較有意思,這篇文章來(lái)詳細(xì)介紹反射背后的原理。
先來(lái)看下面這個(gè)例子:
public class ReflectionTest {
private static int count = 0;
public static void foo() {
new Exception("test#" + (count++)).printStackTrace();
}
public static void main(String[] args) throws Exception {
Class<?> clz = Class.forName("ReflectionTest");
Method method = clz.getMethod("foo");
for (int i = 0; i < 20; i++) {
method.invoke(null);
}
}
}運(yùn)行結(jié)果如下

可以看到同一段代碼,運(yùn)行的堆棧結(jié)果與執(zhí)行次數(shù)有關(guān)系,在 0 ~ 15 次調(diào)用方式為sun.reflect.NativeMethodAccessorImpl.invoke0,從第 16 次開(kāi)始調(diào)用方式變?yōu)榱?code>sun.reflect.GeneratedMethodAccessor1.invoke。原因是什么呢?繼續(xù)往下看。
反射方法源碼分析
Method.invoke 源碼如下:

可以最終調(diào)用了MethodAccessor.invoke方法,MethodAccessor 是一個(gè)接口
public interface MethodAccessor {
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException;
}從輸出的堆??梢钥吹?MethodAccessor 的實(shí)現(xiàn)類(lèi)是委托類(lèi)DelegatingMethodAccessorImpl,它的 invoke 函數(shù)非常簡(jiǎn)單,就是把調(diào)用委托給了真正的實(shí)現(xiàn)類(lèi)。
class DelegatingMethodAccessorImpl extends MethodAccessorImpl {
private MethodAccessorImpl delegate;
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
return delegate.invoke(obj, args);
}通過(guò)堆??梢钥吹皆诘?0 ~ 15 次調(diào)用中,實(shí)現(xiàn)類(lèi)是 NativeMethodAccessorImpl,從第 16 次調(diào)用開(kāi)始實(shí)現(xiàn)類(lèi)是 GeneratedMethodAccessor1,為什么是這樣呢?玄機(jī)就在 NativeMethodAccessorImpl 的 invoke 方法中

前 0 ~ 15 次都會(huì)調(diào)用到invoke0,這是一個(gè) native 的函數(shù)。
private static native Object invoke0(Method m, Object obj, Object[] args);
有興趣的同學(xué)可以去看一下 Hotspot 的源碼,依次跟蹤下面的代碼和函數(shù):
./jdk/src/share/native/sun/reflect/NativeAccessors.c JNIEXPORT jobject JNICALL Java_sun_reflect_NativeMethodAccessorImpl_invoke0 (JNIEnv *env, jclass unused, jobject m, jobject obj, jobjectArray args) ./hotspot/src/share/vm/prims/jvm.cpp JVM_ENTRY(jobject, JVM_InvokeMethod(JNIEnv *env, jobject method, jobject obj, jobjectArray args0)) ./hotspot/src/share/vm/runtime/reflection.cpp oop Reflection::invoke_method(oop method_mirror, Handle receiver, objArrayHandle args, TRAPS)
這里不詳細(xì)展開(kāi) native 實(shí)現(xiàn)的細(xì)節(jié)。
15 次以后會(huì)走新的邏輯,使用 GeneratedMethodAccessor1 來(lái)調(diào)用反射的方法。MethodAccessorGenerator 的作用是通過(guò) ASM 生成新的類(lèi) sun.reflect.GeneratedMethodAccessor1。為了查看整個(gè)類(lèi)的內(nèi)容,可以使用阿里的 arthas 工具。修改上面的代碼,在 main 函數(shù)的最后加上System.in.read();讓 JVM 進(jìn)程不要退出。 執(zhí)行 arthas 工具中的./as.sh,會(huì)要求輸入 JVM 進(jìn)程

選擇在運(yùn)行的 ReflectionTest 進(jìn)程號(hào) 7 就進(jìn)入到了 arthas 交互性界面。執(zhí)行 dump sun.reflect.GeneratedMethodAccessor1文件就保存到了本地。

來(lái)看下這個(gè)類(lèi)的字節(jié)碼

翻譯一下這個(gè)字節(jié)碼,忽略掉異常處理以后的代碼如下
public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
@Override
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException {
ReflectionTest.foo();
return null;
}
}那為什么要采用 0 ~ 15 次使用 native 方式來(lái)調(diào)用,15 次以后使用 ASM 新生成的類(lèi)來(lái)處理反射的調(diào)用呢?
一切都是基于性能的考慮。JNI native 調(diào)用的方式要比動(dòng)態(tài)生成類(lèi)調(diào)用的方式慢 20 倍,但是又由于第一次字節(jié)碼生成的過(guò)程比較慢。如果反射僅調(diào)用一次的話(huà),采用生成字節(jié)碼的方式反而比 native 調(diào)用的方式慢 3 ~ 4 倍。
inflation 機(jī)制
因?yàn)楹芏嗲闆r下,反射只會(huì)調(diào)用一次,因此 JVM 想了一招,設(shè)置了 15 這個(gè) sun.reflect.inflationThreshold 閾值,反射方法調(diào)用超過(guò) 15 次時(shí)(從 0 開(kāi)始),采用 ASM 生成新的類(lèi),保證后面的調(diào)用比 native 要快。如果小于 15 次的情況下,還不如生成直接 native 來(lái)的簡(jiǎn)單直接,還不造成額外類(lèi)的生成、校驗(yàn)、加載。這種方式被稱(chēng)為 「inflation 機(jī)制」。inflation 這個(gè)單詞也比較有意思,它的字面意思是「膨脹;通貨膨脹」。
JVM 與 inflation 相關(guān)的屬性有兩個(gè),一個(gè)是剛提到的閾值 sun.reflect.inflationThreshold,還有一個(gè)是是否禁用 inflation的屬性 sun.reflect.noInflation,默認(rèn)值為 false。如果把這個(gè)值設(shè)置成true 的話(huà),從第 0 次開(kāi)始就使用動(dòng)態(tài)生成類(lèi)的方式來(lái)調(diào)用反射方法了,不會(huì)使用 native 的方式。
增加 noInflation 屬性重新執(zhí)行上述 Java 代碼
java -cp . -Dsun.reflect.noInflation=true ReflectionTest
輸出結(jié)果為
java.lang.Exception: test#0
at ReflectionTest.foo(ReflectionTest.java:10)
at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Method.java:497)
at ReflectionTest.main(ReflectionTest.java:18)
java.lang.Exception: test#1
at ReflectionTest.foo(ReflectionTest.java:10)
at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Method.java:497)
at ReflectionTest.main(ReflectionTest.java:18)
可以看到,從第 0 次開(kāi)始就已經(jīng)沒(méi)有使用 native 方法來(lái)調(diào)用反射方法了。
小結(jié)
這篇文章主要從字節(jié)碼角度看了Java中的synchronized和射調(diào)用底層的原理,當(dāng)然還有一些其他比較有意思的語(yǔ)法比如lambda, switch等, 感興趣的小伙伴也可以從字節(jié)碼角度去了解一下, 相信你會(huì)有很多不一樣的收獲,更多關(guān)于字節(jié)碼解析synchronized反射的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
解決JPA @OneToMany及懶加載無(wú)效的問(wèn)題
這篇文章主要介紹了解決JPA @OneToMany及懶加載無(wú)效的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10
request如何獲取body的json數(shù)據(jù)
這篇文章主要介紹了request如何獲取body的json數(shù)據(jù)操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06
SpringBoot使用Caffeine實(shí)現(xiàn)內(nèi)存緩存示例詳解
caffeine提供了四種緩存策略:分別為手動(dòng)加載、自動(dòng)加載、異步手動(dòng)加載、異步自動(dòng)加載,這篇文章主要介紹了SpringBoot使用Caffeine實(shí)現(xiàn)內(nèi)存緩存,需要的朋友可以參考下2023-06-06
Java中實(shí)現(xiàn)在一個(gè)方法中調(diào)用另一個(gè)方法
下面小編就為大家分享一篇Java中實(shí)現(xiàn)在一個(gè)方法中調(diào)用另一個(gè)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-02-02
Java ZooKeeper分布式鎖實(shí)現(xiàn)圖解
ZooKeeper是一個(gè)分布式的,開(kāi)放源碼的分布式應(yīng)用程序協(xié)調(diào)服務(wù),是Google的Chubby一個(gè)開(kāi)源的實(shí)現(xiàn),是Hadoop和Hbase的重要組件。它是一個(gè)為分布式應(yīng)用提供一致性服務(wù)的軟件,提供的功能包括:配置維護(hù)、域名服務(wù)、分布式同步、組服務(wù)等2022-03-03

