動態鏈接–指向運行時常量池的方法引用
- 每一個棧幀內部包含一個指向運行時常量池中該棧幀所屬方法的引用,包含這個引用的目的為了支持當前方法的代碼能夠實現動態鏈接(Dynamic Linking),如invokednamic指令。
- 在Java源文件被編譯到字節碼文件中時,所有的變量和方法引用都作為符號引用保存在class文件的常量池中,比如一個方法調用了另一個方法,就是通過常量池中方法的符號引用來表示的,動態鏈接的作用是為了將這些符號引用轉換為調用方法的直接引用。
為什么需要常量池?
常量池的作用,是為了提供一些符號和常量,便于指令的識別
方法的調用
在JVM中,將符號引用轉換為調用方法的直接引用與方法的綁定機制相關,綁定機制分為早期綁定和晚期綁定,綁定是一個字段、方法或者類在符號引用被替換為直接引用的過程,這僅僅發生一次。
- 早期綁定:指被調用的目標方法如果在編譯期可知,且運行期保持不變時,即可將這個方法與所屬的類型進行綁定,這樣一來,由于明確了被調用的目標方法究竟是哪一個,因此也可以使用靜態鏈接的方式將符號引用轉換為直接引用
- 晚期綁定:如果被調用的方法在編譯期無法被確定下來,只能夠在程序運行期根據實際的類型綁定相關的方法,稱為晚期綁定
靜態鏈接
當一個字節碼文件被裝載進JVM內部時,如果被調用的目標方法在編譯期可知,且運行期間保持不變,這種情況下將調用方法的符號引用轉換為直接引用的過程稱為靜態鏈接
動態鏈接
如果被調用方法在編譯期無法被確定下來,只能夠在程序運行期調用方法的符號引用轉換為直接引用,由于這種引用轉換過程具備動態性,因此被稱為動態鏈接
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 {public void showAnimal(Animal animal) {animal.eat(); //晚期綁定}public void showHunt(Huntable h) {h.hunt(); //晚期綁定}
}
虛方法和非虛方法
-
非虛方法:方法在編譯期就確定了具體的調用版本,這個版本在運行時是不可變的,這種方法稱為非虛方法。
-
靜態方法、私有方法、final方法、實例構造器、父類方法都是非虛方法。
-
其它方法為虛方法
-
普通調用指令
- invokestatic:調用靜態方法,解析階段確定唯一方法版本
- invokespecial:調用方法,私有及父類方法,解析階段確定唯一方法版本
- invokevirtual:調用所有虛方法
- invokeinterface:調用接口方法
-
動態調用指令
- invokedynamic:動態解析出需要調用的方法,然后執行
普通調用指令固化在虛擬機內部,方法的調用執行不可人為干預,而invokedynamic指令則支持由用戶確定方法版本,其中invokestatic指令和invokespecial指令調用的方法稱為非虛方法,其余的(final修飾的除外)稱為虛方法
靜態類型語言是判斷變量自身的類型信息,動態類型語言是判斷變量值的類型信息,變量沒有類型信息,變量值才有類型信息。
- invokedynamic:動態解析出需要調用的方法,然后執行
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() {super();}public Son(int age) {this();}//非重寫父類的方法,靜態方法不允許重寫public static void showStatus(String str) {System.out.println("son " + str);}private void showPrivate(String str) {System.out.println("son private " + str);}public void show() {//invokestaticshowStatic("lotus.com");//invokestaticsuper.showStatic("goods!");//invokespecialshowPrivate("hellow!");//invokespecialsuper.showCommon();//invokevirtual,此方法聲明有final,不能被子類重寫,此方法為非虛方法showFinal();//invokevirtualshowCommon();info();MethodInterface in = null;//invokeinterfacein.methodA();}private 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();
}
@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();//invokedynamicFunc func = s-> {return true;};//invokedynamiclambda.lambda(s-> {return true;});}
}
方法重寫本質
- 找到操作數棧頂的第一個元素所執行的對象的實際類型,記作C
- 如果過程結束,不通過類型C中找到與常量中的描述符符合簡單名稱都相符的方法,則進行訪問權限校驗,如果通過則返回這個方法的直接引用,查找不到,則返回java.lang.IllegalAccessError異常
- 否則,按繼承關系從下向上依次對C的各個父類進行第2步搜索和驗證過程
- 如果始終沒有找到合適的方法,則拋出java.lang.AbstractMethodError異常。
虛方法表
- 在面向對象的編程中,會頻繁的使用到動態分派,如果在每次動態分派的過程中都要重新在類的方法元數據中搜索合適的目標的話,就可能影響到執行效率,因此,為了提高性能,JVM采用在類的方法區建立一個虛方法表(非虛方法不會出現在表中)來實現,使用索引表代替查找
- 每個類中都有一個虛方法表,表中存放各個方法的實際入口
- 虛方法表會在類加載的鏈接階段被創建并開始初使化,類的變量初使值準備完成之后,JVM會把該類的方法一也初始化完畢
方法返回地址
- 存放調用該方法的PC寄存器的值
- 一個方法結束,有兩種方式:
- 正常執行完成
- 一個方法在正常調用完成之后究竟需要使用哪一個返回指令還需要根據方法返回值的實際數據類型而定
- 在字節碼指令中,返回指令包含ireturn(當返回值是boolean,byte,char,short,int類型時),lreturn,freturn,dreturn以及areturn,另外還有一個return指令供聲明為void方法,實例化初始化方法、類和接口的初始化方法使用。
- 出現未處理的異常,非正常退出
- 方法執行過程中拋出異常時的異常處理,存儲在一個異常處理表,方便在發生異常的時候找到處理異常的代碼。
- 正常執行完成
- 無論哪種方式退出,在方法退出后都返回到該方法被調用的位置,方法正常退出時,調用者PC寄存器的值做為返回地址,好調用該方法的指令的下一條指令地址,而通過異常退出的,返回地址是要通過異常表來確定,棧幀中一般不會保存這部分信息。
import java.io.FileReader;
import java.io.IOException;
import java.util.Date;/*** Administrator* 2024/5/17*/
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(){}static {int i = 10;}public void method2() {methodVoid();try {method1();} catch (IOException e) {e.printStackTrace();}}public void method1() throws IOException {FileReader fr = new FileReader("lotus.txt");char[] cBuffers = new char[1024];int len;while ((len = fr.read(cBuffers))!=-1) {String str = new String(cBuffers,0,len);System.out.println(str);}fr.close();}public static void main(String[] args) {}
}
方法返回地址本質上,方法的退出就是當前棧幀出棧的過程,此時,需要恢復上層方法的局部變量表、操作數棧、將返回值壓入調用者棧幀的操作數棧、設置PC寄存器值等,讓調用者方法繼續執行下去
正常完成出口和異常完成出口區別:通過異常完成出口退出的不會給他的上層調用者產生任何返回值