1. 程式人生 > >《深入理解Java虛擬機》讀書筆記7-虛擬機字節碼執行引擎

《深入理解Java虛擬機》讀書筆記7-虛擬機字節碼執行引擎

javac inf spa 註意 ont 開始 多態 bsp 接口

虛擬機字節碼執行引擎

  啟動java程序,包含程序入口main方法的class文件將會率先被JVM獲取到,然後就是類加載階段處理這個class文件,最終通過調用man方法開始一個java程序的執行。可以說Java程序的執行就是一個或多個方法調用鏈,而初始方法就是main。接下來我們介紹java方法的內存模型-棧幀和調用機制。

一、運行時棧幀結構

  在運行時內存區域那一節我們介紹過-程私有的虛擬機棧,虛擬機棧的棧元素是-棧幀,每個棧幀包含局部變量表、操作棧、返回地址、動態鏈接等信息。每個方法對應於一個棧幀,一個方法的執行對應於棧幀的入棧和出棧過程。一個線程的虛擬機棧的模型圖可如下所示:  

技術分享圖片

其中,只有棧頂的棧幀是有效的,稱為當前棧幀,對應的方法稱為當前方法。所有的字節碼指令都是針對當前棧幀操作的。

1、局部變量表

  局部變量表用於存儲方法的參數和局部變量,有以下需要註意項:

  • l 局部變量表索引從0開始,按照參數順序和局部變量定義順序存儲
  • l 對於實例方法,隱含參數this指向實例對象,存儲於索引0
  • l 局部變量表的大小在類編譯為class文件時就已經固定,記錄在方法表的code屬性表的max_locals項。
  • l 局部變量表的最小單位為Slot(變量槽),虛擬機規範只規定了每個slot能夠存儲下一個boolean,byte,char,short,int,float,reference和returnAddress類型的數據(沒有固定指明是32位,也可以使用64位通過對齊和補白方式使外觀一致。對於32位的虛擬機,long和double類型需要兩個slot存儲,此時兩個slot的操作必須綁定,不允許單獨訪問一個)。
  • l Slot是可復用的,當字節碼PC計數器超過了slot中存儲的局部變量的作用域,該局部變量失效,則可以再次使用該slot。(可能會存在一個局部變量已經失效,但還未將slot分配給新的局部變量,此時若原局部變量是一個reference類型,仍然持有了一個對象的引用,則可能導致該對象無法被垃圾收集器回收。之所以是可能是因為我們是在概念模型上討論,實際虛擬機的實現沒有固定,比如當使用JIT編譯後,就可以正常回收。)
  • l 棧幀中的局部變量沒有類變量的準備階段,所以不會說默認值為0,一定需要先賦值後使用
2、操作棧
  • l 操作棧也稱為操作數棧,是一個先入後出的棧結構。
  • l 每個元素可以是java的任意類型。需要註意的是棧中的元素類型必須和字節碼指令序列對應,不允許存在iadd指令,但棧頂是兩個float或其他非int類型的棧元素。
  • l 多虛擬機實現時,會讓兩個棧幀的局部變量表和操作棧共享部分區域,,減少參數復制傳遞。
3、動態鏈接、返回地址以及附加信息

  動態鏈接是指:棧幀中擁有指向運行時常量池中該棧幀所屬方法的引用,從而實現動態鏈接(不懂~~)。

  返回地址:方法返回方式有兩種正常方法出口異常方法出口。兩種方式都需要回到方法調用者,返回地址用於記錄調用者原先的PC計數器。

  還有可能存在有關調試的一些附加信息。把動態鏈接、返回地址、附加信息歸為一類,統稱為棧幀信息。

二、方法調用

  介紹完方法的內存模型,接下來我們看看方法調用(即確定調用哪一個方法的過程,因為java並沒有傳統的連接步驟,方法調用在class文件中存儲的只是符號引用)。

1、解析

  在類加載的解析階段,部分符號引用會轉化為直接引用。這類符號引用轉化成功的前提是:方法在程序真正運行前就確定了可用版本,而且這個方法版本在運行期是不可變的。把調用目標在程序代碼寫好、編譯器進行編譯時就必須確定下來的方法的調用稱為解析(即編譯器可知,運行期不可變)。

  Java語言中符號這個條件的有靜態方法、私有方法、實例構造器、父類方法、以及使用final修飾的方法這5類,即使使用invokestatic、invokespecial字節碼調用的和final修飾的方法,這些方法稱為非虛方法。與之相對的是虛方法(即被invokevirtual、invokeinterface、invokedynamic字節碼調用的方法,除final方法)。

  解析調用是一個靜態過程,在解析階段就會獲得方法的直接引用。

2、分派

  除了解析調用,還有分派調用,但需要註意這兩種方式不是在非此即彼,他們不是在同一層次上。分派根可分為靜態分派動態分派。而通過分派所依據的宗量又可分為單分派多分派宗量包括方法的接收者和方法的參數。以下從java語言多態的重載和重寫來具體描述。

2.1 重載

abstract class Human{}

class Man extends Human{}

class Woman extends Human{}

public class Test{

public static sayHello(Human human){

System.out.println(“hello human”);

}

public static sayHello(Man man){

System.out.println(“hello man”);

}

public static sayHello(Woman Woman){

System.out.println(“hello woman”);

}

public static void main(String[] args){

Human man = new man();

Human woman = new Woman() ;

Test t = new Test() ;

t.sayHello(man);

t.sayHello(woman) ;

}

}

上面代碼的執行結果將是:

Hello human

Hello human

對於:

Human man = new Man();

我們把Human稱為變量的靜態類型(或外觀類型),後面的Man稱為變量的實際類型。其中變量本身的靜態類型並不會變化,只是在使用時可以改變,如:

t.sayHello((Man) man) ;

而實際類型的變化只有在運行期才可知。

  再看源代碼,之所以是這個結果,是因為javac編譯器會根據參數的靜態類型決定使用哪個版本的方法,使用javap –verbose獲得字節碼指令可以看到t.sayHello(man)翻譯之後是invokevirtual後面跟了sayHello(Human human)這個方法的符號引用。這種依據靜態類型來定位方法執行版本的分派動作稱為靜態分派,上述重載代碼就是典型例子。我們還能知道這個過程是根據方法的調用者Test t和方法參數兩個宗量確定的,所以靜態分派也是多分派。

   此外,需要註意,方法重載在遇到多個適用方法時,會按一定的優先級選擇更加合適的版本比如:

char c = ‘a’ ;

sayHello(c) ;

//存在如下的方法:

sayHello(Char c);

sayHello(int c);

sayHello(double c);

sayHello(Character c);

會按char->int->long->float->double的順序轉型進行尋找最適方法,若沒有則char->Character或Comparable<Character>->Object等,若有變長參數char… arg,優先級最後

2.2 重寫

abstract class Human{

abstract void sayHello() ;

}

class Man extends Human{

void sayHello(){

System.out.println(“hello man”);

}

}

class Woman extends Human{

void sayHello(){

System.out.println(“hello woman”) ;

}

}

public class Test{

public static void main(String[] args){

Human man = new Man() ;

man.sayHello();

Human woman = new Woman();

woman.sayHello() ;

}

}

運行結果為:

hello man

hello woman

  為什麽是這個運行結果呢?通過javap –verbose輸出字節碼指令可以看到調用語句為invokevirtual #Human.sayHello:()V,即指令後面跟的方法符號引用是指向Human父類的方法,但這個指令還有個參數是取棧頂的引用類型,作為方法的調用者,也就是說在運行期才獲得了存在於棧頂的引用指向的對象作為方法調用者,這樣才實現了重寫時正確定位方法版本。這種在運行期才確定方法執行版本的分派過程稱為動態分派。此外,此時方法的版本選擇只和方法的調用者有關,所以動態分派屬於單分派類型

  上面只是說了大致的過程,在具體實現中,動態分派要去方法元數據區搜索合適的目標方法,因此基於性能的考慮會使用虛方法表(針對接口則是接口方法表)。如下所示:

技術分享圖片

通過方法表的索引來代替元數據查找以提高性能。需要註意的是相同簽名的方法,在父類和子類的虛方法表都應當具有一樣的索引號,這樣類型變換時只需更換要查找的方法表。方法表一般在類加載的連接階段進行初始化,準備了類的變量初始值後,會把該類的方法表也初始完畢。

三、基於棧的字節碼解釋執行

Java字節碼基本上算是一種基於棧的指令集架構(也不算完全,因為指令偶爾後面會帶參數),相對的是基於寄存器的指令集。基於棧的優點是可移植,不受限於硬件提供的寄存器,代碼相對緊湊等,但基於棧需要額外的出棧和入棧操作,更重要的是基於棧則意味著訪問內存,這樣比訪問寄存器慢很多,所以性能相對較低。

《深入理解Java虛擬機》讀書筆記7-虛擬機字節碼執行引擎