JVM学习(八):虚拟机栈(字节码程度深入剖析)
目錄
一、概述?
1.1 基于棧結構的虛擬機?
1.2 棧和堆
二、虛擬機棧(Java Virtual Machine Stack)詳述
2.1 虛擬機棧介紹?
2.2 虛擬機棧作用
2.3 虛擬機棧特點?
三、棧中常見的異常?
3.1?StackOverflowError異常
3.2?OutOfMemoryError異常
四、棧的運行原理
4.1 棧的存儲單位?
4.2 棧的運行原理?
五、棧幀的內部結構?
六、局部變量表(Local Variable Table)
6.1?局部變量表?
6.1.1 介紹?
6.1.2 實戰
6.2 變量槽slot?
6.2.1?slot的介紹?
6.2.2?slot的測試?
6.2.3?slot的重復利用
6.2.4?類變量和局部變量的區別?
七、操作數棧(Operand Stack)
7.1 介紹?
7.2 原理?
7.3 字節碼層面逐步分析
7.4 對方法返回值的處理
7.5 字節碼中對int類型的理解
7.6 棧頂緩存技術(TOS,Top-of-Stack Cashing)
八、動態鏈接(Dynamic Linking,指向運行時常量池的方法引用)?
8.1 介紹?
8.2 演示?
8.3 常量池的作用
九、方法的調用
9.1 早期綁定與晚期綁定?
9.2 虛方法和非虛方法
9.2.1 概念?
9.2.2 字節碼指令介紹
9.2.3 普通調用指令演示
9.2.4 invokedynamic指令介紹
9.2.5 invokedynamic指令演示?
9.3 方法重寫的本質
9.4 虛方法表
9.4.1 虛方法表介紹?
9.4.2 虛方法表示例?
十、方法返回地址(Return Address)
10.1? 方法返回地址介紹
10.2 方法的退出
10.2.1 正常完成出口?
10.2.2 異常完成出口
10.2.3 方法退出的本質
十一、一些附加信息?
十二、棧相關的面試題?
一、概述?
1.1 基于棧結構的虛擬機?
????????由于跨平臺性的設計,Java的指令都是根據棧來設計的。不同平臺CPU架構不同,所以不能設計為基于寄存器的。
? ? ? ? 基于棧設計的優點是跨平臺,指令集小,編譯器容易實現;缺點是性能下降,實現同樣的功能需要更多的指令。?
?1.2 棧和堆
????????有不少Java開發人員一提到Java內存結構,就會非常粗粒度地將JVM中的內存區理解為僅存Java堆(heap) 和 Java棧(stack),這是不正確的。
????????棧是運行時的單位,而堆是存儲的單位。
????????即:棧解決程序的運行問題,即程序如何執行,或者說如何處理數據。堆解決的是數據存儲的問題,即數據怎么放、放在哪兒。
二、虛擬機棧(Java Virtual Machine Stack)詳述
2.1 虛擬機棧介紹?
????????Java虛擬機棧(Java virtual Machine stack)早期也叫Java棧。每個線程在創建時都會創建一個虛擬機棧,其內部保存一個個的棧幀( Stack Frame) ,對應著一次次的Java方法調用。
? ? ??JVM直接對Java棧的操作只有兩個:
??
? ? ? ? 一個棧幀對應著一個方法。?
2.2 虛擬機棧作用
????????主管Java程序的運行,它保存方法的局部變量、部分結果,并參與方法的調用和返回。
2.3 虛擬機棧特點?
- 線程私有
- 生命周期和線程一致
- 棧是一種快速有效的分配存儲方式,訪問速度僅次于程序計數器
- 對于棧來說存在OOM問題,不存在垃圾回收問題
三、棧中常見的異常?
????????Java虛擬機規范允許Java棧的大小是動態的或者是固定不變的。
3.1?StackOverflowError異常
????????如果采用固定大小的Java虛擬機棧,那每一個線程的Java虛擬機棧容量可以在線程創建的時候獨立選定。如果線程請求分配的棧容量超過Java虛擬機棧允許的最大容量,Java虛擬機將會拋出一個StackOverflowError異常。我們可以使用參數-Xss選項來設置線程的最大棧空間,棧的大小直接決定了函數調用的最大可達深度。
? ? ? ? 最常見的就是遞歸調用了:
public class StackErrorTest {public static void main(String[] args) {main(args);} }? ? ? ? 我們設置一個遞增的變量,看看能調用多少次:
public class StackErrorTest {private static int count = 1;public static void main(String[] args) {System.out.println(count);count++;main(args);}}? ? ? ? 設置一下 -Xss 參數:
3.2?OutOfMemoryError異常
????????如果Java虛擬機棧可以動態擴展,并且在嘗試擴展的時候無法中請到足夠的內存,或者在創建新的線程時沒有足夠的內存去創建對應的虛擬機棧,那Java虛擬機將會拋出一個OutOfMemoryError異常。?
四、棧的運行原理
4.1 棧的存儲單位?
????????每個線程都有自己的棧,棧中的數據都是以棧幀(Stack Frame)的格式存在。在這個線程上正在執行的每個方法都各自對應一個棧幀(Stack Frame),棧幀是一個內存區塊,是一個數據集,維系著方法執行過程中的各種數據信息。
4.2 棧的運行原理?
????????JVM直接對Java棧的操作只有兩個,就是對棧幀的壓棧和出棧,遵循先進后出原則。
????????在一條活動線程中,一個時間點上,只會有一個活動的棧幀。即只有當前正在執行的方法的棧幀(棧頂棧幀)是有效的,這個棧幀被稱為當前棧幀( current Frame),與當前棧幀相對應的方法就是當前方法(Current Method),定義這個方法的類就是當前類(Current Class)。
??
????????執行引擎運行的所有字節碼指令只針對當前棧幀進行操作。
????????如果在該方法中調用了其他方法,對應的新的棧幀會被創建出來,放在棧的頂端,成為新的當前幀。
????????不同線程中所包含的棧幀是不允許存在相互引用的,即不可能在一個棧幀之中引用另外一個線程的棧幀。
????????如果當前方法調用了其他方法,方法返回之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,接著,虛擬機會丟棄當前棧幀,使得前一個棧幀重新成為當前棧幀。
????????Java方法有兩種返回函數的方式,一種是正常的函數返回,使用return指令;另外一種是拋出異常。不管使用哪種方式,都會導致棧幀被彈出。
五、棧幀的內部結構?
????????每個棧幀中存儲著:
- 局部變量表(Local variables)
- 操作數棧(operand stack) (或表達式棧)
- 動態鏈接(Dynamic Linking)(或指向運行時常量池的方法引用)
- 方法返回地址(Return Address)(或方法正常退出或者異常退出的定義)
- 一些附加信息
?
六、局部變量表(Local Variable Table)
6.1?局部變量表?
6.1.1 介紹?
????????局部變量表也被稱之為局部變量數組或本地變量表,定義為一個數字數組,主要用于存儲方法參數和定義在方法體內的局部變量,這些數據類型包括各類基本數據類型、對象引用(reference),以及returnAddress類型。
????????由于局部變量表是建立在線程的棧上,是線程的私有數據,因此不存在數據安全問題。
????????局部變量表所需的容量大小是在編譯期確定下來的,并保存在方法的Code屬性的maximum local variables數據項中。在方法運行期間是不會改變局部變量表的大小的。?
????????方法嵌套調用的次數由棧的大小決定。一般來說,棧越大,方法嵌套調用次數越多。對一個函數而言,它的參數和局部變量越多,使得局部變量表膨脹,它的棧幀就越大,以滿足方法調用所需傳遞的信息增大的需求。進而函數調用就會占用更多的棧空間,導致其嵌套調用次數就會減少。
????????局部變量表中的變量只在當前方法調用中有效。在方法執行時,虛擬機通過使用局部變量表完成參數值到參數變量列表的傳遞過程。當方法調用結束后,隨著方法棧幀的銷毀,局部變量表也會隨之銷毀。
????????在棧幀中,與性能調優關系最為密切的部分就是局部變量表。在方法執行時,康擬機使用局部變量表完成方法的傳遞。局部變量表中的變量也是重要的垃圾回收根節點,只要被局部變量表中直接或間接引用的對象都不會被回收。?
6.1.2 實戰
? ? ? ? 咱們利用?jclasslib Bytecode Viewer 插件來看看局部變量表長什么樣。先編寫一段代碼:
public class StackTest {public static void main(String[] args) {StackTest test = new StackTest();test.methodA();}public void methodA() {int i = 10;int j = 20;methodB();}public void methodB(){int k = 30;int m = 40;} }? ? ? ? ?可以看到main方法中有兩個變量:args和test,按照他們聲明的先后順序,依次占據表中的序號(index)0和1;起始PC(start PC)是變量在字節碼指令中開始的行號;長度(length)是在字節碼指令中占據的長度。
6.2 變量槽slot?
6.2.1?slot的介紹?
? ? ? ? 局部變量表最基本的存儲單元是slot(變量槽),參數值的存放總是在局部變量數組的index0開始,到數組長度-1的索引結束
????????局部變量表中存放編譯期可知的各種基本數據類型(8種),引用類型(reference),returnAddress類型的變量
????????在局部變量表里,32位以內的類型只占用一個slot (包括returnAddress類型),64位的類型(long和double)占用兩個slot。byte , short , char在存儲前被轉換為int;boolean也被轉換為int,0 表示false ,非0表示true;long和double 則占據兩個slot。
????????JVM會為局部變量表中的每一個slot都分配一個訪問索引,通過這個索引即可成功訪問到局部變量表中指定的局部變量值。當一個實例方法被調用的時候,它的方法參數和方法體內部定義的局部變量將會按照順序被復制到局部變量表中的每一個slot上。如果需要訪問局部變量表中一個64bit的局部變量(比如:訪問long或double類型變量)值時,只需要使用前一個索引即可。
????????如果當前幀是由構造方法或者實例方法創建的,那么該對象引用this將會存放在index為0的slot處,其余的參數按照參數表順序繼續排列。
6.2.2?slot的測試?
? ? ? ? 下面我們把前面的介紹一一測試。
public class LocalVariablesTest {private int count = 0;public static void main(String[] args) {LocalVariablesTest test = new LocalVariablesTest();int num = 10;test.test1();}/*** 用于解釋為什么靜態方法不能用this調用*/public static void testStatic(){LocalVariablesTest test = new LocalVariablesTest();Date date = new Date();int count = 10;System.out.println(count);//因為this變量不存在于當前方法的局部變量表中!! // System.out.println(this.count);}public LocalVariablesTest(){this.count = 1;}/*** 用于展示this變量存放在index為0的位置*/public void test1() {Date date = new Date();String name1 = "atguigu.com";test2(date, name1);System.out.println(date + name1);}/*** 用于展示double類型變量占兩個slot* @param dateP* @param name2* @return*/public String test2(Date dateP, String name2) {dateP = null;name2 = "songhongkang";double weight = 130.5;//占據兩個slotchar gender = '男';return dateP + name2;}/*** 用于展示全局變量*/public void test3() {this.count++;} }? ? ? ? ?test1的局部變量表:
? ? ? ? ?test2的局部變量表:
????????test3的局部變量表:
6.2.3?slot的重復利用
- 介紹?
????????棧幀中的局部變量表中的槽位是可以重用的。如果一個局部變量過了其作用域,那么在其作用域之后申明的新的局部變量就很有可能會復用過期局部變量的槽位,從而達到節省資源的目的。
- 演示
? ? ? ? 我們看下面的代碼,代碼里有三個變量,再加上不是靜態方法,還有this變量,應該一共是4個變量。但是我們看看局部變量表:?
public void test4() {int a = 0;{int b = 0;b = a + 1;}//變量c使用之前已經銷毀的變量b占據的slot的位置int c = a + 1;}? ? ? ? ?發現只有三個序號。從起始位置可以看出,c占了b的位置。這說明,b一出大括號就離開了自己的作用域,被銷毀了,新定義的c重復利用的b之前的位置。
6.2.4?類變量和局部變量的區別?
? ? ? ? ?參考我的文章:面試常問:Java中實例變量和局部變量的區別
七、操作數棧(Operand Stack)
7.1 介紹?
????????每一個獨立的棧幀中除了包含局部變量表以外,還包含一個后進先出的操作數棧(Operand Stack),也可以稱之為表達式棧(Ezpression Stack) 。
? ? ? ? 我們都知道,棧可以用數組或鏈表實現。操作數棧是用數組實現的。 但是操作數棧并非采用訪問索引的方式來進行數據訪問的(因為是棧),而是只能通過標準的入棧(push)和出棧(pop)操作來完成一次數據訪問。
????????棧中的任何一個元素都是可以任意的Java數據類型。32bit的類型占用一個棧單位深度,64bit的類型占用兩個棧單位深度。? ? ? ?
7.2 原理?
????????操作數棧主要用于保存計算過程的中間結果,同時作為計算過程中變量臨時的存儲空間。?
????????操作數棧在方法執行過程中,根據字節碼指令往棧中寫入數據或從棧中提取數據,即入棧(push)/出棧(pop)。一些字節碼指令將值壓入操作數棧;其余的字節碼指令將操作數取出棧,使用它們后再把結果壓入棧(比如:執行復制、交換、求和等操作)。例如:
?
????????操作數棧就是JVM執行引擎的一個工作區,當一個方法剛開始執行的時候,一個新的棧幀也會隨之被創建出來,這個方法的操作數棧是空的。每一個操作數棧都會擁有一個明確的棧深度用于存儲數值,其所需的最大深度(數組長度)在編譯期就定義好了,保存在方法的code屬性中,為max_stack的值。
????????如果被調用的方法帶有返回值的話,其返回值將會被壓入當前棧幀的操作數棧中,并更新PC寄存器(程序計數器)中下一條需要執行的字節碼指令。
????????操作數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配,這由編譯器在編譯器期間進行驗證,同時在類加載過程中的類檢驗階段的數據流分析階段要再次驗證。
????????另外,我們說Java虛擬機的解釋引擎是基于棧的執行引擎,其中的棧指的就是操作數棧。
7.3 字節碼層面逐步分析
? ? ? ? 首先編寫一段簡單的代碼,然后來分析每一行字節碼中,程序計數器、局部變量表、操作數棧都做了什么。?
public class OperandStackTest {public void testAddOperation() {//byte、short、char、boolean:都以int型來保存byte i = 15;int j = 8;int k = i + j;} }? ? ? ? 可以使用反編譯命令查看字節碼 javap -v xxx.java,也可以直接使用jclasslib工具或插件查看字節碼:
??
? ? ? ? 首先看第一行,0 bipush 15。程序計數器中保存指令地址0,局部變量表中開辟三個變量的內存空間;bipush中,bi表示將byte類型轉換為int類型,push表示操作數棧中將15入棧。
?
? ? ? ? 再看第二行,2 istore_1。程序計數器保存指令地址2;istore_1中,i表示棧頂是int類型,store表示要操作數棧彈出15,保存到局部變量表中,1表示保存到索引1位置(因為索引0位置保存的是this)。
?
? ? ? ? 3 bipush 8。程序計數器中保存指令地址3,操作數棧中將8入棧。
?
? ? ? ? 5 istore_2。程序計數器保存指令地址5,操作數棧彈出8,保存到局部變量表的索引2位置;
?
? ? ? ? ?6 iload_1 和 7 iload_2。表示將局部變量表中索引1位置和索引2位置的取出,壓入操作數棧中。
?
?
? ? ? ? iadd。操作數棧中彈出8和15,由執行引擎將字節碼翻譯為機器指令,交由CPU完成加法運算,運算結果壓入操作數棧中?
?
????????istore_3?。操作數棧彈出23,保存到局部變量表中索引為3的位置
??
? ? ? ? ?return。方法沒有返回值,return結束。
7.4 對方法返回值的處理
? ? ? ? 前面的介紹中提到:如果被調用的方法帶有返回值的話,其返回值將會被壓入當前棧幀的操作數棧中,我們一起來驗證一下:
? ? ? ? 在下面這段代碼中,getSum方法是有返回值的。?
public class OperandStackTest {public int getSum(){int m = 10;int n = 20;int k = m + n;return k;}public void testGetSum(){//獲取上一個棧楨返回的結果,并保存在操作數棧中int i = getSum();int j = 10;} }? ? ? ? 先看getSum()的字節碼。主要是注意最后的ireturn,說明返回類型是一個int類型,將其保存在操作數棧中。
?
? ? ? ? 再看看testGetSum()的字節碼。?一上來就做了aload_0的操作,load就是從操作數棧中加載數據。
?
7.5 字節碼中對int類型的理解
????????需要注意的一個點是,虛擬機會將int類型變量理解為不同的類型:
? ? ? ? 例如這里有一段代碼:
public class OperandStackTest {public void testAddOperation() {int m = 8;} }? ? ? ? bipush表示先將8理解為是一個byte類型,再轉換為int類型
?
? ? ? ? 如果是800:?
public class OperandStackTest {public void testAddOperation() {int m = 800;} }? ? ? ? ?字節碼為sipush,說明理解為一個short類型,再轉換為int類型。
??
? ? ? ? 跟直接定義為short產生的字節碼是一樣的:
public class OperandStackTest {public void testAddOperation() {short m = 800;} }7.6 棧頂緩存技術(TOS,Top-of-Stack Cashing)
????????我們知道,基于棧式架構的虛擬機所使用的零地址指令比基于寄存器架構的更加緊湊,但完成一項操作的時候必然需要使用更多的入棧和出棧指令,這同時也就意味著將需要更多的指令分派(instruction dispatch)次數和內存讀/寫次數。
????????由于操作數是存儲在內存中的,因此頻繁地執行內存讀/寫操作必然會影響執行速度。為了解決這個問題,HotSpot JVM的設計者們提出了棧頂緩存(Tos,Top-of-stack Cashing)技術,將棧頂元素全部緩存在物理CPU的寄存器中,以此降低對內存的讀/寫次數,提升執行引擎的執行效率。?
八、動態鏈接(Dynamic Linking,指向運行時常量池的方法引用)?
8.1 介紹?
????????每一個棧幀內部都包含一個指向運行時常量池中該棧幀所屬方法的引用。包含這個引用的目的就是為了支持當前方法的代碼能夠實現動態鏈接( Dynamic Linking)。比如: invokedynamic指令。
????????在Java源文件被編譯到字節碼文件中時,所有的變量和方法引用都作為符號引用(Symbolic Reference)保存在class文件的常量池里。比如:描述一個方法調用了另外的其他方法時,就是通過常量池中指向方法的符號引用來表示的,那么動態鏈接的作用就是將這些符號引用轉換為調用方法的直接引用。?
? ? ? ? 動態鏈接 也叫做 指向運行時常量池的方法引用。下圖中藍色的部分即是棧幀,橙色部分就是動態鏈接。
8.2 演示?
? ? ? ? 看看運行時常量池。首先準備一段Java代碼:
public class DynamicLinkingTest {int num = 10;public void methodA(){System.out.println("methodA()....");}public void methodB(){System.out.println("methodB()....");methodA();num++;} }? ? ? ? ?使用命令 javap -v .\DynamicLinkingTest.class 反編譯DynamicLinkingTest.class文件,找到常量池。左邊為符號引用,右邊為真實引用。
? ? ? ? 動態鏈接其實就是把類加載的時候需要使用到的一些信息作為符號加載出來,在方法中具體要引用誰,再在使用的時候指明
8.3 常量池的作用
????????常量池的作用就是提供一些符號和常量,便于指令的識別。
九、方法的調用
9.1 早期綁定與晚期綁定?
????????在JVM中,將符號引用轉換為調用方法的直接引用與方法的綁定機制相關。
- 靜態鏈接:當一個字節碼文件被裝載進JVM內部時,如果被調用的目標方法在編譯期可知,且運行期保持不變時。這種情況下將調用方法的符號引用轉換為直接引用的過程稱之為靜態鏈接。
- 動態鏈接:如果被調用的方法在編譯期無法被確定下來,也就是說,只能夠在程序運行期將調用方法的符號引用轉換為直接引用,由于這種引用轉換過程具備動態性,因此也就被稱之為動態鏈接。
????????對應的方法的綁定機制為:早期綁定(Early Binding)和晚期綁定(Late Binding)。綁定是一個字段、方法或者類在符號引用被替換為直接引用的過程,這僅僅發生一次。
- 早期綁定:早期綁定就是指被調用的目標方法如果在編譯期可知,且運行期保持不變時即可將這個方法與所屬的類型進行綁定,這樣一來,由于明確了被調用的目標方法究竟是哪一個,也就可以使用靜態鏈接的方式將符號引用轉換為直接引用。
- 晚期綁定:如果被調用的方法在編譯期無法被確定下來,只能夠在程序運行期根據實際的類型綁定相關的方法,這種綁定方式也就被稱之為晚期綁定。
? ? ? ? 下面的例子就可以解釋早期綁定和晚期綁定
class Animal{public void eat(){System.out.println("動物進食");} } interface Huntable{void hunt(); } class Dog extends Animal implements Huntable{@Overridepublic void eat() {System.out.println("狗吃骨頭");}@Overridepublic void hunt() {System.out.println("捕食耗子,多管閑事");} }class Cat extends Animal implements Huntable{public Cat(){super();//表現為:早期綁定}public Cat(String name){this();//表現為:早期綁定}@Overridepublic void eat() {super.eat();//表現為:早期綁定System.out.println("貓吃魚");}@Overridepublic void hunt() {System.out.println("捕食耗子,天經地義");} } public class AnimalTest {//只有知道傳入的animal是什么,才能知道結果是什么public void showAnimal(Animal animal){animal.eat();//表現為:晚期綁定}public void showHunt(Huntable h){h.hunt();//表現為:晚期綁定} }????????隨著高級語言的橫空出世,像Java一樣的基于面向對象的編程語言越來越多,盡管這類編程語言在語法風格上存在一定的差別,但是它們彼此之間始終保持著一個共性:那就是都支持封裝、繼承和多態等面向對象特性。既然這一類的編程語言具備多態特性,那么自然也就具備早期綁定和晚期綁定兩種綁定方式。
????????Java中任何一個普通的方法其實都具備虛函數的特征,它們相當于C++語言中的虛函數(C++中則需要使用關鍵字virtual來顯式定義)。如果在Java程序中不希望某個方法擁有虛函數的特征時,則可以使用關鍵字final來標記這個方法。
9.2 虛方法和非虛方法
9.2.1 概念?
????????非虛方法:如果方法在編譯期就確定了具體的調用版本,這個版本在運行時是不可變的,這樣的方法稱為非虛方法。靜態方法、私有方法、final方法、實例構造器、父類方法都是非虛方法。
????????其他方法稱為虛方法。
????????虛擬機中提供了以下幾條方法調用指令:
9.2.2 字節碼指令介紹
- 普通調用指令:
- 動態調用指令:
????????前四條指令固化在虛擬機內部,方法的調用執行不可人為千預,而invokedynamic指令則支持由用戶確定方法版本。其中invokestatic指令和invokespecial指令(藍色字體)調用的方法稱為非虛方法,其余的( final修飾的除外)稱為虛方法。
9.2.3 普通調用指令演示
class Father {public Father() {System.out.println("father的構造器");}public static void showStatic(String str) {System.out.println("father " + str);}public final void showFinal() {System.out.println("father show final");}public void showCommon() {System.out.println("father 普通方法");} }public class Son extends Father {public Son() {//invokespecialsuper();}public Son(int age) {//invokespecialthis();}//不是重寫的父類的靜態方法,因為靜態方法不能被重寫!public static void showStatic(String str) {System.out.println("son " + str);}private void showPrivate(String str) {System.out.println("son private" + str);}public void show() {//invokestaticshowStatic("atguigu.com");//invokestaticsuper.showStatic("good!");//invokespecialshowPrivate("hello!");//invokespecialsuper.showCommon();//invokevirtual 因為此方法聲明有final,不能被子類重寫,所以也認為此方法是非虛方法。showFinal();//虛方法如下://invokevirtualshowCommon();info();MethodInterface in = null;//invokeinterfacein.methodA();}public void info(){}public void display(Father f){f.showCommon();}public static void main(String[] args) {Son so = new Son();so.show();} }interface MethodInterface{void methodA(); }? ? ? ? 運行結果:
father的構造器 son atguigu.com father good! son privatehello! father 普通方法 father show final father 普通方法 Exception in thread "main" java.lang.NullPointerExceptionat com.atguigu.java2.Son.show(Son.java:64)at com.atguigu.java2.Son.main(Son.java:77)? ? ? ? 字節碼:
9.2.4 invokedynamic指令介紹
????????JVM字節碼指令集一直比較穩定,一直到Java7中才增加了一個 invokedynamic指令,這是Java為了實現 “動態類型語言” 支持而做的一種改進。
????????但是在Java7中并沒有提供直接生成invokedynamic指令的方法,需要借助ASM這種底層字節碼工具來產生invokedynamic指令。直到Java8 Lambda表達式的出現,invokedynamic才在Java中才有了直接的生成方式。
????????Java7中增加的動態語言類型支持的本質是對Java虛擬機規范的修改,而不是對Java語言規則的修改,這一塊相對來講比較復雜,增加了虛擬機中的方法調用,最直接的受益者就是運行在Java平臺的動態語言的編譯器。
? ? ? ? 什么是動態類型語言??
????????動態類型語言和靜態類型語言兩者的區別就在于對類型的檢查是在編譯期還是在運行期,滿足前者就是靜態類型語言,反之是動態類型語言。
????????說的再直白一點就是,靜態類型語言是判斷變量自身的類型信息;動態類型語言是判斷變量值的類型信息,變量沒有類型信息,變量值才有類型信息,這是動態語言的一個重要特征。?
? ? ? ? Java本質上還是靜態類型語言,雖然他在一定程度上支持動態類型。
9.2.5 invokedynamic指令演示?
? ? ? ? 在Java8中使用lambda表達式,JVM就會使用invokedynamic指令。
@FunctionalInterface interface Func {public boolean func(String str); }public class Lambda {public void lambda(Func func) {return;}public static void main(String[] args) {Lambda lambda = new Lambda();Func func = s -> {return true;};lambda.lambda(func);lambda.lambda(s -> {return true;});} }?
?9.3 方法重寫的本質
? ? ? ? Java語言中方法重寫的原理:
? ? ? ? IllegalAccessError介紹:
????????程序試圖訪問或修改一個屬性或調用一個方法,而這個屬性或方法沒有權限訪問,一般會引起編譯器異常。這個錯誤如果發生在運行時,就說明一個類發生了不兼容的改變。
9.4 虛方法表
9.4.1 虛方法表介紹?
????????在面向對象的編程中,會很頻繁的使用到動態分派,如果在每次動態分派的過程中都要重新在類的方法元數據中搜索合適的目標的話就可能影響到執行效率。因此,為了提高性能,JVM在類的方法區建立一個虛方法表(virtual method table)(非虛方法不會出現在表中),使用索引表來代替查找。
????????每個類中都有一個虛方法表,表中存放著各個方法的實際入口。
????????那么虛方法表什么時候被創建?虛方法表會在類加載的鏈接階段被創建并開始初始化,類的變量初始值準備完成之后,JVM會把該類的方法表也初始化完畢。對類加載的各個階段的介紹在我的這篇文章中:JVM學習(六):類加載子系統_玉面大蛟龍的博客-CSDN博客
9.4.2 虛方法表示例?
? ? ? ? 我們來具體看看虛方法表起什么作用。如圖,Son類繼承Father類,Father類繼承自Object類。Son中的虛方法(藍色背景的方法)由于虛方法表的存在,會直接指向祖先類,而非虛方法仍指向本身。?
十、方法返回地址(Return Address)
10.1? 方法返回地址介紹
? ? ? ? 方法返回地址 存放調用該方法的PC寄存器的值。
????????一個方法的結束,有兩種方式:
- 正常執行完成
- 出現未處理的異常,非正常退出
????????無論通過哪種方式退出,在方法退出后都返回到該方法被調用的位置。方法正常退出時,調用者的pc計數器的值作為返回地址,即調用該方法的指令的下一條指令的地址。而通過異常退出的,返回地址是要通過異常表來確定,棧幀中一般不會保存這部分信息。 因此方法返回地址主要針對的是正常執行完成的方法。
10.2 方法的退出
????????當一個方法開始執行后,只有兩種方式可以退出這個方法:
10.2.1 正常完成出口?
????????執行引擎遇到任意一個方法返回的字節碼指令(return),會有返回值傳遞給上層的方法調用者,簡稱正常完成出口;
????????一個方法在正常調用完成之后究竟需要使用哪一個返回指令還需要根據方法返回值的實際數據類型而定。在字節碼指令中,返回指令包含ireturn(當返回值是boolean、byte、char.short和int類型時使用)、lreturn、 freturn、dreturn以及areturn,另外還有一個return指令供聲明為void的方法、實例初始化方法、類和接口的初始化方法使用。
? ? ? ? 我們來看看這些返回指令:
public class ReturnAddressTest {//ireturnpublic boolean methodBoolean() {return false;}//ireturnpublic byte methodByte() {return 0;}//ireturnpublic short methodShort() {return 0;}//ireturnpublic char methodChar() {return 'a';}//ireturnpublic int methodInt() {return 0;}//lreturnpublic long methodLong() {return 0L;}//freturnpublic float methodFloat() {return 0.0f;}//dreturnpublic double methodDouble() {return 0.0;}//areturnpublic String methodString() {return null;}//areturnpublic Date methodDate() {return null;}//returnpublic void methodVoid() {}//returnstatic {int i = 10;} }? ? ? ? 圖就不全截了,放一張意思意思。?
?
10.2.2 異常完成出口
????????在方法執行的過程中遇到了異常(Exception),并且這個異常沒有在方法內進行處理,也就是只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會導致方法退出。簡稱異常完成出口。
????????方法執行過程中拋出異常時的異常處理,存儲在一個異常處理表中,方便在發生異常的時候找到處理異常的代碼。
? ? ? ? 下面我們來看看異常表長什么樣,先加一段可能出現異常的代碼:
public void method2() {try {method1();} catch (IOException e) {e.printStackTrace();}}public void method1() throws IOException {FileReader fis = new FileReader("atguigu.txt");char[] cBuffer = new char[1024];int len;while ((len = fis.read(cBuffer)) != -1) {String str = new String(cBuffer, 0, len);System.out.println(str);}fis.close();}? ? ? ? ?反編譯查看:?
??
? ? ? ? ?也可以使用jclasslib工具查看異常處理表:
10.2.3 方法退出的本質
????????本質上,方法的退出就是當前棧幀出棧的過程。此時,需要恢復上層方法的局部變量表、操作數棧、將返回值壓入調用者棧幀的操作數棧、設置PC寄存器值等,讓調用者方法繼續執行下去。正常完成出口和異常完成出口的區別在于:通過異常完成出口退出的不會給他的上層調用者產生任何的返回值。
十一、一些附加信息?
????????棧幀中還允許攜帶與Java虛擬機實現相關的一些附加信息。例如,對程序調試提供支持的信息。?
? ? ? ? 這個不一定都有,在一些介紹棧幀的資料當中甚至省略了這部分。
十二、棧相關的面試題?
- 舉例棧溢出的情況? (StackOverflowError)
? ? ? ? 遞歸調用死循環
- 調整棧大小,就能保證不出現溢出嗎?
????????不能。遞歸調用死循環給多大內存都沒用。
- 分配的棧內存越大越好嗎?
? ? ? ? 不是。會擠占其他內存結構的空間。
- 垃圾回收是否會涉及到虛擬機棧?
? ? ? ? 不會。棧有OOM問題,但不會發生GC
- 方法中定義的局部變量是否線程安全?
????????具體問題具體分析。咱們使用線程不安全的StringBuilder類來討論這個問題:
public class StringBuilderTest {int num = 10;//s1的聲明方式是線程安全的,因為只有一個線程會操作s1public static void method1(){StringBuilder s1 = new StringBuilder();s1.append("a");s1.append("b");//...}//sBuilder的操作過程:是線程不安全的,因為有可能會有多個線程同時調用method2public static void method2(StringBuilder sBuilder){sBuilder.append("a");sBuilder.append("b");//...}//s1的操作:是線程不安全的,因為s1被返回出去之后,可能會有好幾個線程同時來操作它public static StringBuilder method3(){StringBuilder s1 = new StringBuilder();s1.append("a");s1.append("b");return s1;}//s1的操作:是線程安全的,因為toString方法的底層是再new一個String,String是不可變的public static String method4(){StringBuilder s1 = new StringBuilder();s1.append("a");s1.append("b");return s1.toString();}public static void main(String[] args) {StringBuilder s = new StringBuilder();new Thread(() -> {s.append("a");s.append("b");}).start();method2(s);}}總結
以上是生活随笔為你收集整理的JVM学习(八):虚拟机栈(字节码程度深入剖析)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 漫画|图灵奖是怎么来的?
- 下一篇: 招聘画像之Java实习生