Java靜態分派與動態分派(二)
方法調用並不等於方法執行,方法調用階段唯一的任務就是確定被調用方法的版本(即調用哪一個方法),暫時還不涉及方法內部的具體運行過程。
在程序運行時,進行方法調用是最普遍、最頻繁的操作,但是Class文件的編譯過程不包括傳統編譯中的連接步驟,一切方法調用在Class文件裏面存儲的都只是符號引用,而不是方法在實際運行時內存布局中的入口地址(相對於之前說的直接引用)。這個特性給Java帶來了更強大的動態擴展能力,但也使得Java方法調用過程變得相對復雜起來,需要在類加載期間,甚至到運行期間才能確定目標方法的直接引用。
解析
所有方法調用中的目標方法在Class文件裏面都是一個常量池中的引用,在類加載的解析階段
在Java語言中符合“編譯期可知,運行期不可變”這個要求的方法,主要包括靜態方法和私有方法兩大類,前者與類型直接關聯,後者在外部不可被訪問,這兩種方法各自的特點決定了他們不可能通過繼承或別的方式重寫其他版本,因此他們適合在類加載階段進行解析。
靜態方法、私有方法、實例構造器、父類方法。這些方法稱為非虛方法,它們在類加載的時候就會把符號引用解析為該方法的直接引用
分派
靜態分派
[java] view plain copy
- 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,gentlemen!");
- }
- public static void sayHello(Woman guy){
- System.out.println("hello,lady!");
- }
- public static void main(String[] args) {
- Human man=new Man();
- Human woman=new Woman();
- sayHello(man);
- sayHello(woman);
- }
- }
輸出:
hello,guy!
hello,guy!
Human man=new Man();
我們把“Human”稱為變量的靜態類型,後面的“Man”稱為變量的實際類型,靜態類型和實際類型在程序中都可以發生一些變化,區別是靜態類型的變化僅僅在使用時發生,變量本身的靜態類型不會被改變,並且最終的靜態類型在編譯器可知;而實際類型變化的結果在運行期才確定,編譯器在編譯期並不知道一個對象的實際類型是什麽。
[java] view plain copy
- Human man=new Man();
- sayHello(man);
- sayHello((Man)man);//類型轉換,靜態類型變化,我們知道轉型後的靜態類型一定是Man
- man=new Woman(); //實際類型變化,實際類型卻是不確定的
- sayHello(man);
- sayHello((Woman)man);//類型轉換,靜態類型變化
輸出:
hello,guy!
hello,gentlemen!
hello,guy!
hello,lady!
編譯器在重載時是通過參數的靜態類型而不是實際類型作為判定的依據。並且靜態類型在編譯期可知,因此,編譯階段,Javac編譯器會根據參數的靜態類型決定使用哪個重載版本。
所有依賴靜態類型來定位方法執行版本的分派動作稱為靜態分派。靜態分派的典型應用就是方法重載。
靜態分派發生在編譯階段,因此確定靜態分派的動作實際上不是由虛擬機來執行的,而是由編譯器來完成。
但是,字面量沒有顯示的靜態類型,它的靜態類型只能通過語言上的規則去理解和推斷。
[java] view plain copy
- 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[] args) {
- sayHello(‘a‘);
- }
- }
輸出:
hello char
將重載方法從上向下依次註釋,將會得到不同的輸出。
如果編譯器無法確定要自定轉型為哪種類型,會提示類型模糊,拒絕編譯。
[java] view plain copy
- import java.util.Random;
- 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:i);//編譯錯誤
- sayHello(r.nextInt()%2!=0?‘a‘:false);//編譯錯誤
- }
- }
動態分派
[java] view plain copy
- 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 woman=new Woman();
- man.sayHello();
- woman.sayHello();
- man=new Woman();
- man.sayHello();
- }
- }
輸出:
man say hello!
woman say hello!
woman say hello!
顯然,這裏不可能再根據靜態類型來決定,因為靜態類型同樣是Human的兩個變量man和woman在調用sayHello()方法時執行了不同的行為,並且變量man在兩次調用中執行了不同的方法。導致這個現象的原因很明顯,是這兩個變量的實際類型不同,Java虛擬機是如何根據實際類型來分派方法執行版本的呢?
我們從invokevirtual指令的多態查找過程開始說起,invokevirtual指令的運行時解析過程大致分為以下幾個步驟:
1、找到操作數棧頂的第一個元素所指向的對象的實際類型,記作C。
2、如果在類型C中找到與常量中的描述符和簡單名稱相符合的方法,然後進行訪問權限驗證,如果驗證通過則返回這個方法的直接引用,查找過程結束;如果驗證不通過,則拋出java.lang.IllegalAccessError異常。
3、否則未找到,就按照繼承關系從下往上依次對類型C的各個父類進行第2步的搜索和驗證過程。
4、如果始終沒有找到合適的方法,則跑出java.lang.AbstractMethodError異常。
由於invokevirtual指令執行的第一步就是在運行期確定接收者的實際類型,所以兩次調用中的invokevirtual指令把常量池中的類方法符號引用解析到了不同的直接引用上,這個過程就是Java語言方法重寫的本質。我們把這種在運行期根據實際類型確定方法執行版本的分派過程稱為動態分派。
虛擬機動態分派的實現
前面介紹的分派過程,作為對虛擬機概念模型的解析基本上已經足夠了,它已經解決了虛擬機在分派中"會做什麽"這個問題。
但是,虛擬機”具體是如何做到的“,可能各種虛擬機實現都會有些差別。
由於動態分派是非常頻繁的動作,而且動態分派的方法版本選擇過程需要運行時在類的方法元數據中搜索合適的目標方法,因此虛擬機的實際實現中基於性能的考慮,大部分實現都不會真正的進行如此頻繁的搜索。面對這種情況,最常用的”穩定優化“手段就是為類在方法區中建立一個虛方法表(Virtual Method Table,也稱為vtable),使用虛方法表索引來代替元數據查找以提高性能。
虛方法表中存放著各個方法的實際入口地址。如果某個方法在子類中沒有被重寫,那子類的虛方法表裏面的地址入口和父類相同方法的地址入口是一致的,都是指向父類的實際入口。如果子類中重寫了這個方法,子類方法表中的地址將會替換為指向子類實際版本的入口地址。
為了程序實現上的方便,具有相同簽名的方法,在父類、子類的虛方法表中具有一樣的索引序號,這樣當類型變換時,僅僅需要變更查找的方法表,就可以從不同的虛方法表中按索引轉換出所需要的入口地址。
方法表一般在類加載階段的連接階段進行初始化,準備了類的變量初始值後,虛擬機會把該類的方法表也初始化完畢。
內容源自:
《深入理解Java虛擬機》
轉自:http://blog.csdn.net/sunxianghuang/article/details/52280002
Java靜態分派與動態分派(二)