虛擬機棧出現的背景
由于跨平臺性的設計,Java的指令都是根據棧來設計的。不同平臺CPU架構不同,所以不能設計為基于寄存器的。優點是跨平臺,指令集小,編譯器容易實現,缺點是性能下降,實現同樣的功能需要更多的指令。
內存中的棧與堆
有不少Java開發人員一提到Java內存結構,就會非常粗粒度地將JVM中的內存區理解為僅有Java堆(heap)和Java棧(stack),他們的關系是,棧是運行時的單位,而堆是存儲的單位
● 棧解決程序的運行問題,即程序如何執行,或者說如何處理數據。
● 堆解決的是數據存儲的問題,即數據怎么放,放哪里
Java虛擬機棧是什么?
Java虛擬機棧(Java Virtual Machine Stack),早期也叫Java棧。每個線程在創建時都會創建一個虛擬機棧,其內部保存一個個的棧幀(Stack Frame),對應著一次次的Java方法調用,是線程私有的。它的生命周期和線程一致(上一章已經說過的了,棧是線程獨有的)
作用
主管Java程序的運行,它保存方法的局部變量、部分結果,并參與方法的調用和返回。
棧的特點
棧是一種快速有效的分配存儲方式,訪問速度僅次于程序計數器。
JVM直接對Java棧的操作只有兩個:
● 每個方法執行,伴隨著進棧(入棧、壓棧)
● 執行結束后的出棧工作
對于棧來說不存在垃圾回收問題(棧存在溢出的情況)
棧中可能出現的異常
Java 虛擬機規范允許Java棧的大小是動態的或者是固定不變的
● 如果采用固定大小的Java虛擬機棧,那每一個線程的Java虛擬機棧容量可以在線程創建的時候獨立選定。如果線程請求分配的棧容量超過Java虛擬機棧允許的最大容量,Java虛擬機將會拋出一個StackOverflowError 異常。
● 如果Java虛擬機棧可以動態擴展,并且在嘗試擴展的時候無法申請到足夠的內存,或者在創建新的線程時沒有足夠的內存去創建對應的虛擬機棧,那Java虛擬機將會拋出一個 OutOfMemoryError 異常。
案例(我們僅演示第一點,因為第二個問題需要在我們虛擬機內存不夠的時候才會報)
/*** 測試結果,main遞歸執行11417次棧溢出** 設置棧的大小為:-Xss256k** 修改后只遞歸2000次便溢出,修改生效***/
public class StackTest {public static int count;public static void main(String[] args) {System.out.println(count);count++;main(args);}
}
棧中存儲什么?
每個線程都有自己的棧,棧中的數據都是以棧幀(Stack Frame)的格式存在。在這個線程上正在執行的每個方法都各自對應一個棧幀(Stack Frame)。棧幀是一個內存區塊,是一個數據集,維系著方法執行過程中的各種數據信息。
JVM直接對Java棧的操作只有兩個,就是對棧幀的壓棧和出棧,遵循“先進后出”/“后進先出”原則。
在一條活動線程中,一個時間點上,只會有一個活動的棧幀。即只有當前正在執行的方法的棧幀(棧頂棧幀)是有效的,這個棧幀被稱為當前棧幀(Current Frame),與當前棧幀相對應的方法就是當前方法(Current Method),定義這個方法的類就是當前類(Current Class)。
執行引擎運行的所有字節碼指令只針對當前棧幀進行操作。
如果在該方法中調用了其他方法,對應的新的棧幀會被創建出來,放在棧的頂端,成為新的當前幀。
不同線程中所包含的棧幀是不允許存在相互引用的,即不可能在一個棧幀之中引用另外一個線程的棧幀。
如果當前方法調用了其他方法,方法返回之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,接著,虛擬機會丟棄當前棧幀,使得前一個棧幀重新成為當前棧幀。
Java方法有兩種返回函數的方式,一種是正常的函數返回,使用return指令;另外一種是拋出異常。不管使用哪種方式,都會導致棧幀被彈出。
/*** 測試手段:三個方法嵌套運行debug* 打印如下:* main方法開始執行* method1 begin* method2 begin* method3 begin* method3 end* method2 will end(wait return)* method1 end* main方法正常結束*/
public class StackWorkTest {public static void main(String[] args) throws Exception{System.out.println("main方法開始執行");StackWorkTest stackWorkTest = new StackWorkTest();try {stackWorkTest.method01();}catch (Exception e) {e.printStackTrace();}System.out.println("main方法正常結束");}//整體是嵌套關系public void method01(){System.out.println("method1 begin");//執行到這里時method1是當前棧幀method02();System.out.println("method1 end");//執行到這里(回到method1時)method1是當前棧幀
// int i = 10 / 0;return; //這里寫和不寫都是一樣的,void方法最終都會有默認的return;當然我們可以通過return;使得方法提前結束}public int method02(){System.out.println("method2 begin");int i = 10;int v = (int) method03();System.out.println("method2 will end(wait return)");return v;}public double method03(){System.out.println("method3 begin");double i = 20.0;System.out.println("method3 end");return i;}
}
結束方式:return or exception without try-catch,上面是方法正常結束的例子
package com.sobot.net.jvm;/*** 異常時打印如下** main方法開始執行* method1 begin* method2 begin* method3 begin* method3 end* method2 will end(wait return)* method1 end* Exception in thread "main" java.lang.ArithmeticException: / by zero* at com.sobot.net.jvm.StackWorkTest.method01(StackWorkTest.java:39)* at com.sobot.net.jvm.StackWorkTest.main(StackWorkTest.java:30)*/
public class StackWorkTest {public static void main(String[] args) throws Exception{System.out.println("main方法開始執行");StackWorkTest stackWorkTest = new StackWorkTest();
// try {
// stackWorkTest.method01();
// }catch (Exception e) {
// e.printStackTrace();
// }stackWorkTest.method01();System.out.println("main方法正常結束");}//整體是嵌套關系public void method01(){System.out.println("method1 begin");//執行到這里時method1是當前棧幀method02();System.out.println("method1 end");//執行到這里(回到method1時)method1是當前棧幀int i = 10 / 0;return;}public int method02(){System.out.println("method2 begin");int i = 10;int v = (int) method03();System.out.println("method2 will end(wait return)");return v;}public double method03(){System.out.println("method3 begin");double i = 20.0;System.out.println("method3 end");return i;}
}
棧幀的內部結構
每個棧幀中存儲著:
● 局部變量表(Local Variables)
● 操作數棧(operand Stack)(或表達式棧)
● 動態鏈接(DynamicLinking)(或指向運行時常量池的方法引用)
● 方法返回地址(Return Address)(或方法正常退出或者異常退出的定義)
● 一些附加信息
并行每個線程下的棧都是私有的,因此每個線程都有自己各自的棧,并且每個棧里面都有很多棧幀,棧幀的大小主要由局部變量表 和 操作數棧決定的,而棧中棧幀的數目又受到棧幀大小的影響
局部變量表(Local Variables)
● 定義為一個數字數組,主要用于存儲方法參數和定義在方法體內的局部變量,這些數據類型包括各類基本數據類型、對象引用(reference),以及returnAddress類型。
● 由于局部變量表是建立在線程的棧上,是線程的私有數據,因此不存在數據安全問題
● 局部變量表所需的容量大小是在編譯期確定下來的,并保存在方法的Code屬性的maximum local variables數據項中。在方法運行期間是不會改變局部變量表的大小的。
● 方法嵌套調用的次數由棧的大小決定。一般來說,棧越大,方法嵌套調用次數越多。對一個函數而言,它的參數和局部變量越多,使得局部變量表膨脹,它的棧幀就越大,以滿足方法調用所需傳遞的信息增大的需求。進而函數調用就會占用更多的棧空間,導致其嵌套調用次數就會減少。 參考上一個案例,默認main方法可以嵌套執行11400次,但我們改了棧的大小為256k時僅能嵌套2000多次
● 局部變量表中的變量只在當前方法調用中有效。在方法執行時,虛擬機通過使用局部變量表完成參數值到參數變量列表的傳遞過程。當方法調用結束后,隨著方法棧幀的銷毀,局部變量表也會隨之銷毀。
案例
package com.sobot.net.jvm;import java.util.Date;//局部變量表的大小在編譯時就已經確定下來了
//普通方法入棧后棧中局部變量表都默認會有this,靜態方法局部變量表沒有this
public class LocalVariableTest {public LocalVariableTest() {}public LocalVariableTest(int count) {this.count = count;}private int count = 0;public static void main(String[] args) {LocalVariableTest localVariableTest = new LocalVariableTest();int num = 10;localVariableTest.test();}public void test(){Date date = new Date();int b = 10;double a = 10.0;String name = "atgwqqqqqqw";String weight = "asau";//如果不聲明test變量,即這行代碼變為test(date, name);時,局部變量表就沒有test這個變量了String test = test(date, name);System.out.println(date + name);}public static void staticTest(){LocalVariableTest localVariableTest = new LocalVariableTest();Date date = new Date();int count = 10;System.out.println(count);//不能使用this,因為this變量不在當前方法的局部變量表里
// System.out.println(this.count);}public String test(Date date,String str) {return date + str;}public void test3(Date date,String str) {this.count++;}public void test4() {int a = 0;{int b = 0;b = a + 1;}int c = a + 1;}
}
main方法中的局部變量如下
起始pc指局部變量從什么時候生效的,數字代表的是字節碼的行號,參考字節碼如下
首先args參數從最開始,也就是字節碼第0行就開始生效了,而localVariableTest在字節碼第8行生效,num參數在第11行生效,長度即代表生效范圍
計算公式為字節碼長度減去起始PC
關于Slot的理解
● 局部變量表,最基本的存儲單元是Slot(變量槽)
● 參數值的存放總是在局部變量數組的index0開始,到數組長度-1的索引結束。
● 局部變量表中存放編譯期可知的各種基本數據類型(8種),引用類型(reference),returnAddress類型的變量。
● 在局部變量表里,32位以內的類型只占用一個slot(包括returnAddress類型),64位的類型(long和double)占用兩個slot。
● byte、short、char 在存儲前被轉換為int,boolean也被轉換為int,0表示false,非0表示true。
● JVM會為局部變量表中的每一個Slot都分配一個訪問索引,通過這個索引即可成功訪問到局部變量表中指定的局部變量值
● 當一個實例方法被調用的時候,它的方法參數和方法體內部定義的局部變量將會按照順序被復制到局部變量表中的每一個slot上
● 如果需要訪問局部變量表中一個64bit的局部變量值時,只需要使用前一個索引即可。(比如:訪問long或doub1e類型變量)
● 如果當前幀是由構造方法或者實例方法創建的,那么該對象引用this將會存放在index為0的slot處,其余的參數按照參數表順序繼續排列。
關于占用曹的大小,我們以上個案例中的test方法演示,如下
int型占用一個槽(43-42),同理String類型的double類型都是占用兩個槽
棧幀中的局部變量表中的槽位是可以重用的,如果一個局部變量過了其作用域,那么在其作用域之后申明的新的局部變就很有可能會復用過期局部變量的槽位,從而達到節省資源的目的。如上述案例中的test4方法,如下
我們可以看出方法test4中的變量b在出了括號后就已經無效了,所以b所占有的槽(index=2)就會重新分配給變量c,畢竟局部變量表的本質是一個數組結構,大小不會隨筆變動,所以重復利用是最好的版本
總結
* 變量的分類* 按數據類型分類:基本數據類型和引用數據類型* 按照類中聲明位置* 成員變量* * 類變量(靜態成員變量),被類加載器加載后,在link的prepare階段默認賦值,initalize階段顯示復雜(直接在聲明類變量時賦值或者在靜態代碼快賦值)* * 實例變量,隨著對象的創建在堆中進行賦值,如果沒有進行顯示賦值(比如構造方法為空或者壓根都沒有重寫構造方法)那就默認賦值* 局部變量:使用前必須進行顯示賦值,大家一試便知
補充說明
- 在棧幀中,與性能調優關系最為密切的部分就是前面提到的局部變量表。在方法執行時,虛擬機使用局部變量表完成方法的傳遞。因為局部變量表有很多引用類型的局部變量,這本質上是引用,執行了堆中真實存儲的對象,所以這個地方如果處理不當也是容易引發OOM的;
- 局部變量表中的變量也是重要的垃圾回收根節點,只要被局部變量表中直接或間接引用的對象都不會被回收。
4.4. 操作數棧(Operand Stack)
每一個獨立的棧幀除了包含局部變量表以外,還包含一個后進先出(Last-In-First-Out)的 操作數棧,也可以稱之為表達式棧(Expression Stack)
操作數棧,在方法執行過程中,根據字節碼指令,往棧中寫入數據或提取數據,即入棧(push)和 出棧(pop)
● 某些字節碼指令將值壓入操作數棧,其余的字節碼指令將操作數取出棧。使用它們后再把結果壓入棧
● 比如:執行復制、交換、求和等操作
操作數棧,主要用于保存計算過程的中間結果,同時作為計算過程中變量臨時的存儲空間。操作數棧就是JVM執行引擎的一個工作區,當一個方法剛開始執行的時候,一個新的棧幀也會隨之被創建出來,這個方法的操作數棧是空的。
每一個操作數棧都會擁有一個明確的棧深度用于存儲數值,其所需的最大深度在編譯期就定義好了,保存在方法的Code屬性中,為max_stack的值。棧中的任何一個元素都是可以任意的Java數據類型
● 32bit的類型占用一個棧單位深度
● 64bit的類型占用兩個棧單位深度
操作數棧并非采用訪問索引的方式來進行數據訪問的,而是只能通過標準的入棧和出棧操作來完成一次數據訪問
如果被調用的方法帶有返回值的話,其返回值將會被壓入當前棧幀的操作數棧中,并更新PC寄存器中下一條需要執行的字節碼指令。
操作數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配,這由編譯器在編譯器期間進行驗證,同時在類加載過程中的類檢驗階段的數據流分析階段要再次驗證。
另外,我們說Java虛擬機的解釋引擎是基于棧的執行引擎,其中的棧指的就是操作數棧。
案例代碼
public void testAddOperation() {//byte,short,boolen,short都是按int類型進行保存byte i = 15;int j = 8;int k = i + j;}
反編譯其字節碼文件如下
0 bipush 15 2 istore_13 bipush 85 istore_26 iload_17 iload_28 iadd9 istore_3
10 return
執行的第一步bipush命令:bipush命令時把int型變量(byte,short,boolen,short都是按int類型進行保存)入棧,然后pc寄存器保存當前命令行即為0
istore_1 是把棧頂的操作數保存到局部變量表中索引位置為1的地方,然后pc寄存器保存當前命令行即為2,后續同理,不再對pc寄存器做說明了
第3,4行命令是繼續把j=8這個變量入棧然后存儲到到局部變量表中索引位置為2的地方
第5,6行iload命令則是分別在局部變量表中索引為1,2的位置分別取出15和8入棧
在棧中做運算,執行iadd命令,iadd命令的執行需要依賴于執行引擎調用cpu來運算,然后再把結果入棧
再把棧中變量值為23的變量k存儲到局部變量吧第3個位置上
然后就是return方法結束
補充:這里是void方法,執行到最后就通過return指令方法就正常結束了,如果是帶返回值的類型,那么就里的return xxx除了起到結束方法(結束方法也意味著該方法對應棧幀的局部變量表和操作數棧的清空)的作用(同return)還會把返回值xxx返回給他的上一個調用方法的棧中,也是先入棧,隨后通常會再存到局部變量表,這里也要注意
public int testAddOperation() {byte i = 15;int j = 8;return 10;}public int testAddOperation2() {byte i = 15;int j = 8;return 10;}public void test(){int i = testAddOperation();testAddOperation2();}
test()是int型方法,第一行我們定義了int i = testAddOperation(),這樣會存入局部變量表的第一個位置里,第二行testAddOperation2();由于沒有定義局部變量來接收返回值,所以不會存入局部變量表,如下test方法
0 aload_0 #剛開始時就把上一個棧幀中方法testAddOperation的返回值加載到當前棧幀(test方法)操作數棧中1 invokevirtual #2 <com/sobot/net/jvm/StackTest.testAddOperation : ()I>4 istore_1 #把變量i存入局部變量表的第一個位置里5 aload_0 #把上一個棧幀中方法testAddOperation2的返回值加載到當前棧幀(test方法)操作數棧中6 invokevirtual #3 <com/sobot/net/jvm/StackTest.testAddOperation2 : ()I>9 pop
10 return
前面提過,基于棧式架構的虛擬機所使用的零地址指令更加緊湊,但完成一項操作的時候必然需要使用更多的入棧和出棧指令,這同時也就意味著將需要更多的指令分派(instruction dispatch)次數和內存讀/寫次數。
由于操作數是存儲在內存中的,因此頻繁地執行內存讀/寫操作必然會影響執行速度。為了解決這個問題,HotSpot JVM的設計者們提出了棧頂緩存(Tos,Top-of-Stack Cashing)技術,將棧頂元素全部緩存在物理CPU的寄存器中,以此降低對內存的讀/寫次數,提升執行引擎的執行效率。
為什么需要運行時常量池呢?
常量池的作用:就是為了提供一些符號和常量,便于指令的識別
案例
public class DynamicLinkingTest {int num = 10;public void methodA() {System.out.println("MethodA");}public void methodB() {System.out.println("MethodB");methodA();num++;}
}
javap -v命令編譯后
public class DynamicLinkingTestminor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:#1 = Methodref #9.#20 // java/lang/Object."<init>":()V#2 = Fieldref #8.#21 // DynamicLinkingTest.num:I#3 = Fieldref #22.#23 // java/lang/System.out:Ljava/io/PrintStream;#4 = String #24 // MethodA#5 = Methodref #25.#26 // java/io/PrintStream.println:(Ljava/lang/String;)V#6 = String #27 // MethodB#7 = Methodref #8.#28 // DynamicLinkingTest.methodA:()V#8 = Class #29 // DynamicLinkingTest#9 = Class #30 // java/lang/Object#10 = Utf8 num#11 = Utf8 I#12 = Utf8 <init>#13 = Utf8 ()V#14 = Utf8 Code#15 = Utf8 LineNumberTable#16 = Utf8 methodA#17 = Utf8 methodB#18 = Utf8 SourceFile#19 = Utf8 DynamicLinkingTest.java#20 = NameAndType #12:#13 // "<init>":()V#21 = NameAndType #10:#11 // num:I#22 = Class #31 // java/lang/System#23 = NameAndType #32:#33 // out:Ljava/io/PrintStream;#24 = Utf8 MethodA#25 = Class #34 // java/io/PrintStream#26 = NameAndType #35:#36 // println:(Ljava/lang/String;)V#27 = Utf8 MethodB#28 = NameAndType #16:#13 // methodA:()V#29 = Utf8 DynamicLinkingTest#30 = Utf8 java/lang/Object#31 = Utf8 java/lang/System#32 = Utf8 out#33 = Utf8 Ljava/io/PrintStream;#34 = Utf8 java/io/PrintStream#35 = Utf8 println#36 = Utf8 (Ljava/lang/String;)V
{int num;descriptor: Iflags:public DynamicLinkingTest();descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: aload_05: bipush 107: putfield #2 // Field num:I10: returnLineNumberTable:line 3: 0line 4: 4public void methodA();descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=1, args_size=10: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #4 // String MethodA5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 6: 0line 7: 8public void methodB();descriptor: ()Vflags: ACC_PUBLICCode:stack=3, locals=1, args_size=10: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #6 // String MethodB5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: aload_09: invokevirtual #7 // Method methodA:()V12: aload_013: dup14: getfield #2 // Field num:I17: iconst_118: iadd19: putfield #2 // Field num:I22: returnLineNumberTable:line 10: 0line 11: 8line 12: 12line 13: 22
}
}
我們來分析methodA的調用,首先是調用了命令
9: invokevirtual #7 // Method methodA:()V
我們看 #7這個符號引用類似是方法引用 Methodref類型,又引用了 #8和#28
#7 = Methodref #8.#28 // DynamicLinkingTest.methodA:()V
#8是引用的class類型,即相當于引用當前的類的一個指針
#8 = Class #29 // DynamicLinkingTest
#29這個引用 只能在代表當前類
#29 = Utf8 DynamicLinkingTest
回到#28,即方法名與類型
#28 = NameAndType #16:#13 // methodA:()V
#16和#13分別如下,含義很直白標識方法名和類型
#13 = Utf8 ()V#16 = Utf8 methodA
這就是方法A的完整流程,
總之,類在加載的時候會在編譯時把所需要的常量加載到運行時常量池里,然后方法在當前棧幀執行時就會通過一系列的符號引用指向運行時常量池
比如著一些最常見的
#29 = Utf8 DynamicLinkingTest#30 = Utf8 java/lang/Object#31 = Utf8 java/lang/System#32 = Utf8 out#33 = Utf8 Ljava/io/PrintStream;#34 = Utf8 java/io/PrintStream#35 = Utf8 println#36 = Utf8 (Ljava/lang/String;)V
這樣最主要的時提高資源的復用性,因為棧是線程共享的,高并發環境下如果每個棧都要把這些資源加載一份那是不可能做到的,所以通過一個小小的引用地址(起到指針作用)來引用這些資源是很好理解的,運行時常量池對于一個類中的各線程來說時需要共享的,那就比如會存儲在一個從類角度上時共享的區域即方法區中
動態鏈接、方法返回地址、附加信息 : 有些地方被稱為幀數據區,每一個棧幀內部都包含一個指向運行時常量池中該棧幀所屬方法的引用。包含這個引用的目的就是為了支持當前方法的代碼能夠實現動態鏈接(Dynamic Linking)。比如:invokedynamic指令
在Java源文件被編譯到字節碼文件中時,所有的變量和方法引用都作為符號引用(Symbolic Reference)保存在class文件的常量池里。比如:字節碼文件中描述一個方法調用了另外的其他方法時,就是通過常量池中指向方法的符號引用來表示的,那么動態鏈接的作用就是為了將這些符號引用轉換為調用方法的直接引用。
常量池的作用:可以總結為就是為了提供一些符號和常量,便于指令的識別
方法的調用:解析與分配
在JVM中,將符號引用轉換為調用方法的直接引用與方法的綁定機制相關
靜態鏈接
當一個字節碼文件被裝載進JVM內部時,如果被調用的目標方法在編譯期可知,且運行期保持不變時,這種情況下降調用方法的符號引用轉換為直接引用的過程稱之為靜態鏈接
動態鏈接
如果被調用的方法在編譯期無法被確定下來,只能夠在程序運行期將調用的方法的符號轉換為直接引用,由于這種引用轉換過程具備動態性,因此也被稱之為動態鏈接。
靜態鏈接和動態鏈接不是名詞,而是動詞,這是理解的關鍵。
早期綁定
早期綁定就是指被調用的目標方法如果在編譯期可知,且運行期保持不變時,即可將這個方法與所屬的類型進行綁定,這樣一來,由于明確了被調用的目標方法究竟是哪一個,因此也就可以使用靜態鏈接的方式將符號引用轉換為直接引用。
4.8.4. 晚期綁定
如果被調用的方法在編譯期無法被確定下來,只能夠在程序運行期根據實際的類型綁定相關的方法,這種綁定方式也就被稱之為晚期綁定。
‘’
public class Aniaml {public void eat(){System.out.println("動物進食");}
}interface Huntable {void hunt();
}class Dog extends Aniaml implements Huntable{@Overridepublic void hunt() {System.out.println("多管閑事");}public void eat(){System.out.println("夠吃骨頭");}}class Cat extends Aniaml implements Huntable{public Cat(String name) {this();//典型的早期綁定}public Cat() {super();//典型的早期綁定}@Overridepublic void hunt() {System.out.println("天經地義");}public void eat(){super.eat(); //典型的早期綁定System.out.println("貓吃耗子");}}class AniamlTest {public void showAnimal(Aniaml aniaml){aniaml.eat();//表現為晚期綁定}public void showHunt(Huntable hunt){hunt.hunt();//接口那更是晚期綁定}
}
對 AniamlTest進行編譯,關注里面的兩個方法
showAnimal
0 aload_1
1 invokevirtual #2 <com/sobot/net/jvm/Aniaml.eat : ()V>
4 return
showHunt
0 aload_1
1 invokeinterface #3 <com/sobot/net/jvm/Huntable.hunt : ()V> count 1
6 return
可以看出invokevirtual和 invokeinterface都是典型的虛方法調用,與之相對的是,我們關注Cat類中
兩個init方法分別如下
0 aload_0
1 invokespecial #1 <com/sobot/net/jvm/Cat.<init> : ()V>
4 return
0 aload_0
1 invokespecial #2 <com/sobot/net/jvm/Aniaml.<init> : ()V>
4 return
eat方法(主要關注里面對父類方法的調用super.eat();)
0 aload_01 invokespecial #6 <com/sobot/net/jvm/Aniaml.eat : ()V>4 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>7 ldc #7 <貓吃耗子>9 invokevirtual #5 <java/io/PrintStream.println : (Ljava/lang/String;)V>
12 return
如果被調用的方法在編譯期無法被確定下來,只能夠在程序運行期根據實際的類型綁定相關的方法,這種綁定方式也就被稱之為晚期綁定。隨著高級語言的橫空出世,類似于Java一樣的基于面向對象的編程語言如今越來越多,盡管這類編程語言在語法風格上存在一定的差別,但是它們彼此之間始終保持著一個共性,那就是都支持封裝、繼承和多態等面向對象特性,既然這一類的編程語言具備多態特悄,那么自然也就具備早期綁定和晚期綁定兩種綁定方式。Java中任何一個普通的方法其實都具備虛函數的特征,它們相當于C語言中的虛函數(C中則需要使用關鍵字virtual來顯式定義)。如果在Java程序中不希望某個方法擁有虛函數的特征時,則可以使用關鍵字final來標記這個方法。
虛方法和非虛方法
如果方法在編譯期就確定了具體的調用版本,這個版本在運行時是不可變的。這樣的方法稱為非虛方法。反之即為非虛方法
總結
動態連接 => 晚期綁定 =>虛方法 => 調用虛方法invokeVirtual(子類重寫了父類的方法,final方法),invokeinterface(接口方法),這里注意一個坑,fianl方法是非虛方法但字節碼中顯示依然是使用的invokeVirtual,
靜態連接 => 晚期綁定 =>非虛方法 => 調用非虛方法invokestatic(調用靜態方法),invokespecial(實例構造方法,私有方法,父類方法),fianl方法(對應invokeVirtual),都是非虛方法都是非虛方法
動態調用指令:invokedynamic:動態解析出需要調用的方法,然后執行
● JVM字節碼指令集一直比較穩定,一直到Java7中才增加了一個invokedynamic指令,這是Java為了實現「動態類型語言」支持而做的一種改進。
● 但是在Java7中并沒有提供直接生成invokedynamic指令的方法,需要借助ASM這種底層字節碼工具來產生invokedynamic指令。直到Java8的Lambda表達式的出現,invokedynamic指令的生成,在Java中才有了直接的生成方式。
● Java7中增加的動態語言類型支持的本質是對Java虛擬機規范的修改,而不是對Java語言規則的修改,這一塊相對來講比較復雜,增加了虛擬機中的方法調用,最直接的受益者就是運行在Java平臺的動態語言的編譯器。
動態類型語言和靜態類型語言兩者的區別就在于對類型的檢查是在編譯期還是在運行期,滿足前者就是靜態類型語言,反之是動態類型語言。說的再直白一點就是,靜態類型語言是判斷變量自身的類型信息;動態類型語言是判斷變量值的類型信息,變量沒有類型信息,變量值才有類型信息,這是動態語言的一個重要特征
我們分別以java,python,js來舉例
String str = "abc";
int a = 10;
float f = 10.0f;
long l = 10l;
...............
我們可以看出java定義變量必須指明變量類型,而js則不需要指明變量類型,只需要用var來定義一個變量
var a = 10;
var str = ‘b’;
................
而python則是更絕
info = 100.0;
案例代碼
interface Func{public boolean func(String str);
}public class Lamda{public void method(Func func) {return;}public static void main(String[] args) {Lamda lamda = new Lamda();//invokespecial//創建func的實例傳入methodFunc func = str -> {return true;};lamda.method(func);//直接以匿名的方式傳入進來lamda.method(str -> {return true;});}
}
這里可能有些難理解,但結合動態語言的本質特點是編譯時不定死類型而是在運行時才考慮類型,在回到我們代碼
Lamda lamda = new Lamda();//invokespecial//創建func的實例傳入methodFunc func = str -> {return true;};lamda.method(func);//直接以匿名的方式傳入進來lamda.method(str -> {return true;});
編譯時我們根本就無法確定等號右邊部分(匿名函數表達式)對象的類型,只有在運行時才能獲取,這就已經是符合動態語言特點了,所以對應的func這個方法的調用類型為invokedynamic,主要還是因為對調用這個方法的引用func完全無法在編譯期間確認下來類型
注意:可能有人感覺虛方法和動態調用比較像,它們間的確有共同點就是,總結為一個字就是晚,即編譯期間無法下定論,運行時才見真章,但虛方法是站在方法調用角度的而動態調用是站在對象創建角度來說的,這是本質區別
方法返回地址(return address)
存放調用該方法的pc寄存器的值。一個方法的結束,有兩種方式:
● 正常執行完成
● 出現未處理的異常,非正常退出
無論通過哪種方式退出,在方法退出后都返回到該方法被調用的位置。方法正常退出時,調用者的pc計數器的值作為返回地址,即調用該方法的指令的下一條指令的地址。而通過異常退出的,返回地址是要通過異常表來確定,棧幀中一般不會保存這部分信息。
當一個方法開始執行后,只有兩種方式可以退出這個方法:
- 執行引擎遇到任意一個方法返回的字節碼指令(return),會有返回值傳遞給上層的方法調用者,簡稱正常完成出口; 一個方法在正常調用完成之后,究竟需要使用哪一個返回指令,還需要根據方法返回值的實際數據類型而定。在字節碼指令中,返回指令包含ireturn(當返回值是boolean,byte,char,short和int類型時使用),lreturn(Long類型),freturn(Float類型),dreturn(Double類型),areturn。另外還有一個return指令聲明為void的方法,實例初始化方法,類和接口的初始化方法使用。
- 在方法執行過程中遇到異常(Exception),并且這個異常沒有在方法內進行處理,也就是只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會導致方法退出,簡稱異常完成出口。
方法執行過程中,拋出異常時的異常處理,存儲在一個異常處理表,方便在發生異常的時候找到處理異常的代碼
案例代碼
public void method1() {vretrun();try {method2();} catch (Exception e) {e.printStackTrace();}}private void method2() throws IOException {FileReader fileReader = new FileReader("d://test.txt");char[] buffer = new char[1024];int len = 0;while((len = fileReader.read(buffer)) != -1) {String s = new String(buffer, 0, len);System.out.println(s);}fileReader.close();}
查看方法1中的異常表,注意方法2是把異常給拋出去了,所以沒有異常表,方法2把異常拋給了方法1,方法1沒有繼續拋給它的上一個調用方法而是通過try-catch進線處理,所以方法1有異常表,如下
含義是字節碼4到8行范圍內有捕獲到異常那就調整到第11行,然后我們參考
字節碼和行號對應關系發現
其實含義就是try-catch包裹的代碼塊中的代碼出現問題后直接跳轉到catch塊內進行處理
本質上,方法的退出就是當前棧幀出棧的過程。此時,需要恢復上層方法的局部變量表、操作數棧、將返回值壓入調用者棧幀的操作數棧、設置PC寄存器值等,讓調用者方法繼續執行下去。
正常完成出口和異常完成出口的區別在于:通過異常完成出口退出的不會給他的上層調用者產生任何的返回值。