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