從字節(jié)碼角度解析synchronized和反射實現(xiàn)原理
引言
前幾天,關(guān)于字節(jié)碼技術(shù),我們講了字節(jié)碼的基礎(chǔ), 常見的字節(jié)碼框架以及在軟件破解和APM鏈路監(jiān)控方面的一些應(yīng)用.
今天我們回到Java本身, 看下我們常用的synchronized關(guān)鍵字和反射在字節(jié)碼層面是如何實現(xiàn)的.
synchronized
代碼塊級別的 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 any
Java 虛擬機中代碼塊的同步是通過 monitorenter 和 monitorexit 兩個支持 synchronized 關(guān)鍵字語意的。比如上面的字節(jié)碼
- 0 ~ 5:將 lock 對象入棧,使用 dup 指令復(fù)制棧頂元素,并將它存入局部變量表位置 1 的地方,現(xiàn)在棧上還剩下一個 lock 對象
- 6:以棧頂元素 lock 做為鎖,使用 monitorenter 開始同步
- 7 ~ 8:調(diào)用 bar() 方法
- 11 ~ 12:將 lock 對象入棧,調(diào)用 monitorexit 釋放鎖
monitorenter 對操作數(shù)棧的影響如下
- 16 ~ 20:執(zhí)行異常處理,我們代碼中本來沒有 try-catch 的代碼,為什么字節(jié)碼會幫忙加上這段邏輯呢?
因為編譯器必須保證,無論同步代碼塊中的代碼以何種方式結(jié)束(正常 return 或者異常退出),代碼中每次調(diào)用 monitorenter 必須執(zhí)行對應(yīng)的 monitorexit 指令。為了保證這一點,編譯器會自動生成一個異常處理器,這個異常處理器的目的就是為了同步代碼塊拋出異常時能執(zhí)行 monitorexit。這也是字節(jié)碼中,只有一個 monitorenter 卻有兩個 monitorexit 的原因
可理解為這樣的一段 Java 代碼
public void _foo() throws Throwable { monitorenter(lock); try { bar(); } finally { monitorexit(lock); } }
根據(jù)我們之前介紹的 try-catch-finally 的字節(jié)碼實現(xiàn)原理,復(fù)制 finally 語句塊到所有可能函數(shù)退出的地方,上面的代碼等價于
public void _foo() throws Throwable { monitorenter(lock); try { bar(); monitorexit(lock); } catch (Throwable e) { monitorexit(lock); throw e; } }
方法級的 synchronized
方法級的同步與上述有所不同,它是由常量池中方法的 ACC_SYNCHRONIZED 標(biāo)志來隱式實現(xiàn)的。
synchronized public void testMe() { } 對應(yīng)字節(jié)碼 public synchronized void testMe(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED
JVM 不會使用特殊的字節(jié)碼來調(diào)用同步方法,當(dāng) JVM 解析方法的符號引用時,它會判斷方法是不是同步的(檢查方法 ACC_SYNCHRONIZED 是否被設(shè)置)。如果是,執(zhí)行線程會先嘗試獲取鎖。如果是實例方法,JVM 會嘗試獲取實例對象的鎖,如果是類方法,JVM 會嘗試獲取類鎖。在同步方法完成以后,不管是正常返回還是異常返回,都會釋放鎖.
反射
在 Java 中反射隨處可見,它底層的原也比較有意思,這篇文章來詳細(xì)介紹反射背后的原理。
先來看下面這個例子:
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); } } }
運行結(jié)果如下
可以看到同一段代碼,運行的堆棧結(jié)果與執(zhí)行次數(shù)有關(guān)系,在 0 ~ 15 次調(diào)用方式為sun.reflect.NativeMethodAccessorImpl.invoke0
,從第 16 次開始調(diào)用方式變?yōu)榱?code>sun.reflect.GeneratedMethodAccessor1.invoke。原因是什么呢?繼續(xù)往下看。
反射方法源碼分析
Method.invoke 源碼如下:
可以最終調(diào)用了MethodAccessor.invoke
方法,MethodAccessor 是一個接口
public interface MethodAccessor { public Object invoke(Object obj, Object[] args) throws IllegalArgumentException, InvocationTargetException; }
從輸出的堆??梢钥吹?MethodAccessor 的實現(xiàn)類是委托類DelegatingMethodAccessorImpl,它的 invoke 函數(shù)非常簡單,就是把調(diào)用委托給了真正的實現(xiàn)類。
class DelegatingMethodAccessorImpl extends MethodAccessorImpl { private MethodAccessorImpl delegate; public Object invoke(Object obj, Object[] args) throws IllegalArgumentException, InvocationTargetException { return delegate.invoke(obj, args); }
通過堆棧可以看到在第 0 ~ 15 次調(diào)用中,實現(xiàn)類是 NativeMethodAccessorImpl
,從第 16 次調(diào)用開始實現(xiàn)類是 GeneratedMethodAccessor1
,為什么是這樣呢?玄機就在 NativeMethodAccessorImpl 的 invoke 方法中
前 0 ~ 15 次都會調(diào)用到invoke0,這是一個 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ì)展開 native 實現(xiàn)的細(xì)節(jié)。
15 次以后會走新的邏輯,使用 GeneratedMethodAccessor1 來調(diào)用反射的方法。MethodAccessorGenerator 的作用是通過 ASM 生成新的類 sun.reflect.GeneratedMethodAccessor1
。為了查看整個類的內(nèi)容,可以使用阿里的 arthas 工具。修改上面的代碼,在 main 函數(shù)的最后加上System.in.read()
;讓 JVM 進(jìn)程不要退出。 執(zhí)行 arthas 工具中的./as.sh,會要求輸入 JVM 進(jìn)程
選擇在運行的 ReflectionTest 進(jìn)程號 7 就進(jìn)入到了 arthas 交互性界面。執(zhí)行 dump sun.reflect.GeneratedMethodAccessor1
文件就保存到了本地。
來看下這個類的字節(jié)碼
翻譯一下這個字節(jié)碼,忽略掉異常處理以后的代碼如下
public class GeneratedMethodAccessor1 extends MethodAccessorImpl { @Override public Object invoke(Object obj, Object[] args) throws IllegalArgumentException, InvocationTargetException { ReflectionTest.foo(); return null; } }
那為什么要采用 0 ~ 15 次使用 native 方式來調(diào)用,15 次以后使用 ASM 新生成的類來處理反射的調(diào)用呢?
一切都是基于性能的考慮。JNI native 調(diào)用的方式要比動態(tài)生成類調(diào)用的方式慢 20 倍,但是又由于第一次字節(jié)碼生成的過程比較慢。如果反射僅調(diào)用一次的話,采用生成字節(jié)碼的方式反而比 native 調(diào)用的方式慢 3 ~ 4 倍。
inflation 機制
因為很多情況下,反射只會調(diào)用一次,因此 JVM 想了一招,設(shè)置了 15 這個 sun.reflect.inflationThreshold
閾值,反射方法調(diào)用超過 15 次時(從 0 開始),采用 ASM 生成新的類,保證后面的調(diào)用比 native 要快。如果小于 15 次的情況下,還不如生成直接 native 來的簡單直接,還不造成額外類的生成、校驗、加載。這種方式被稱為 「inflation 機制」。inflation 這個單詞也比較有意思,它的字面意思是「膨脹;通貨膨脹」。
JVM 與 inflation 相關(guān)的屬性有兩個,一個是剛提到的閾值 sun.reflect.inflationThreshold
,還有一個是是否禁用 inflation的屬性 sun.reflect.noInflation
,默認(rèn)值為 false。如果把這個值設(shè)置成true 的話,從第 0 次開始就使用動態(tài)生成類的方式來調(diào)用反射方法了,不會使用 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 次開始就已經(jīng)沒有使用 native 方法來調(diào)用反射方法了。
小結(jié)
這篇文章主要從字節(jié)碼角度看了Java中的synchronized和射調(diào)用底層的原理,當(dāng)然還有一些其他比較有意思的語法比如lambda, switch等, 感興趣的小伙伴也可以從字節(jié)碼角度去了解一下, 相信你會有很多不一樣的收獲,更多關(guān)于字節(jié)碼解析synchronized反射的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
request如何獲取body的json數(shù)據(jù)
這篇文章主要介紹了request如何獲取body的json數(shù)據(jù)操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06SpringBoot使用Caffeine實現(xiàn)內(nèi)存緩存示例詳解
caffeine提供了四種緩存策略:分別為手動加載、自動加載、異步手動加載、異步自動加載,這篇文章主要介紹了SpringBoot使用Caffeine實現(xiàn)內(nèi)存緩存,需要的朋友可以參考下2023-06-06Java中實現(xiàn)在一個方法中調(diào)用另一個方法
下面小編就為大家分享一篇Java中實現(xiàn)在一個方法中調(diào)用另一個方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-02-02