本課時我們主要分析從字節碼看方法調用的底層實現。
#### 字節碼結構
#### 基本結構
在開始之前,我們先簡要地介紹一下 class 文件的內容,這個結構和我們前面使用的 jclasslib 是一樣的。關于 class 文件結構的資料已經非常多了(點擊這里可查看官網詳細介紹),這里不再展開講解了,大體介紹如下。

* magic:魔數,用于標識當前 class 的文件格式,JVM 可據此判斷該文件是否可以被解析,目前固定為 0xCAFEBABE。
* major_version:主版本號。
* minor_version:副版本號,這兩個版本號用來標識編譯時的 JDK 版本,常見的一個異常比如 Unsupported major.minor version 52.0 就是因為運行時的 JDK 版本低于編譯時的 JDK 版本(52 是 Java 8 的主版本號)。
* constant_pool_count:常量池計數器,等于常量池中的成員數加 1。
* constant_pool:常量池,是一種表結構,包含 class 文件結構和子結構中引用的所有字符串常量,類或者接口名,字段名和其他常量。
* access_flags:表示某個類或者接口的訪問權限和屬性。
* this_class:類索引,該值必須是對常量池中某個常量的一個有效索引值,該索引處的成員必須是一個 CONSTANT_Class_info 類型的結構體,表示這個 class 文件所定義的類和接口。
* super_class:父類索引。
* interfaces_count:接口計數器,表示當前類或者接口直接繼承接口的數量。
* interfaces:接口表,是一個表結構,成員同 this_class,是對常量池中 CONSTANT_Class_info 類型的一個有效索引值。
* fields_count:字段計數器,當前 class 文件所有字段的數量。
* fields:字段表,是一個表結構,表中每個成員必須是 filed_info 數據結構,用于表示當前類或者接口的某個字段的完整描述,但它不包含從父類或者父接口繼承的字段。
* methods_count:方法計數器,表示當前類方法表的成員個數。
* methods:方法表,是一個表結構,表中每個成員必須是 method_info 數據結構,用于表示當前類或者接口的某個方法的完整描述。
* attributes_count:屬性計數器,表示當前 class 文件 attributes 屬性表的成員個數。
* attributes:屬性表,是一個表結構,表中每個成員必須是 attribute_info 數據結構,這里的屬性是對 class 文件本身,方法或者字段的補充描述,比如 SourceFile 屬性用于表示 class 文件的源代碼文件名。

當然,class 文件結構的細節是非常多的,如上圖,展示了一個簡單方法的字節碼描述,可以看到真正的執行指令在整個文件結構中的位置。
#### 實際觀測
為了避免枯燥的二進制對比分析,直接定位到真正的數據結構,這里介紹一個小工具,使用這種方式學習字節碼會節省很多時間。這個工具就是 asmtools,為了方便使用,我已經編譯了一個 jar 包,放在了倉庫里。
執行下面的命令,將看到類的 JCOD 語法結果。
```
java?-jar?asmtools-7.0.jar?jdec?LambdaDemo.class
```
輸出的結果類似于下面的結構,它與我們上面介紹的字節碼組成是一一對應的,對照官網或者資料去學習,速度飛快。若想要細挖字節碼,一定要掌握好它。
```
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?"<init>";?//?#9
????Utf8?"()V";?//?#10
????Utf8?"Code";?//?#11
```
了解了類的文件組織方式,下面我們來看一下,類文件在加載到內存中以后,是一個怎樣的表現形式。
#### 內存表示
準備以下代碼,使用 javac -g InvokeDemo.java 進行編譯,然后使用 java 命令執行。程序將阻塞在 sleep 函數上,我們來看一下它的內存分布:
```
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 先加入的調試工具,我們可以在命令行中使用 jhsdb hsdb 來啟動它。注意,要加載相應的進程時,必須確保是同一個版本的應用進程,否則會產生報錯。

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

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

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

接下來使用命令行來檢查這個數組里面的值。打開菜單中的 Console,然后輸入 examine 命令,可以看到這個數組里的內容,對應的地址就是 Class 視圖中的方法地址。
```
examine?0x000000010e650570/10
```

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

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

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

#### 方法調用指令
關于方法的調用,Java 共提供了 5 個指令,來調用不同類型的函數:
* invokestatic ?用來調用靜態方法;
* invokevirtual ?用于調用非私有實例方法,比如 public 和 protected,大多數方法調用屬于這一種;
* invokeinterface 和上面這條指令類似,不過作用于接口類;
* invokespecial 用于調用私有實例方法、構造器及 super 關鍵字等;
* invokedynamic 用于調用動態方法。
我們依然使用上面的代碼片段來看一下前四個指令的使用場景。代碼中包含一個接口 I、一個抽象類 Abs、一個實現和繼承了兩者類的 InvokeDemo。
回想一下,第 03 課時講到的類加載機制,在 class 文件被加載到方法區以后,就完成了從符號引用到具體地址的轉換過程。
我們可以看一下編譯后的 main 方法字節碼,尤其需要注意的是對于接口方法的調用。使用實例對象直接調用,和強制轉化成接口調用,所調用的字節碼指令分別是 invokevirtual 和 invokeinterface,它們是有所不同的。
```
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?"<init>":()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
```
另外還有一點,和我們想象中的不同,大多數普通方法調用,使用的是 invokevirtual 指令,它其實和 invokeinterface 是一類的,都屬于虛方法調用。很多時候,JVM 需要根據調用者的動態類型,來確定調用的目標方法,這就是動態綁定的過程。
invokevirtual 指令有多態查找的機制,該指令運行時,解析過程如下:
* 找到操作數棧頂的第一個元素所指向的對象實際類型,記做 c;
* 如果在類型 c 中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問權限校驗,如果通過則返回這個方法直接引用,查找過程結束,不通過則返回 java.lang.IllegalAccessError;
* 否則,按照繼承關系從下往上依次對 c 的各個父類進行第二步的搜索和驗證過程;
* 如果始終沒找到合適的方法,則拋出 java.lang.AbstractMethodError 異常,這就是 Java 語言中方法重寫的本質。
相對比,invokestatic 指令加上 invokespecial 指令,就屬于靜態綁定過程。
所以靜態綁定,指的是能夠直接識別目標方法的情況,而動態綁定指的是需要在運行過程中根據調用者的類型來確定目標方法的情況。
可以想象,相對于靜態綁定的方法調用來說,動態綁定的調用會更加耗時一些。由于方法的調用非常的頻繁,JVM 對動態調用的代碼進行了比較多的優化,比如使用方法表來加快對具體方法的尋址,以及使用更快的緩沖區來直接尋址( 內聯緩存)。
* [ ] invokedynamic
有時候在寫一些 Python 腳本或者JS 腳本時,特別羨慕這些動態語言。如果把查找目標方法的決定權,從虛擬機轉嫁給用戶代碼,我們就會有更高的自由度。
之所以單獨把 invokedynamic 抽離出來介紹,是因為它比較復雜。和反射類似,它用于一些動態的調用場景,但它和反射有著本質的不同,效率也比反射要高得多。
這個指令通常在 Lambda 語法中出現,我們來看一下一小段代碼:
```
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)來實現的。方法句柄是一個能夠被執行的引用,它可以指向靜態方法和實例方法,以及虛構的 get 和 set 方法,從 IDE 中可以看到這些函數。
句柄類型(MethodType)是我們對方法的具體描述,配合方法名稱,能夠定位到一類函數。訪問方法句柄和調用原來的指令基本一致,但它的調用異常,包括一些權限檢查,在運行時才能被發現。
下面這段代碼,可以完成一些動態語言的特性,通過方法名稱和傳入的對象主體,進行不同的調用,而 Bike 和 Man 類,可以沒有任何關系。
```
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 語言實際上是通過方法句柄來完成的,在調用鏈上自然也多了一些調用步驟,那么在性能上,是否就意味著 Lambda 性能低呢?對于大部分“非捕獲”的 Lambda 表達式來說,JIT 編譯器的逃逸分析能夠優化這部分差異,性能和傳統方式無異;但對于“捕獲型”的表達式來說,則需要通過方法句柄,不斷地生成適配器,性能自然就低了很多(不過和便捷性相比,一丁點性能損失是可接受的)。
除了 Lambda 表達式,我們還沒有其他的方式來產生 invokedynamic 指令。但可以使用一些外部的字節碼修改工具,比如 ASM,來生成一些帶有這個指令的字節碼,這通常能夠完成一些非常酷的功能,比如完成一門弱類型檢查的 JVM-Base 語言。
#### 小結
本課時從 Java 字節碼的頂層結構介紹開始,通過一個實際代碼,了解了類加載以后,在 JVM 內存里的表現形式,并學習了 jhsdb 對 Java 進程的觀測方式。
接下來,我們分析了 invokestatic、invokevirtual、invokeinterface、invokespecial 這四個字節碼指令的使用場景,并從字節碼中看到了這些區別。
最后,了解了 Java 7 之后的 invokedynamic 指令,它實際上是通過方法句柄來實現的。和我們關系最大的就是 Lambda 語法,了解了這些原理,可以忽略那些對 Lambda 性能高低的爭論,要盡量寫一些“非捕獲”的 Lambda 表達式。
- 前言
- 開篇詞
- 基礎原理
- 第01講:一探究竟:為什么需要 JVM?它處在什么位置?
- 第02講:大廠面試題:你不得不掌握的 JVM 內存管理
- 第03講:大廠面試題:從覆蓋 JDK 的類開始掌握類的加載機制
- 第04講:動手實踐:從棧幀看字節碼是如何在 JVM 中進行流轉的
- 垃圾回收
- 第05講:大廠面試題:得心應手應對 OOM 的疑難雜癥
- 第06講:深入剖析:垃圾回收你真的了解嗎?(上)
- 第06講:深入剖析:垃圾回收你真的了解嗎?(下)
- 第07講:大廠面試題:有了 G1 還需要其他垃圾回收器嗎?
- 第08講:案例實戰:億級流量高并發下如何進行估算和調優
- 實戰部分
- 第09講:案例實戰:面對突如其來的 GC 問題如何下手解決
- 第10講:動手實踐:自己模擬 JVM 內存溢出場景
- 第11講:動手實踐:遇到問題不要慌,輕松搞定內存泄漏
- 第12講:工具進階:如何利用 MAT 找到問題發生的根本原因
- 第13講:動手實踐:讓面試官刮目相看的堆外內存排查
- 第14講:預警與解決:深入淺出 GC 監控與調優
- 第15講:案例分析:一個高死亡率的報表系統的優化之路
- 第16講:案例分析:分庫分表后,我的應用崩潰了
- 進階部分
- 第17講:動手實踐:從字節碼看方法調用的底層實現
- 第18講:大廠面試題:不要搞混 JMM 與 JVM
- 第19講:動手實踐:從字節碼看并發編程的底層實現
- 第20講:動手實踐:不為人熟知的字節碼指令
- 第21講:深入剖析:如何使用 Java Agent 技術對字節碼進行修改
- 第22講:動手實踐:JIT 參數配置如何影響程序運行?
- 第23講:案例分析:大型項目如何進行性能瓶頸調優?
- 彩蛋
- 第24講:未來:JVM 的歷史與展望
- 第25講:福利:常見 JVM 面試題補充