1. 程式人生 > >java靜態分配和動態分配

java靜態分配和動態分配

1.方法呼叫

先來說說java方法的呼叫,方法的呼叫不等於方法執行,方法呼叫階段唯一的任務是確定被呼叫方法的版本(即呼叫哪個方法,不是唯一的,確定一個“更加合適”的版本),不涉及方法內部的具體執行過程。 我們都是知道java檔案都需要編譯成class檔案,而一切方法呼叫在class檔案裡儲存的都是符號引用,而不是方法的實際執行時記憶體佈局的入口地址(相當於直接引用)。在類載入的解析階段,會將其中的一部分符號引用轉化為直接引用,這種解析成立的前提是:方法的程式真正執行之前就有一個可確認的呼叫版本,並且這個方法的呼叫版本在執行期是不可變的。換句話說,呼叫目標在程式程式碼寫好、編輯器進行編譯時就必須確認下來,這類方法呼叫的呼叫稱為解析。
在Java虛擬機器裡提供了5條呼叫方法位元組碼指令,分別如下。     invokestatic:呼叫靜態方法     invokespeciak: 呼叫例項構造器<init>方法、私用方法和父類方法     invokevirtual: 呼叫所有的虛方法     invokeinterface:呼叫介面時,會在執行再確定一個實現介面的物件     invokedynamic:現在執行時動態解析出呼叫點限定符引用的方法,再執行方法 只有被invokestatic和invokespecial指令呼叫的方法,可以在解析階段中確定呼叫的版本,符合這個條件的靜態方法、私有方法、例項構造器、父類方法。它們在類載入的解析時候就會把符號引用解析為直接引用。這些方法被稱為非虛方法。解析呼叫一定是一個靜態的過程,在編譯期間就完全確定,而分配呼叫可能是靜態的也可能是動態的

2.分派

Java是一門面向物件的程式語言,因為Java具備面向物件的3個基本特徵:封裝、繼承、多型。來看看虛擬機器如何通過分派確定“重寫”和”過載“方法的目標方法。

來看一個靜態分配的例子

package com.jvm;
/**
 * 靜態分派
 * @author renhj
 *
 */
public class StaticDispatch {
		
	static class Human {
	     
	}
	 
	static class Man extends Human {
	     
	}
	 
	static class Women extends Human {
	     
	}
	
	public void sayHello(Human guy) {
        System.out.println("hello, guy!");
    }
     
    public void sayHello(Man guy) {
        System.out.println("hello, man!");
    }
     
    public void sayHello(Women guy) {
        System.out.println("hello, women!");
    }
     
    
    public static void main(String[] args){
    	
        Human man = new Man(); 
        Human women = new Women();
         
        StaticDispatch sd = new StaticDispatch();
        sd.sayHello(man);   
        sd.sayHello(women);
 
    }
 
}
輸出結果:

這個答案是你心目中的答案嗎?

Human man = new Man(); 我們把上面程式碼中的“Human”稱為變數的靜態型別,後面的“Men”稱為變數的實際型別,靜態型別和實際型別在程式中都可以發生一些變化,區別是靜態型別的變化僅僅在使用時發生,變數本身的靜態型別不會改變,並且最終的靜態型別是在編譯期間可知的;而實際型別變化的結果在執行期間才可確定,編譯器在編譯程式時並不知道一個物件的實際型別是什麼。
再回到上面例子程式碼中,mian()中兩次呼叫sayHello()方法,在方法接受者已經確定是物件“src”的前提下,使用哪個過載版本,就完全取決於傳入引數的數量和資料型別。編譯器在過載時通過引數的靜態型別而不是實際型別作為判斷依據的,因此在編譯階段Java編譯器根據引數的靜態型別決定使用哪個過載版本。

我們使用javap命令輸出這個類的位元組碼,輸出結果位元組碼如下。


所有依賴靜態型別來定位方法執行版本的分派動作稱為靜態分配,靜態分配的典型動作是方法過載,靜態分派發生在編譯階段,雖然編譯器能確定方法的過載版本,但是很多情況下這個過載的版本並不是“唯一的”,往往只能確定一個“更加合適的”版本。產生這種模糊結論的主要原因是字面量不需要定義,所以字面量沒有顯示的靜態型別,它的靜態型別只能通過語言上的規則去理解和推斷。

“更加合適”版本例子

package com.jvm;
/**
 * 過載方法屁匹配優先順序
 * @author renhj
 *
 */
public class Verload {
	
	private static void sayHello(char arg){
		System.out.println("hello char");
	}

	private static void sayHello(Object arg){
		System.out.println("hello Object");
	}
	
	private static void sayHello(int arg){
		System.out.println("hello int");
	}
	
	private static void sayHello(long arg){
		System.out.println("hello long");
	}
	
	public static void main(String[] args) {
		
		sayHello('c');
	}

}
上面程式碼執行後,正常回輸出:hello char,如果註釋掉sayHello(char arg)方法,那輸出就會變成:hello int。

3.動態分配

我們接下來看一下動態分配的過程,它和多型性的另外一個重要體現--重寫(Override)有著密切的關係,先看例子。

package com.jvm;
/**
 * 動態分派
 * @author renhj
 *
 */
public class DynamicDispatch {
		
	static abstract class Human {
	    protected abstract void sayHello();
	}
	
	static class Man extends Human {
		
	    @Override
	    protected void sayHello() {
	        System.out.println("hello man!");
	    }	     
	}
	 
	static class Women extends Human {
	 
	    @Override
	    protected void sayHello() {
	        System.out.println("hello women!");
	    }	     
	}
     
    
    public static void main(String[] args){
    	
    	Human man = new Man();
        Human women = new Women();
         
        man.sayHello();
        women.sayHello();
        
        man = new Women();
        man.sayHello();
 
    }
 
}
執行結果:

hello man!
hello women!
hello women!
這個結果相信不會出乎任何人的意料,那Java虛擬機器是如何根據實際型別來分配方法執行版本的呢?我們使用javap命令輸出這個類的位元組碼,嘗試從中尋找答案,輸出結果位元組碼如下。


0~15主要是建立man和woman的儲存空間、呼叫Man和Woman型別的例項構造器,並將兩個例項存放在第一個和第二個區域性變量表Slot之中。接下來的16~21句是關鍵部分,16、20兩句分別是把剛剛穿件的兩個物件的引用壓到棧頂,17、21兩句是方法呼叫指令,這兩條呼叫指令從位元組角度來看,無論指令(invokevirtual)還是引數完全一樣,但是這兩條指令最終執行的目標方法並不相同,原因需要從invokevirtual指令的多型查詢過程開始說起,invokevirtual指令的執行時解析過程大致如下幾個步驟:

1). 找到運算元棧頂的第一個元素所指向的物件的實際型別,記作C.

2). 如果在型別C中找到與常量池中描述符和簡單名稱都相符的方法,則進行訪問許可權的校驗,如果校驗不通過,則返回java.lang.IllegaAccessError異常,校驗通過則直接返回方法的直接引用,查詢過程結束。

3). 否則,按照繼承關係從下往上一次對C的各個父類進行第二步驟的搜尋和驗證過程。

4). 如果始終還是沒有找到合適的方法直接引用,則丟擲java.lang.AbstractMethodError異常。

由於invokevirtual指令執行的第一步是在執行時確定接收者的實際型別,所以兩次中的invokevirtual指令把常量池中的類方法符號引用解析到不同的直接引用上,這個就是java語言中方法重寫的本質,我們把這種在執行期根據實際型別確定方法執行版本的分派過程稱為動態分派。

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

前面介紹的分派過程,作為對虛擬機器概念模型的解析基本上已經足夠了,它已經解決了虛擬機器在分派中“對做什麼”問題,但是虛擬機器“具體是如何做到的”,可能各種虛擬機器的實現都會有些差異。 由於動態分派是非常頻繁的動作,而且動態分派的方法版本選擇過程需要執行時在類的方法元資料中搜索合適的目標方法,因此在虛擬機器的實際實現中基於效能的考慮,大部分實現都不會直接真正進行如此頻繁的搜尋。面對這種情況,最常用的“穩定優化”手段就是為類在方法區中建立一個虛方法表(Vritual Method Table),使用虛方法表索引來代替元資料查詢以提高效能。我們先看看一個虛方法表結構示例,如下圖。
虛方法表中存放著各個方法的實際入口地址。如果某個方法在子類中沒有被重寫,那子類的虛方法表裡面的地址入口和父類相同方法的地址入口時一致的,都指向父類的實現入口,如果之類重寫了這個方法,子類方法表中的地址將會替換為指向子類實現版本的入口地址。圖中Son重寫了來之Father的全部方法,因此Son所以的方法表沒有指向父類Father型別資料的箭頭。但是Son和Father都沒有重寫來自Object的方法,所以它們的方法表中所有的從Object繼承來的方法都指向了Object的資料型別。 方法表一般在類載入的連線階段進行初始化,準備了類的變數初始化後,虛擬機器會把該類的方法表也初始化完畢。方法表示分派呼叫的“穩定優化”手段,虛擬機器除了使用方法表外,在條件允許的情況下,還會使用內聯快取(Inine Cache)和基於“型別繼承關係分析”技術的守護內聯(Guarded Inlining)。