<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          揭密 Java方法調(diào)用的底層原理

          共 10109字,需瀏覽 21分鐘

           ·

          2021-11-14 03:25

          點擊上方“程序員大白”,選擇“星標”公眾號

          重磅干貨,第一時間送達

          我們在日常開發(fā)中,其實很少去關(guān)注字節(jié)碼層面的東西。但,作為我們的吃飯家伙,個人覺得還是很有必要了解的。

          Java源碼(我們開發(fā)出來的.java結(jié)尾的文章)在運行之前都要編譯成為字節(jié)碼格式(如.class文件),然后由ClassLoader將字節(jié)碼載入運行。在字節(jié)碼文件中,指令代碼只是其中的一部分,里面還記錄了字節(jié)碼文件的編譯版本、常量池、訪問權(quán)限、所有成員變量和成員方法等信息。

          Java指令是基于棧的體系結(jié)構(gòu),大部分的指令默認的操作數(shù)在棧中。映像中ARM是基于寄存器的操作指令,而x86好像是混合寄存器和存儲器的,發(fā)現(xiàn)基于棧的操作指令確實簡單,學起來很快。

          我的理解,網(wǎng)絡(luò)是Java一個非常重要的特性,而且Java在設(shè)計之初就認為字節(jié)碼是要在網(wǎng)絡(luò)中傳輸?shù)模瑸榱藴p少網(wǎng)絡(luò)傳輸流量,字節(jié)碼就要盡量設(shè)計精簡、緊湊。因而Java增加了很多重復(fù)指令,比如盡量減少操作數(shù),因而我們會發(fā)現(xiàn)Java的很多指令都是沒有操作數(shù)的;并且指令中的操作數(shù)基本上都是當無法將值放到棧中的數(shù)據(jù),比如局部變量的索引號和常量池中的索引號。

          字節(jié)碼結(jié)構(gòu)

          基本結(jié)構(gòu)

          在開始之前,我們先簡要地介紹一下class文件的內(nèi)容。關(guān)于class 文件結(jié)構(gòu)的資料已經(jīng)非常多了,這里不再展開講解了。

          官網(wǎng):

          https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html

          大體介紹如下:

          圖片

          下面來簡單介紹一下圖中的一些關(guān)鍵字:

          magic:魔法數(shù)字,用于標識當前 class 的文件格式,JVM 可據(jù)此判斷該文件是否可以被解析,目前固定為?0xCAFEBABE

          major_version:主版本號。

          minor_version:副版本號,這兩個版本號用來標識編譯時的 JDK 版本,常見的一個異常比如?Unsupported major.minor version 52.0?就是因為運行時的 JDK 版本低于編譯時的 JDK 版本(52 是 Java 8 的主版本號)。

          constant_pool_count:常量池計數(shù)器,等于常量池中的成員數(shù)加 1。

          constant_pool:常量池,是一種表結(jié)構(gòu),包含 class 文件結(jié)構(gòu)和子結(jié)構(gòu)中引用的所有字符串常量,類或者接口名,字段名和其他常量。

          access_flags:表示某個類或者接口的訪問權(quán)限和屬性。

          this_class:類索引,該值必須是對常量池中某個常量的一個有效索引值,該索引處的成員必須是一個?CONSTANT_Class_info類型的結(jié)構(gòu)體,表示這個 class 文件所定義的類和接口。

          super_class:父類索引。

          interfaces_count:接口計數(shù)器,表示當前類或者接口直接繼承接口的數(shù)量。

          interfaces:接口表,是一個表結(jié)構(gòu),成員同 this_class,是對常量池中 CONSTANT_Class_info 類型的一個有效索引值。

          fields_count:字段計數(shù)器,當前 class 文件所有字段的數(shù)量。

          fields:字段表,是一個表結(jié)構(gòu),表中每個成員必須是 filed_info 數(shù)據(jù)結(jié)構(gòu),用于表示當前類或者接口的某個字段的完整描述,但它不包含從父類或者父接口繼承的字段。

          methods_count:方法計數(shù)器,表示當前類方法表的成員個數(shù)。

          methods:方法表,是一個表結(jié)構(gòu),表中每個成員必須是?method_info?數(shù)據(jù)結(jié)構(gòu),用于表示當前類或者接口的某個方法的完整描述。

          attributes_count:屬性計數(shù)器,表示當前 class 文件?attributes屬性表的成員個數(shù)。

          attributes:屬性表,是一個表結(jié)構(gòu),表中每個成員必須是?attribute_info數(shù)據(jù)結(jié)構(gòu),這里的屬性是對 class 文件本身,方法或者字段的補充描述,比如?SourceFile?屬性用于表示 class 文件的源代碼文件名。

          圖片

          當然,class 文件結(jié)構(gòu)的細節(jié)是非常多的,如上圖,展示了一個簡單方法的字節(jié)碼描述,可以看到真正的執(zhí)行指令在整個文件結(jié)構(gòu)中的位置。

          實際觀測

          為了避免枯燥的二進制對比分析,直接定位到真正的數(shù)據(jù)結(jié)構(gòu),這里介紹一個小工具,使用這種方式學習字節(jié)碼會節(jié)省很多時間。這個工具就是?asmtools,官網(wǎng)

          https://wiki.openjdk.java.net/display/CodeTools/asmtools

          為了方便使用,我已經(jīng)編譯了一個 jar 包,放在了倉庫里。

          執(zhí)行下面的命令,將看到類的?JCOD?語法結(jié)果。

          java?-jar?asmtools-7.0.jar?jdec?LambdaDemo.class??

          輸出的結(jié)果類似于下面的結(jié)構(gòu),它與我們上面介紹的字節(jié)碼組成是一一對應(yīng)的,對照官網(wǎng)或者資料去學習,速度飛快。若想要細挖字節(jié)碼,一定要掌握好它。

          class?LambdaDemo?{??
          ??0xCAFEBABE;??
          ??0;?//?minor?version??
          ??52;?//?version??
          ??[]?{?//?Constant?Pool??
          ????;?//?first?element?is?empty??
          ????Method?#8?#25;?//?#1??
          ????InvokeDynamic?0s?#30;?//?#2??
          ????InterfaceMethod?#31?#32;?//?#3??
          ????Field?#33?#34;?//?#4??
          ????String?#35;?//?#5??
          ????Method?#36?#37;?//?#6??
          ????class?#38;?//?#7??
          ????class?#39;?//?#8??
          ????Utf8?"";?//?#9??
          ????Utf8?"()V";?//?#10??
          ????Utf8?"Code";?//?#11??

          了解了類的文件組織方式,下面我們來看一下,類文件在加載到內(nèi)存中以后,是一個怎樣的表現(xiàn)形式。

          內(nèi)存表示

          準備以下代碼,使用javac -g InvokeDemo.java進行編譯,然后使用 java 命令執(zhí)行。程序?qū)⒆枞?sleep 函數(shù)上,我們來看一下它的內(nèi)存分布:

          interface?I?{??
          ????default?void?infMethod()?{?}??
          ??
          ????void?inf();??
          }??
          ??
          abstract?class?Abs?{??
          ????abstract?void?abs();??
          }??
          ??
          public?class?InvokeDemo?extends?Abs?implements?I?{??
          ??
          ??
          ????static?void?staticMethod()?{?}??
          ??
          ????private?void?privateMethod()?{?}??
          ??
          ????public?void?publicMethod()?{?}??
          ??
          ????@Override??
          ????public?void?inf()?{?}??
          ??
          ????@Override??
          ????void?abs()?{?}??
          ??
          ????public?static?void?main(String[]?args)?throws?Exception{??
          ????????InvokeDemo?demo?=?new?InvokeDemo();??
          ??
          ????????InvokeDemo.staticMethod();??
          ????????demo.abs();??
          ????????((Abs)?demo).abs();??
          ????????demo.inf();??
          ????????((I)?demo).inf();??
          ????????demo.privateMethod();??
          ????????demo.publicMethod();??
          ????????demo.infMethod();??
          ????????((I)?demo).infMethod();??
          ??
          ??
          ????????Thread.sleep(Integer.MAX_VAL)??
          ????}??
          }??

          為了更加明顯的看到這個過程,下面介紹一個jhsdb工具,這是在 Java 9 之后 JDK 先加入的調(diào)試工具,我們可以在命令行中使用jhsdb hsdb來啟動它。注意,要加載相應(yīng)的進程時,必須確保是同一個版本的應(yīng)用進程,否則會產(chǎn)生報錯。

          圖片

          attach 啟動 Java 進程后,可以在?Class Browser菜單中查看加載的所有類信息。我們在搜索框中輸入InvokeDemo,找到要查看的類。

          圖片

          @符號后面的,就是具體的內(nèi)存地址,我們可以復(fù)制一個,然后在?Inspector 視圖中查看具體的屬性,可以大體認為這就是類在方法區(qū)的具體存儲。

          圖片

          在?Inspector?視圖中,我們找到方法相關(guān)的屬性?_methods,可惜它無法點開,也無法查看。

          圖片

          接下來使用命令行來檢查這個數(shù)組里面的值。打開菜單中的Console,然后輸入examine?命令,可以看到這個數(shù)組里的內(nèi)容,對應(yīng)的地址就是 Class 視圖中的方法地址。

          examine?0x000000010e650570/10

          圖片

          我們可以在 Inspect 視圖中看到方法所對應(yīng)的內(nèi)存信息,這確實是一個 Method 方法的表示。

          圖片

          相比較起來,對象就簡單了,它只需要保存一個到達 Class 對象的指針即可。我們需要先從對象視圖中進入,然后找到它,一步步進入 Inspect 視圖。

          圖片

          由以上的這些分析,可以得出下面這張圖。執(zhí)行引擎想要運行某個對象的方法,需要先在棧上找到這個對象的引用,然后再通過對象的指針,找到相應(yīng)的方法字節(jié)碼。

          圖片

          方法調(diào)用指令

          關(guān)于方法的調(diào)用,Java 共提供了 5 個指令,來調(diào)用不同類型的函數(shù):

          • invokestatic?用來調(diào)用靜態(tài)方法;

          • invokevirtual?用于調(diào)用非私有實例方法,比如 public 和 protected,大多數(shù)方法調(diào)用屬于這一種;

          • invokeinterface和上面這條指令類似,不過作用于接口類;

          • invokespecial用于調(diào)用私有實例方法、構(gòu)造器及 super 關(guān)鍵字等;

          • invokedynamic用于調(diào)用動態(tài)方法。

          我們依然使用上面的代碼片段來看一下前四個指令的使用場景。代碼中包含一個接口 I、一個抽象類 Abs、一個實現(xiàn)和繼承了兩者類的InvokeDemo

          回想類加載機制,在 class 文件被加載到方法區(qū)以后,就完成了從符號引用到具體地址的轉(zhuǎn)換過程。

          我們可以看一下編譯后的 main 方法字節(jié)碼,尤其需要注意的是對于接口方法的調(diào)用。使用實例對象直接調(diào)用,和強制轉(zhuǎn)化成接口調(diào)用,所調(diào)用的字節(jié)碼指令分別是invokevirtualinvokeinterface,它們是有所不同的。

          public?static?void?main(java.lang.String[]);??
          ????descriptor:?([Ljava/lang/String;)V??
          ????flags:?ACC_PUBLIC,?ACC_STATIC??
          ????Code:??
          ??????stack=2,?locals=2,?args_size=1??
          ?????????0:?new???????????#2??????????????????//?class?InvokeDemo??
          ?????????3:?dup??
          ?????????4:?invokespecial?#3??????????????????//?Method?"":()V??
          ?????????7:?astore_1??
          ?????????8:?invokestatic??#4??????????????????//?Method?staticMethod:()V??
          ????????11:?aload_1??
          ????????12:?invokevirtual?#5??????????????????//?Method?abs:()V??
          ????????15:?aload_1??
          ????????16:?invokevirtual?#6??????????????????//?Method?Abs.abs:()V??
          ????????19:?aload_1??
          ????????20:?invokevirtual?#7??????????????????//?Method?inf:()V??
          ????????23:?aload_1??
          ????????24:?invokeinterface?#8,??1????????????//?InterfaceMethod?I.inf:()V??
          ????????29:?aload_1??
          ????????30:?invokespecial?#9??????????????????//?Method?privateMethod:()V??
          ????????33:?aload_1??
          ????????34:?invokevirtual?#10?????????????????//?Method?publicMethod:()V??
          ????????37:?aload_1??
          ????????38:?invokevirtual?#11?????????????????//?Method?infMethod:()V??
          ????????41:?aload_1??
          ????????42:?invokeinterface?#12,??1???????????//?InterfaceMethod?I.infMethod:()V??
          ????????47:?return??

          另外還有一點,和我們想象中的不同,大多數(shù)普通方法調(diào)用,使用的是?invokevirtual指令,它其實和invokeinterface是一類的,都屬于虛方法調(diào)用。很多時候,JVM 需要根據(jù)調(diào)用者的動態(tài)類型,來確定調(diào)用的目標方法,這就是動態(tài)綁定的過程。

          invokevirtual指令有多態(tài)查找的機制,該指令運行時,解析過程如下:

          • 找到操作數(shù)棧頂?shù)牡谝粋€元素所指向的對象實際類型,記做 c;

          • 如果在類型 c 中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問權(quán)限校驗,如果通過則返回這個方法直接引用,查找過程結(jié)束,不通過則返回?java.lang.IllegalAccessError

          • 否則,按照繼承關(guān)系從下往上依次對 c 的各個父類進行第二步的搜索和驗證過程;

          • 如果始終沒找到合適的方法,則拋出?java.lang.AbstractMethodError異常,這就是 Java 語言中方法重寫的本質(zhì)。

          相對比,invokestatic?指令加上invokespecial指令,就屬于靜態(tài)綁定過程。

          所以靜態(tài)綁定,指的是能夠直接識別目標方法的情況,而動態(tài)綁定指的是需要在運行過程中根據(jù)調(diào)用者的類型來確定目標方法的情況。

          可以想象,相對于靜態(tài)綁定的方法調(diào)用來說,動態(tài)綁定的調(diào)用會更加耗時一些。由于方法的調(diào)用非常的頻繁,JVM 對動態(tài)調(diào)用的代碼進行了比較多的優(yōu)化,比如使用方法表來加快對具體方法的尋址,以及使用更快的緩沖區(qū)來直接尋址( 內(nèi)聯(lián)緩存)。

          invokedynamic

          有時候在寫一些Python?腳本或者JS 腳本時,特別羨慕這些動態(tài)語言。如果把查找目標方法的決定權(quán),從虛擬機轉(zhuǎn)嫁給用戶代碼,我們就會有更高的自由度。

          之所以單獨把?invokedynamic抽離出來介紹,是因為它比較復(fù)雜。和反射類似,它用于一些動態(tài)的調(diào)用場景,但它和反射有著本質(zhì)的不同,效率也比反射要高得多。

          這個指令通常在?Lambda語法中出現(xiàn),我們來看一下一小段代碼:

          public?class?LambdaDemo?{??
          ????public?static?void?main(String[]?args)?{??
          ????????Runnable?r?=?()?->?System.out.println("Hello?Lambda");??
          ????????r.run();??
          ????}??
          }??

          使用javap -p -v命令可以在 main 方法中看到invokedynamic指令:

          public?static?void?main(java.lang.String[]);??
          ????descriptor:?([Ljava/lang/String;)V??
          ????flags:?ACC_PUBLIC,?ACC_STATIC??
          ????Code:??
          ??????stack=1,?locals=2,?args_size=1??
          ?????????0:?invokedynamic?#2,??0??????????????//?InvokeDynamic?#0:run:()Ljava/lang/Runnable;??
          ?????????5:?astore_1??
          ?????????6:?aload_1??
          ?????????7:?invokeinterface?#3,??1????????????//?InterfaceMethod?java/lang/Runnable.run:()V??
          ????????12:?return??

          另外,我們在javap?的輸出中找到了一些奇怪的東西:

          BootstrapMethods:??
          ??0:?#27?invokestatic?java/lang/invoke/LambdaMetafactory.metafactory:??
          ??(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang??
          ??/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/??
          ??MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;??
          ????Method?arguments:??
          ??????#28?()V??
          ??????#29?invokestatic?LambdaDemo.lambda$main$0:()V??
          ??????#28?()V??

          BootstrapMethods屬性在 Java 1.7 以后才有,位于類文件的屬性列表中,這個屬性用于保存invokedynamic指令引用的引導方法限定符。

          和上面介紹的四個指令不同,invokedynamic并沒有確切的接受對象,取而代之的,是一個叫CallSite的對象。

          static?CallSite?bootstrap(MethodHandles.Lookup?caller,?String?name,?MethodType?type);??

          其實,invokedynamic?指令的底層,是使用方法句柄(MethodHandle)來實現(xiàn)的。方法句柄是一個能夠被執(zhí)行的引用,它可以指向靜態(tài)方法和實例方法,以及虛構(gòu)的 get 和 set 方法,從?IDEA?中可以看到這些函數(shù)。

          句柄類型MethodType)是我們對方法的具體描述,配合方法名稱,能夠定位到一類函數(shù)。訪問方法句柄和調(diào)用原來的指令基本一致,但它的調(diào)用異常,包括一些權(quán)限檢查,在運行時才能被發(fā)現(xiàn)。

          下面這段代碼,可以完成一些動態(tài)語言的特性,通過方法名稱和傳入的對象主體,進行不同的調(diào)用,而 Bike 和 Man 類,可以沒有任何關(guān)系。

          import?java.lang.invoke.MethodHandle;??
          import?java.lang.invoke.MethodHandles;??
          import?java.lang.invoke.MethodType;??
          ??
          public?class?MethodHandleDemo?{??
          ????static?class?Bike?{??
          ????????String?sound()?{??
          ????????????return?"ding?ding";??
          ????????}??
          ????}??
          ??
          ????static?class?Animal?{??
          ????????String?sound()?{??
          ????????????return?"wow?wow";??
          ????????}??
          ????}??
          ??
          ??
          ????static?class?Man?extends?Animal?{??
          ????????@Override??
          ????????String?sound()?{??
          ????????????return?"hou?hou";??
          ????????}??
          ????}??
          ??
          ??
          ????String?sound(Object?o)?throws?Throwable?{??
          ????????MethodHandles.Lookup?lookup?=?MethodHandles.lookup();??
          ????????MethodType?methodType?=?MethodType.methodType(String.class);??
          ????????MethodHandle?methodHandle?=?lookup.findVirtual(o.getClass(),?"sound",?methodType);??
          ??
          ????????String?obj?=?(String)?methodHandle.invoke(o);??
          ????????return?obj;??
          ????}??
          ??
          ????public?static?void?main(String[]?args)?throws?Throwable?{??
          ????????String?str?=?new?MethodHandleDemo().sound(new?Bike());??
          ????????System.out.println(str);??
          ????????str?=?new?MethodHandleDemo().sound(new?Animal());??
          ????????System.out.println(str);??
          ????????str?=?new?MethodHandleDemo().sound(new?Man());??
          ????????System.out.println(str);??
          ????}??
          }??

          可以看到Lambda語言實際上是通過方法句柄來完成的,在調(diào)用鏈上自然也多了一些調(diào)用步驟,那么在性能上,是否就意味著 Lambda 性能低呢?對于大部分“非捕獲”的 Lambda 表達式來說,JIT 編譯器逃逸分析能夠優(yōu)化這部分差異,性能和傳統(tǒng)方式無異;但對于“捕獲型”的表達式來說,則需要通過方法句柄,不斷地生成適配器,性能自然就低了很多(不過和便捷性相比,一丁點性能損失是可接受的)。

          除了Lambda表達式,我們還沒有其他的方式來產(chǎn)生invokedynamic指令。但可以使用一些外部的字節(jié)碼修改工具,比如ASM,來生成一些帶有這個指令的字節(jié)碼,這通常能夠完成一些非常酷的功能,比如完成一門弱類型檢查的?JVM-Base語言。

          總結(jié)

          從 Java 字節(jié)碼的頂層結(jié)構(gòu)介紹開始,通過一個實際代碼,了解了類加載以后,在?JVM內(nèi)存里的表現(xiàn)形式,并學習了?jhsdb?對 Java 進程的觀測方式。

          關(guān)于每個字節(jié)代碼的含義,我建議給大家推薦一個已經(jīng)翻譯好的字節(jié)碼對應(yīng)表:

          圖片

          字節(jié)碼指令有200來個,所以這里就不一一貼出來了。如果想了解更多字節(jié)碼相關(guān)文章,我給大家找了一個博客:

          http://www.blogjava.net/DLevin/category/48888.html

          建議自己寫一個簡單Java代碼,然后通過文章提到的命令,在結(jié)合我給大家推薦博客,看起來你會覺得很爽。

          13個你一定要知道的PyTorch特性

          解讀:為什么要做特征歸一化/標準化?

          一文搞懂 PyTorch 內(nèi)部機制

          張一鳴:每個逆襲的年輕人,都具備的底層能力


          關(guān)


          西質(zhì)結(jié)關(guān)[]


          瀏覽 165
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲第一视频在线观看 | 人人操在线观看 | 国产视频三区 | 日韩日屄视频在线免费观看 | 日韩欧美三级 |