首頁 > 軟體

Java方法呼叫解析靜態分派動態分派執行過程

2022-06-23 18:00:03

方法呼叫

在程式執行時,進行方法呼叫是最普遍,最頻繁的操作

方法呼叫不等於方法執行:

  • 方法呼叫階段唯一的任務就是確定被呼叫的方法版本,即呼叫哪一個方法
  • 不涉及方法內部的具體執行過程

Class檔案的編譯過程不包括傳統編譯中的連線步驟

Class檔案中的一切方法呼叫在Class檔案裡面儲存的都是符號參照,而不是方法在在實際執行時記憶體佈局中的入口地址,即之前的直接參照:

  • 這樣使得Java具有更強大的動態擴充套件能力
  • 同時也使得Java方法呼叫過程變得相對複雜
  • 需要在類載入期間,甚至會到執行期間才能確定目標方法的直接參照

方法解析

所有方法呼叫中的目標方法在Class檔案裡都是一個常數池的參照

在類的載入解析階段,會將其中的一部分符號參照轉化為直接參照:

方法在程式真正執行之前就有一個可確定的呼叫版本,並且這個方法的呼叫版本在執行期是不可改變的

也就是說,呼叫目標在程式程式碼中完成,編譯器進行編譯時就必須確定下來,這也叫做方法解析

Java方法分類

在Java中符合 "編譯期可知,執行期不可變" 的方法有兩大類:

  • 靜態方法: 與型別直接關聯
  • 私有方法: 在外部不可被存取

這兩種方法各自的特點決定這兩種方法都不可能通過繼承或者別的方式重寫版本,因此適合在類載入階段進行解析

非虛方法: 在類載入階段會把符號參照解析為該方法的直接參照

  • 靜態方法
  • 私有方法
  • 範例構造器
  • 父類別方法

虛方法: 在類載入階段不會將符號參照解析為該方法的直接參照

除去以上的非虛方法,其它的方法均為虛方法

靜態分派

public class StaticDispatch {
	static abstract class Human {
	}
	static class Man extends Human {
	}
	static class Woman extends Human {
	}
	public static void sayHello(Human guy) {
		System.out.println("Hello, Guy!");
	}
	public static void sayHello(Man guy) {
		System.out.println("Hello, Gentleman!");
	}
	public static void sayHello(woman guy) {
		System.out.println("Hello, Lady!");
	}
	public static void main(String[] args) {
		Human man = new Man();
		Human women = new Woman();
		sayHello(man);
		sayHello(woman);
	}
}

Human man = new Human();

Human為變數的靜態型別

Man為變數的實際型別

靜態型別和實際型別在程式中都會放生變化:

靜態型別:

  • 靜態型別的變化僅僅在使用時發生
  • 變數本身的靜態型別不會被改變
  • 最終的靜態型別在編譯器中可知

實際型別:

  • 實際型別變化的結果在執行期才確定下來
  • 編譯器在編譯期間並不知道一個物件的實際型別是什麼
Human human = new Man();
sayHello(man);
sayHello((Man)man);		// 型別轉換,靜態型別變化,轉型後的靜態型別一定是Man
man = new woman();		// 實際型別變化,實際型別是不確定的
sayHello(man);
sayHello((Woman)man);	// 型別轉換,靜態型別變化

編譯器在過載時是通過引數的靜態型別而不是實際型別作為判斷依據,靜態型別在編譯期間可以知道:

編譯階段,Javac編譯器會根據引數的靜態型別決定使用哪個過載版本

靜態分派:

  • 所有依賴靜態型別來定位方法的執行版本的分派動作
  • 典型應用 :方法過載

靜態分派發生在編譯階段,因此確定靜態分派的的動作不是由虛擬機器器執行的,而是由編譯器完成的

由於字面量沒有顯示靜態型別,只能通過語言上的規則去理解和推斷

public class LiteralTest {
	public static void sayHello(char arg) {
		System.out.println("Hello, char!");
	}
	public static void sayHello(int arg) {
		System.out.println("Hello, int!");
	}
	public static void sayHello(long arg) {
		System.out.println("Hello, long!");
	}
	public static void sayHello(Character arg) {
		System.out.println("Hello, Character!");
	}
	public static void main(String[] arg) {
		sayHello('a');
	}
}

編譯器將過載方法從上向下依次註釋,得到不同的輸出

如果編譯器無法確定要自定轉型為哪種型別,會提示型別模糊,拒絕編譯

public class LiteralTest {
	public static void sayHello(String arg) {	// 新增過載方法
		System.out.println("Hello, String!");
	}
	public static void sayHello(char arg) {	
		System.out.println("Hello, char!");
	}
	public static void sayHello(int arg) {
		System.out.println("Hello, int!");
	}
	public static void sayHello(long arg) {
		System.out.println("Hello, long!");
	}
	public static void sayHello(Character arg) {
		System.out.println("Hello, Character!");
	}
	public static void main(String[] args) {
		Random r = new Random();
		String s = "abc";
		int i = 0;
		sayHello(r.nextInt() % 2 != 0 ? s : 1 );	// 編譯錯誤
		sayHello(r.nextInt() % 2 != 0 ? 'a' : false);	//編譯錯誤
	}
}

動態分派

public class DynamicDispatch {
	static abstract class Human {
		protected abstract void sayHello();
	}
	static class Man extends Human {
		@override
		protected void sayHello() {
			System.out.println("Man Say Hello!");
		}
	}
	static class Woman extends Human {
		@override
		protected void sayHello() {
			System.out.println("Woman Say Hello!");
		}
	}
	public static void main(String[] args) {
		Human man = new Man();
		Human women = new Woman();
		man.sayHello();
		woman.sayHello();
		man = new Woman();
		man.sayHello();
	}
}

這裡不是根據靜態型別決定的

  • 靜態型別的Human兩個變數man和woman在呼叫sayHello() 方法時執行了不同的行為
  • 變數man在兩次呼叫中執行了不同的方法

導致這個現象的額原因 :這兩個變數的實際型別不同

Java虛擬機器器是如何根據實際型別分派方法的執行版本的: 從invokevirtual指令的多型查詢過程開始 ,invokevirtual指令執行時解析過程大致分為以下幾個步驟:

  • 找到運算元棧頂的第一個元素所指向的物件的實際型別,記作C
  • 如果在型別C中找到與常數中的描述符和簡單名稱相符合的方法,然後進行存取許可權驗證,如果驗證通過則返回這個方法的直接參照,查詢過程結束;如果驗證不通過,則丟擲java.lang.illegalAccessError異常
  • 如果未找到,就按照繼承關係從下往上依次對型別C的各個父類別進行第二步的搜尋和驗證過程
  • 如果始終沒有找到合適的方法,則丟擲java.lang.AbstractMethodError異常

Java語言方法重寫的本質:

invokevirtual指令執行的第一步就是在執行時期確定接收者的實際型別,所以兩次呼叫中的invokevirtual指令把常數池中的類方法符號參照解析到了不同的直接參照上

這種在執行時期根據實際型別確定方法執行版本的分派過程就叫做動態分派

虛擬機器器動態分派的實現

虛擬機器器概念解析的模式就是靜態分派和動態分派,可以理解虛擬機器器在分派中 "會做什麼" 這個問題

虛擬機器器 "具體是如何做到的" 在各種虛擬機器器實現上會有差別:

  • 由於動態分派是非常頻繁的動作,而且動態分派的方法版本選擇過程需要執行時在類的方法後設資料中搜尋合適的目標方法
  • 因此在虛擬機器器的實際實現中,為了基於效能的考慮,大部分實現都不會真正的進行如此頻繁的搜尋
  • 最常用的"穩定優化"的方式是為類在方法區中建立一個虛方法表(Virtual Method Table,即vtable), 使用虛方法表索引代替後設資料查詢以提高效能

虛方法表中存放著各個方法的實際入口地址:

  • 如果某個方法在子類中沒有被重寫,那子類的虛方法表裡面的地址入口和父類別相同方法的地址入口是一致的,都指向父類別的實際入口
  • 如果子類中重寫了這個方法,子類方法表中的地址將會替換為指向子類實際方法的入口地址

具有相同簽名的方法,在父類別,子類的虛方法表中具有一樣的索引序號:

這樣當型別變換時,僅僅需要變更查詢的方法表,就可以從不同的虛方法表中按索引轉換出所需要的入口地址

方法表一般在類載入階段的連線階段進行初始化:

準備了類的變數初始值後,虛擬機器器會把該類的方法表也初始化完畢

以上就是Java方法呼叫解析靜態分派動態分派執行過程的詳細內容,更多關於Java靜態動態分派執行過程的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com