自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

JVM深度剖析:一文詳解JVM是如何實(shí)現(xiàn)反射的?

云計(jì)算 虛擬化
反射是 Java 語(yǔ)言中一個(gè)相當(dāng)重要的特性,它允許正在運(yùn)行的 Java 程序觀測(cè),甚至是修改程序的動(dòng)態(tài)行為。

 [[422686]]

反射是 Java 語(yǔ)言中一個(gè)相當(dāng)重要的特性,它允許正在運(yùn)行的 Java 程序觀測(cè),甚至是修改程序的動(dòng)態(tài)行為。

舉例來(lái)說(shuō),我們可以通過(guò) Class 對(duì)象枚舉該類(lèi)中的所有方法,我們還可以通過(guò)Method.setAccessible(位于 java.lang.reflect 包,該方法繼承自 AccessibleObject)繞過(guò) Java 語(yǔ)言的訪問(wèn)權(quán)限,在私有方法所在類(lèi)之外的地方調(diào)用該方法。

反射在 Java 中的應(yīng)用十分廣泛。開(kāi)發(fā)人員日常接觸到的 Java 集成開(kāi)發(fā)環(huán)境(IDE)便運(yùn)用了這一功能:每當(dāng)我們敲入點(diǎn)號(hào)時(shí),IDE 便會(huì)根據(jù)點(diǎn)號(hào)前的內(nèi)容,動(dòng)態(tài)展示可以訪問(wèn)的字段或者方法。

另一個(gè)日常應(yīng)用則是 Java 調(diào)試器,它能夠在調(diào)試過(guò)程中枚舉某一對(duì)象所有字段的值。

(圖中 eclipse 的自動(dòng)提示使用了反射)

在 Web 開(kāi)發(fā)中,我們經(jīng)常能夠接觸到各種可配置的通用框架。為了保證框架的可擴(kuò)展性,它們往往借助 Java 的反射機(jī)制,根據(jù)配置文件來(lái)加載不同的類(lèi)。舉例來(lái)說(shuō),Spring 框架的依賴(lài)反轉(zhuǎn)(IoC),便是依賴(lài)于反射機(jī)制。

然而,我相信不少開(kāi)發(fā)人員都嫌棄反射機(jī)制比較慢。甚至是甲骨文關(guān)于反射的教學(xué)網(wǎng)頁(yè)[1],也強(qiáng)調(diào)了反射性能開(kāi)銷(xiāo)大的缺點(diǎn)。

反射調(diào)用的實(shí)現(xiàn)

首先,我們來(lái)看看方法的反射調(diào)用,也就是 Method.invoke,是怎么實(shí)現(xiàn)的。

  1. public final class Method extends Executable { 
  2.     ... 
  3.     public Object invoke(Object obj, Object... args) throws ... { 
  4.         ... //權(quán)限檢查 
  5.         MethodAccessor ma = methodAccessor; 
  6.         if (ma == null) { 
  7.             ma = acquireMethodAccessor(); 
  8.         } 
  9.         return ma.invoke(obj, args); 
  10.     } 

如果你查閱 Method.invoke 的源代碼,那么你會(huì)發(fā)現(xiàn),它實(shí)際上委派給MethodAccessor 來(lái)處理。MethodAccessor 是一個(gè)接口,它有兩個(gè)已有的具體實(shí)現(xiàn):一個(gè)通過(guò)本地方法來(lái)實(shí)現(xiàn)反射調(diào)用,另一個(gè)則使用了委派模式。為了方便記憶,我便用“本地實(shí)現(xiàn)”和“委派實(shí)現(xiàn)”來(lái)指代這兩者。

每個(gè) Method 實(shí)例的第一次反射調(diào)用都會(huì)生成一個(gè)委派實(shí)現(xiàn),它所委派的具體實(shí)現(xiàn)便是一個(gè)本地實(shí)現(xiàn)。本地實(shí)現(xiàn)非常容易理解。當(dāng)進(jìn)入了 Java 虛擬機(jī)內(nèi)部之后,我們便擁有了Method 實(shí)例所指向方法的具體地址。這時(shí)候,反射調(diào)用無(wú)非就是將傳入的參數(shù)準(zhǔn)備好,然后調(diào)用進(jìn)入目標(biāo)方法。

  1. // v0版本 
  2. import java.lang.reflect.Method; 
  3. public class Test { 
  4.     public static void target(int i) { 
  5.         new Exception("#" + i).printStackTrace(); 
  6.     } 
  7.     public static void main(String[] args) throws Exception { 
  8.         Class<?> klass = Class.forName("Test"); 
  9.         Method method = klass.getMethod("target"int.class); 
  10.         method.invoke(null, 0); 
  11.     } 
  12. #不同版本的輸出略有不同,這里我使用了Java 10。 
  13. $ java Test 
  14. java.lang.Exception: #0 
  15. at Test.target(Test.java:5) 
  16. at java.base/jdk.internal.reflect.NativeMethodAccessorImpl .invoke0(Native Methoa      t java.base/jdk.internal.reflect.NativeMethodAccessorImpl. .invoke(NativeMethodAt       java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.i .invoke(Delegatin 
  17. java.base/java.lang.reflect.Method.invoke(Method.java:564) 
  18. t        Test.main(Test.java:131 

為了方便理解,我們可以打印一下反射調(diào)用到目標(biāo)方法時(shí)的棧軌跡。在上面的 v0 版本代碼中,我們獲取了一個(gè)指向 Test.target 方法的 Method 對(duì)象,并且用它來(lái)進(jìn)行反射調(diào)用。在 Test.target 中,我會(huì)打印出棧軌跡。

可以看到,反射調(diào)用先是調(diào)用了 Method.invoke,然后進(jìn)入委派實(shí)現(xiàn)(DelegatingMethodAccessorImpl),再然后進(jìn)入本地實(shí)現(xiàn)(NativeMethodAccessorImpl),最后到達(dá)目標(biāo)方法。

這里你可能會(huì)疑問(wèn),為什么反射調(diào)用還要采取委派實(shí)現(xiàn)作為中間層?直接交給本地實(shí)現(xiàn)不可以么?

其實(shí),Java 的反射調(diào)用機(jī)制還設(shè)立了另一種動(dòng)態(tài)生成字節(jié)碼的實(shí)現(xiàn)(下稱(chēng)動(dòng)態(tài)實(shí)現(xiàn)),直接使用 invoke 指令來(lái)調(diào)用目標(biāo)方法。之所以采用委派實(shí)現(xiàn),便是為了能夠在本地實(shí)現(xiàn)以及動(dòng)態(tài)實(shí)現(xiàn)中切換。

  1. //動(dòng)態(tài)實(shí)現(xiàn)的偽代碼,這里只列舉了關(guān)鍵的調(diào)用邏輯,其實(shí)它還包括調(diào)用者檢測(cè)、參數(shù)檢測(cè)的字節(jié)碼。 
  2. package jdk.internal.reflect; 
  3. public class GeneratedMethodAccessor1 extends ... { 
  4.     @Overrides 
  5.     public Object invoke(Object obj, Object[] args) throws ... { 
  6.         Test.target((int) args[0]); 
  7.         return null
  8.     } 

動(dòng)態(tài)實(shí)現(xiàn)和本地實(shí)現(xiàn)相比,其運(yùn)行效率要快上 20 倍。這是因?yàn)閯?dòng)態(tài)實(shí)現(xiàn)無(wú)需經(jīng)過(guò) Java到 C++ 再到 Java 的切換,但由于生成字節(jié)碼十分耗時(shí),僅調(diào)用一次的話,反而是本地實(shí)現(xiàn)要快上 3 到 4 倍。

考慮到許多反射調(diào)用僅會(huì)執(zhí)行一次,Java 虛擬機(jī)設(shè)置了一個(gè)閾值 15(可以通過(guò)-Dsun.reflect.inflationThreshold= 來(lái)調(diào)整),當(dāng)某個(gè)反射調(diào)用的調(diào)用次數(shù)在 15 之下時(shí),采用本地實(shí)現(xiàn);當(dāng)達(dá)到 15 時(shí),便開(kāi)始動(dòng)態(tài)生成字節(jié)碼,并將委派實(shí)現(xiàn)的委派對(duì)象切換至動(dòng)態(tài)實(shí)現(xiàn),這個(gè)過(guò)程我們稱(chēng)之為 Inflation。

為了觀察這個(gè)過(guò)程,我將剛才的例子更改為下面的 v1 版本。它會(huì)將反射調(diào)用循環(huán) 20 次。

  1. // v1版本 
  2. import java.lang.reflect.Method; 
  3. public class Test { 
  4.     public static void target(int i) { 
  5.         new Exception("#" + i).printStackTrace(); 
  6.     } 
  7.     public static void main(String[] args) throws Exception { 
  8.         Class<?> klass = Class.forName("Test"); 
  9.         Method method = klass.getMethod("target"int.class); 
  10.         for (int i = 0; i < 20; i++) { 
  11.             method.invoke(null, i); 
  12.         } 
  13.     } 
  14. #使用-verbose:class打印加載的類(lèi) 
  15. $ java -verbose:class Test 
  16. ... 
  17. java.lang.Exception: #14 
  18. at Test.target(Test.java:5) 
  19. at java.base/jdk.internal.reflect.NativeMethodAccessorImpl .invoke0(Native Methoat java.base/jdk.internal.reflect.NativeMethodAccessorImpl .invoke(NativeMethodAat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl .invoke(Delegatinat java.base/java.lang.reflect.Method.invoke(Method.java:564) 
  20. at Test.main(Test.java:12) 
  21. [0.158s][info][class,load] ... 
  22. ... 
  23. [0.160s][info][class,load] jdk.internal.reflect.GeneratedMethodAccessor1 source: __JVM_Djava.lang.Exception: #15 
  24. at Test.target(Test.java:5) 
  25. at java.base/jdk.internal.reflect.NativeMethodAccessorImpl .invoke0(Native Methodat java.base/jdk.internal.reflect.NativeMethodAccessorImpl .invoke(NativeMethodAcat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl .invoke(Delegatingat java.base/java.lang.reflect.Method.invoke(Method.java:564) 
  26. at Test.main(Test.java:12) 
  27. java.lang.Exception: #16 
  28. at Test.target(Test.java:5) 
  29. at jdk.internal.reflect.GeneratedMethodAccessor1 .invoke(Unknown Source) 
  30. at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl .invoke(Delegatingat java.base/java.lang.reflect.Method.invoke(Method.java:564) 
  31. at Test.main(Test.java:12) 
  32. ... 

可以看到,在第 15 次(從 0 開(kāi)始數(shù))反射調(diào)用時(shí),我們便觸發(fā)了動(dòng)態(tài)實(shí)現(xiàn)的生成。這時(shí)候,Java 虛擬機(jī)額外加載了不少類(lèi)。其中,最重要的當(dāng)屬GeneratedMethodAccessor1(第 30 行)。并且,從第 16 次反射調(diào)用開(kāi)始,我們便切換至這個(gè)剛剛生成的動(dòng)態(tài)實(shí)現(xiàn)(第 40 行)。

反射調(diào)用的 Inflation 機(jī)制是可以通過(guò)參數(shù)(-Dsun.reflect.noInflation=true)來(lái)關(guān)閉的。這樣一來(lái),在反射調(diào)用一開(kāi)始便會(huì)直接生成動(dòng)態(tài)實(shí)現(xiàn),而不會(huì)使用委派實(shí)現(xiàn)或者本地實(shí)現(xiàn)。

反射調(diào)用的開(kāi)銷(xiāo)

下面,我們便來(lái)拆解反射調(diào)用的性能開(kāi)銷(xiāo)。

在剛才的例子中,我們先后進(jìn)行了 Class.forName,Class.getMethod 以及Method.invoke 三個(gè)操作。其中,Class.forName 會(huì)調(diào)用本地方法,Class.getMethod則會(huì)遍歷該類(lèi)的公有方法。如果沒(méi)有匹配到,它還將遍歷父類(lèi)的公有方法??上攵?,這兩個(gè)操作都非常費(fèi)時(shí)。

值得注意的是,以 getMethod 為代表的查找方法操作,會(huì)返回查找得到結(jié)果的一份拷貝。因此,我們應(yīng)當(dāng)避免在熱點(diǎn)代碼中使用返回 Method 數(shù)組的 getMethods 或者getDeclaredMethods 方法,以減少不必要的堆空間消耗。

在實(shí)踐中,我們往往會(huì)在應(yīng)用程序中緩存 Class.forName 和 Class.getMethod 的結(jié)果。因此,下面我就只關(guān)注反射調(diào)用本身的性能開(kāi)銷(xiāo)。

為了比較直接調(diào)用和反射調(diào)用的性能差距,我將前面的例子改為下面的 v2 版本。它會(huì)將反射調(diào)用循環(huán)二十億次。此外,它還將記錄下每跑一億次的時(shí)間。

我將取最后五個(gè)記錄的平均值,作為預(yù)熱后的峰值性能。(注:這種性能評(píng)估方式并不嚴(yán)謹(jǐn),我會(huì)在專(zhuān)欄的第三部分介紹如何用 JMH 來(lái)測(cè)性能。)

在我這個(gè)老筆記本上,一億次直接調(diào)用耗費(fèi)的時(shí)間大約在 120ms。這和不調(diào)用的時(shí)間是一致的。其原因在于這段代碼屬于熱循環(huán),同樣會(huì)觸發(fā)即時(shí)編譯。并且,即時(shí)編譯會(huì)將對(duì)Test.target 的調(diào)用內(nèi)聯(lián)進(jìn)來(lái),從而消除了調(diào)用的開(kāi)銷(xiāo)。

  1. // v2版本 
  2. mport java.lang.reflect.Method; 
  3. public class Test { 
  4.     public static void target(int i) { 
  5.         //空方法 
  6.     } 
  7.     public static void main(String[] args) throws Exception { 
  8.         Class<?> klass = Class.forName("Test"); 
  9.         Method method = klass.getMethod("target"int.class); 
  10.         long current = System.currentTimeMillis(); 
  11.         for (int i = 1; i <= 2_000_000_000; i++) { 
  12.             if (i % 100_000_000 == 0) { 
  13.                 long temp = System.currentTimeMillis(); 
  14.                 System.out.println(temp - current); 
  15.                 current = temp
  16.             } 
  17.             method.invoke(null, 128); 
  18.         } 
  19.     } 

下面我將以 120ms 作為基準(zhǔn),來(lái)比較反射調(diào)用的性能開(kāi)銷(xiāo)。

由于目標(biāo)方法 Test.target 接收一個(gè) int 類(lèi)型的參數(shù),因此我傳入 128 作為反射調(diào)用的參數(shù),測(cè)得的結(jié)果約為基準(zhǔn)的 2.7 倍。我們暫且不管這個(gè)數(shù)字是高是低,先來(lái)看看在反射調(diào)用之前字節(jié)碼都做了什么。

  1. aload_2                         //加載Method對(duì)象 
  2. aconst_null                     //反射調(diào)用的第一個(gè)參數(shù)null 
  3. iconst_1 
  4. anewarray Object                //生成一個(gè)長(zhǎng)度為1的Object數(shù)組 
  5. dup 
  6. iconst_0 
  7. sipush 128 
  8. invokestatic Integer.valueOf    //將128自動(dòng)裝箱成Integer73: aastore                         //存入Object數(shù)組中 
  9. invokevirtual Method.invoke     //反射調(diào)用 

這里我截取了循環(huán)中反射調(diào)用編譯而成的字節(jié)碼??梢钥吹?,這段字節(jié)碼除了反射調(diào)用外,還額外做了兩個(gè)操作。

  • 由于 Method.invoke 是一個(gè)變長(zhǎng)參數(shù)方法,在字節(jié)碼層面它的最后一個(gè)參數(shù)會(huì)是Object 數(shù)組(感興趣的同學(xué)私下可以用 javap 查看)。Java 編譯器會(huì)在方法調(diào)用處生成一個(gè)長(zhǎng)度為傳入?yún)?shù)數(shù)量的 Object 數(shù)組,并將傳入?yún)?shù)一一存儲(chǔ)進(jìn)該數(shù)組中。
  • 由于 Object 數(shù)組不能存儲(chǔ)基本類(lèi)型,Java 編譯器會(huì)對(duì)傳入的基本類(lèi)型參數(shù)進(jìn)行自動(dòng)裝箱。

這兩個(gè)操作除了帶來(lái)性能開(kāi)銷(xiāo)外,還可能占用堆內(nèi)存,使得 GC 更加頻繁。(如果你感興趣的話,可以用虛擬機(jī)參數(shù) -XX:+PrintGC 試試。)那么,如何消除這部分開(kāi)銷(xiāo)呢?

關(guān)于第二個(gè)自動(dòng)裝箱,Java 緩存了 [-128, 127] 中所有整數(shù)所對(duì)應(yīng)的 Integer 對(duì)象。當(dāng)需要自動(dòng)裝箱的整數(shù)在這個(gè)范圍之內(nèi)時(shí),便返回緩存的 Integer,否則需要新建一個(gè) Integer對(duì)象。

因此,我們可以將這個(gè)緩存的范圍擴(kuò)大至覆蓋 128(對(duì)應(yīng)參數(shù)-Djava.lang.Integer.IntegerCache.high=128),便可以避免需要新建 Integer 對(duì)象的場(chǎng)景。

或者,我們可以在循環(huán)外緩存 128 自動(dòng)裝箱得到的 Integer 對(duì)象,并且直接傳入反射調(diào)用中。這兩種方法測(cè)得的結(jié)果差不多,約為基準(zhǔn)的 1.8 倍。

現(xiàn)在我們?cè)倩貋?lái)看看第一個(gè)因變長(zhǎng)參數(shù)而自動(dòng)生成的 Object 數(shù)組。既然每個(gè)反射調(diào)用對(duì)應(yīng)的參數(shù)個(gè)數(shù)是固定的,那么我們可以選擇在循環(huán)外新建一個(gè) Object 數(shù)組,設(shè)置好參數(shù),并直接交給反射調(diào)用。改好的代碼可以參照文稿中的 v3 版本。

  1. // v3版本 
  2. import java.lang.reflect.Method; 
  3. public class Test { 
  4.     public static void target(int i) { 
  5.         //空方法 
  6.     } 
  7.     public static void main(String[] args) throws Exception { 
  8.         Class<?> klass = Class.forName("Test"); 
  9.         Method method = klass.getMethod("target"int.class); 
  10.         Object[] arg = new Object[1]; 
  11.         //在循環(huán)外構(gòu)造參數(shù)數(shù)組 
  12.         arg[0] = 128; 
  13.         long current = System.currentTimeMillis(); 
  14.         for (int i = 1; i <= 2_000_000_000; i++) { 
  15.             if (i % 100_000_000 == 0) { 
  16.                 long temp = System.currentTimeMillis(); 
  17.                 System.out.println(temp - current); 
  18.                 current = temp
  19.             } 
  20.             method.invoke(null, arg); 
  21.         } 
  22.     } 

測(cè)得的結(jié)果反而更糟糕了,為基準(zhǔn)的 2.9 倍。這是為什么呢?

如果你在上一步解決了自動(dòng)裝箱之后查看運(yùn)行時(shí)的 GC 狀況,你會(huì)發(fā)現(xiàn)這段程序并不會(huì)觸發(fā) GC。其原因在于,原本的反射調(diào)用被內(nèi)聯(lián)了,從而使得即時(shí)編譯器中的逃逸分析將原本新建的 Object 數(shù)組判定為不逃逸的對(duì)象。

如果一個(gè)對(duì)象不逃逸,那么即時(shí)編譯器可以選擇棧分配甚至是虛擬分配,也就是不占用堆空間。具體我會(huì)在本專(zhuān)欄的第二部分詳細(xì)解釋。

如果在循環(huán)外新建數(shù)組,即時(shí)編譯器無(wú)法確定這個(gè)數(shù)組會(huì)不會(huì)中途被更改,因此無(wú)法優(yōu)化掉訪問(wèn)數(shù)組的操作,可謂是得不償失。

到目前為止,我們的最好記錄是 1.8 倍。那能不能再進(jìn)一步提升呢?

剛才我曾提到,可以關(guān)閉反射調(diào)用的 Inflation 機(jī)制,從而取消委派實(shí)現(xiàn),并且直接使用動(dòng)態(tài)實(shí)現(xiàn)。此外,每次反射調(diào)用都會(huì)檢查目標(biāo)方法的權(quán)限,而這個(gè)檢查同樣可以在 Java 代碼里關(guān)閉,在關(guān)閉了這兩項(xiàng)機(jī)制之后,也就得到了我們的 v4 版本,它測(cè)得的結(jié)果約為基準(zhǔn)的1.3 倍。

  1. // v4版本 
  2. import java.lang.reflect.Method; 
  3. //在運(yùn)行指令中添加如下兩個(gè)虛擬機(jī)參數(shù): 
  4. // -Djava.lang.Integer.IntegerCache.high=128 
  5. // -Dsun.reflect.noInflation=true 
  6. public class Test { 
  7.     public static void target(int i) { 
  8.         //空方法 
  9.     } 
  10.     public static void main(String[] args) throws Exception { 
  11.         Class<?> klass = Class.forName("Test"); 
  12.         Method method = klass.getMethod("target"int.class); 
  13.         method.setAccessible(true); 
  14.         //關(guān)閉權(quán)限檢查 
  15.         long current = System.currentTimeMillis(); 
  16.         for (int i = 1; i <= 2_000_000_000; i++) { 
  17.             if (i % 100_000_000 == 0) { 
  18.                 long temp = System.currentTimeMillis(); 
  19.                 System.out.println(temp - current); 
  20.                 current = temp
  21.             } 
  22.             method.invoke(null, 128); 
  23.         } 
  24.     } 

到這里,我們基本上把反射調(diào)用的水分都榨干了。接下來(lái),我來(lái)把反射調(diào)用的性能開(kāi)銷(xiāo)給提回去。

首先,在這個(gè)例子中,之所以反射調(diào)用能夠變得這么快,主要是因?yàn)榧磿r(shí)編譯器中的方法內(nèi)聯(lián)。在關(guān)閉了 Inflation 的情況下,內(nèi)聯(lián)的瓶頸在于 Method.invoke 方法中對(duì)MethodAccessor.invoke 方法的調(diào)用。

我會(huì)在后面的文章中介紹方法內(nèi)聯(lián)的具體實(shí)現(xiàn),這里先說(shuō)個(gè)結(jié)論:在生產(chǎn)環(huán)境中,我們往往擁有多個(gè)不同的反射調(diào)用,對(duì)應(yīng)多個(gè) GeneratedMethodAccessor,也就是動(dòng)態(tài)實(shí)現(xiàn)。

由于 Java 虛擬機(jī)的關(guān)于上述調(diào)用點(diǎn)的類(lèi)型 profile(注:對(duì)于 invokevirtual 或者invokeinterface,Java 虛擬機(jī)會(huì)記錄下調(diào)用者的具體類(lèi)型,我們稱(chēng)之為類(lèi)型 profile)無(wú)法同時(shí)記錄這么多個(gè)類(lèi),因此可能造成所測(cè)試的反射調(diào)用沒(méi)有被內(nèi)聯(lián)的情況。

  1. // v5版本 
  2. import java.lang.reflect.Method; 
  3. public class Test { 
  4.     public static void target(int i) { 
  5.         //空方法 
  6.     } 
  7.     public static void main(String[] args) throws Exception { 
  8.         Class<?> klass = Class.forName("Test"); 
  9.         Method method = klass.getMethod("target"int.class); 
  10.         method.setAccessible(true); 
  11.         //關(guān)閉權(quán)限檢查 
  12.         polluteProfile(); 
  13.         long current = System.currentTimeMillis(); 
  14.         for (int i = 1; i <= 2_000_000_000; i++) { 
  15.             if (i % 100_000_000 == 0) { 
  16.                 long temp = System.currentTimeMillis(); 
  17.                 System.out.println(temp - current); 
  18.                 current = temp
  19.             } 
  20.             method.invoke(null, 128); 
  21.         } 
  22.     } 
  23.     public static void polluteProfile() throws Exception { 
  24.         Method method1 = Test.class.getMethod("target1"int.class); 
  25.         Method method2 = Test.class.getMethod("target2"int.class); 
  26.         for (int i = 0; i < 2000; i++) { 
  27.             method1.invoke(null, 0); 
  28.             method2.invoke(null, 0); 
  29.         } 
  30.     } 
  31.     public static void target1(int i) { 
  32.     } 
  33.     public static void target2(int i) { 
  34.     } 

在上面的 v5 版本中,我在測(cè)試循環(huán)之前調(diào)用了 polluteProfile 的方法。該方法將反射調(diào)用另外兩個(gè)方法,并且循環(huán)上 2000 遍。

而測(cè)試循環(huán)則保持不變。測(cè)得的結(jié)果約為基準(zhǔn)的 6.7 倍。也就是說(shuō),只要誤擾了Method.invoke 方法的類(lèi)型 profile,性能開(kāi)銷(xiāo)便會(huì)從 1.3 倍上升至 6.7 倍。

之所以這么慢,除了沒(méi)有內(nèi)聯(lián)之外,另外一個(gè)原因是逃逸分析不再起效。這時(shí)候,我們便可以采用剛才 v3 版本中的解決方案,在循環(huán)外構(gòu)造參數(shù)數(shù)組,并直接傳遞給反射調(diào)用。這樣子測(cè)得的結(jié)果約為基準(zhǔn)的 5.2 倍。

除此之外,我們還可以提高 Java 虛擬機(jī)關(guān)于每個(gè)調(diào)用能夠記錄的類(lèi)型數(shù)目(對(duì)應(yīng)虛擬機(jī)參數(shù) -XX:TypeProfileWidth,默認(rèn)值為 2,這里設(shè)置為 3)。最終測(cè)得的結(jié)果約為基準(zhǔn)的2.8 倍,盡管它和原本的 1.3 倍還有一定的差距,但總算是比 6.7 倍好多了。

總結(jié)與實(shí)踐

在默認(rèn)情況下,方法的反射調(diào)用為委派實(shí)現(xiàn),委派給本地實(shí)現(xiàn)來(lái)進(jìn)行方法調(diào)用。在調(diào)用超過(guò)15 次之后,委派實(shí)現(xiàn)便會(huì)將委派對(duì)象切換至動(dòng)態(tài)實(shí)現(xiàn)。這個(gè)動(dòng)態(tài)實(shí)現(xiàn)的字節(jié)碼是自動(dòng)生成的,它將直接使用 invoke 指令來(lái)調(diào)用目標(biāo)方法。

方法的反射調(diào)用會(huì)帶來(lái)不少性能開(kāi)銷(xiāo),原因主要有三個(gè):變長(zhǎng)參數(shù)方法導(dǎo)致的 Object 數(shù)組,基本類(lèi)型的自動(dòng)裝箱、拆箱,還有最重要的方法內(nèi)聯(lián)。

本文的實(shí)踐環(huán)節(jié),你可以將最后一段代碼中 polluteProfile 方法的兩個(gè) Method 對(duì)象,都改成獲取名字為“target”的方法。請(qǐng)問(wèn)這兩個(gè)獲得的 Method 對(duì)象是同一個(gè)嗎(==)?他們 equal 嗎(.equals(…))?對(duì)我們的運(yùn)行結(jié)果有什么影響?

  1. import java.lang.reflect.Method; 
  2. public class Test { 
  3.     public static void target(int i) { 
  4.         //空方法 
  5.     } 
  6.     public static void main(String[] args) throws Exception { 
  7.         Class<?> klass = Class.forName("Test"); 
  8.         Method method = klass.getMethod("target"int.class); 
  9.         method.setAccessible(true); 
  10.         //關(guān)閉權(quán)限檢查 
  11.         polluteProfile(); 
  12.         long current = System.currentTimeMillis(); 
  13.         for (int i = 1; i <= 2_000_000_000; i++) { 
  14.             if (i % 100_000_000 == 0) { 
  15.                 long temp = System.currentTimeMillis(); 
  16.                 System.out.println(temp - current); 
  17.                 current = temp
  18.             } 
  19.             method.invoke(null, 128); 
  20.         } 
  21.     } 
  22.     public static void polluteProfile() throws Exception { 
  23.         Method method1 = Test.class.getMethod("target"int.class); 
  24.         Method method2 = Test.class.getMethod("target"int.class); 
  25.         for (int i = 0; i < 2000; i++) { 
  26.             method1.invoke(null, 0); 
  27.             method2.invoke(null, 0); 
  28.         } 
  29.     } 
  30.     public static void target1(int i) { 
  31.     } 
  32.     public static void target2(int i) { 
  33.     } 

 

責(zé)任編輯:武曉燕 來(lái)源: 今日頭條
相關(guān)推薦

2019-10-11 08:41:35

JVM虛擬機(jī)語(yǔ)言

2020-01-07 14:43:26

JVM類(lèi)加載器執(zhí)行引擎

2021-10-13 21:43:18

JVMRPC框架

2021-06-06 13:06:34

JVM內(nèi)存分布

2023-10-07 08:41:42

JavaJVM

2024-08-26 08:58:50

2021-09-08 17:42:45

JVM內(nèi)存模型

2023-08-27 21:29:43

JVMFullGC調(diào)優(yōu)

2021-01-27 11:10:49

JVM性能調(diào)優(yōu)

2010-09-26 14:32:34

JDKJREJVM

2020-05-20 22:28:10

JVM運(yùn)行機(jī)制

2021-08-11 10:21:24

云直播阿里云邊緣云

2018-08-16 08:19:30

2025-01-13 12:00:00

反射Java開(kāi)發(fā)

2025-04-27 09:59:38

深度學(xué)習(xí)AI人工智能

2019-12-12 11:19:33

JVM內(nèi)存線程

2022-05-26 15:44:43

混合網(wǎng)絡(luò)網(wǎng)絡(luò)安全

2019-10-28 10:19:27

JVM 類(lèi)加載器Java

2021-08-06 09:21:26

Linux內(nèi)核 Coredump

2021-08-09 16:39:52

工具JVM剖析
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)