Java虛擬機棧--棧幀

棧幀的內部結構

每一個棧幀中存儲着java

1.局部變量表(Local Variables)python

2.操做數棧(Operand Stack)(或表達式棧)編程

3.動態連接(Dynamic Linking)(或執行"運行時常量池"的方法引用)----深刻理解Java多態特性必讀!!數組

4.方法返回地址(Return Adress)(或方法正常退出或者異常退出的定義)緩存

5.一些附加信息安全

 

其中部分參考書目上,稱方法返回地址、動態連接、附加信息爲幀數據區架構

 

 

 

局部變量表(Local Variables)

1.局部變量表也被稱之爲局部變量數組或本地變量表jvm

2.定義爲一個數字數組,主要用於存儲方法參數和定義在方法體內的局部變量這些數據類型包括各種基本數據類型、對象引用(reference),以及returnAddressleixing編程語言

3.因爲局部變量表是創建在線程的棧上,是線程私有的數據,所以不存在數據安全問題函數

4.局部變量表所需的容量大小是在編譯期肯定下來的,並保存在方法的Code屬性的maximum local variables數據項中。在方法運行期間是不會改變局部變量表的大小的

5.方法嵌套調用的次數由棧的大小決定。通常來講,棧越大,方法嵌套調用次數越多對一個函數而言,他的參數和局部變量越多,使得局部變量表膨脹,它的棧幀就越大,以知足方法調用所需傳遞的信息增大的需求。進而函數調用就會佔用更多的棧空間。

6.局部變量表中的變量只在當前方法調用中有效。在方法執行時,虛擬機經過使用局部變量表完成參數值到參數變量列表的傳遞過程。當方法調用結束後,隨着方法棧幀的銷燬,局部變量表也會隨之銷燬。

查看幀的局部變量表

利用javap命令對字節碼文件進行解析查看main()方法對應棧幀的局部變量表,如圖:

 

 也能夠在IDEA 上安裝jclasslib byte viewcoder插件查看方法內部字節碼信息剖析,以main()方法爲例

 

 

 

 

 

 

 

 

變量槽slot的理解與演示

1.參數值的存放老是在局部變量數組的index0開始,到數組長度-1的索引結束

2.局部變量表,最基本的存儲單元是Slot(變量槽)

3.局部變量表中存放編譯期可知的各類基本數據類型(8種),引用類型(reference),returnAddress類型的變量。

4.在局部變量表裏,32位之內的類型只佔用一個slot(包括returnAddress類型),64位的類型(long和double)佔用兩個slot。

byte、short、char、float在存儲前被轉換爲int,boolean也被轉換爲int,0表示false,非0表示true;

long和double則佔據兩個slot。

 

 

5.JVM會爲局部變量表中的每個slot都分配一個訪問索引,經過這個索引便可成功訪問到局部變量表中指定的局部變量值

6.當一個實例方法被調用的時候,它的方法參數和方法體內部定義的局部變量將會按照聲明順序被複制到局部變量表中的每個slot上

7.若是須要訪問局部變量表中一個64bit的局部變量值時,只須要使用前一個索引便可。(好比:訪問long或者double類型變量)

8.若是當前幀是由構造方法或者實例方法建立的(意思是當前幀所對應的方法是構造器方法或者是普通的實例方法),那麼該對象引用this將會存放在index爲0的slot處,其他的參數按照參數表順序排列。

9.靜態方法中不能引用this,是由於靜態方法所對應的棧幀當中的局部變量表中不存在this

示例代碼:

public class LocalVariablesTest {

    private int count = 1;
    //靜態方法不能使用this
    public static void testStatic(){
        //編譯錯誤,由於this變量不存在與當前方法的局部變量表中!!!
        System.out.println(this.count);
    }
}

 

slot的重複利用

棧幀中的局部變量表中的槽位是能夠重複利用的,若是一個局部變量過了其做用域,那麼在其做用域以後申明的新的局部變量就頗有可能會複用過時局部變量的槽位,從而達到節省資源的目的。

private void test2() {
        int a = 0;
        {
            int b = 0;
            b = a+1;
        }
        //變量c使用以前以及經銷燬的變量b佔據的slot位置
        int c = a+1;
    }

上述代碼對應的棧幀中局部變量表中一共有多少個slot,或者說局部變量表的長度是幾?

答案是3:

變量b的做用域是

{
     int b = 0;
     b = a+1;
}

this佔0號、a單獨佔1個槽號、c重複使用了b的槽號

靜態變量與局部變量的對比及小結

變量的分類:

  • 按照數據類型分:
    • ①基本數據類型;
    • ②引用數據類型;
  • 按照在類中聲明的位置分:
    • ①成員變量:在使用前,都經歷過默認初始化賦值
      • static修飾:類變量:類加載連接的準備preparation階段給類變量默認賦0值——>初始化階段initialization給類變量顯式賦值即靜態代碼塊賦值;
      • 不被static修飾:實例變量:隨着對象的建立,會在堆空間分配實例變量空間,並進行默認賦值
    • 局部變量:在使用前,必需要進行顯式賦值的!不然,編譯不經過

補充說明

  • 在棧幀中,與性能調優關係最爲密切的部分就是局部變量表。在方法執行時,虛擬機使用局部變量表完成方法的傳遞
  • 局部變量表中的變量也是重要的垃圾回收根節點,只要被局部變量表中直接或間接引用的對象都不會被回收

操做數棧(Operand Stack)

1.棧 :可使用數組或者鏈表來實現

2.每個獨立的棧幀中除了包含局部變量表之外,還包含一個後進先出的操做數棧,也能夠成爲表達式棧

3.操做數棧,在方法執行過程當中,根據字節碼指令,往棧中寫入數據或提取數據,即入棧(push)或出棧(pop)

某些字節碼指令將值壓入操做數棧,其他的字節碼指令將操做數取出棧,使用他們後再把結果壓入棧。(如字節碼指令bipush操做)

好比:執行復制、交換、求和等操做

代碼舉例

 

 

操做數棧特色

  • 操做數棧,主要用於保存計算過程的中間結果,同時做爲計算過程當中變量臨時的存儲空間。
  • 操做數棧就是jvm執行引擎的一個工做區,當一個方法開始執行的時候,一個新的棧幀也會隨之被建立出來,這個方法的操做數棧是空的
  • 每個操做數棧都會擁有一個明確的棧深度用於存儲數值,其所需的最大深度在編譯器就定義好了,保存在方法的code屬性中,爲max_stack的值。
  • 棧中的任何一個元素都是能夠任意的java數據類型
    • 32bit的類型佔用一個棧單位深度
    • 64bit的類型佔用兩個棧深度單位
  • 操做數棧並不是採用訪問索引的方式來進行數據訪問的,而是隻能經過標準的入棧push和出棧pop操做來完成一次數據訪問
  • 若是被調用的方法帶有返回值的話,其返回值將會被壓入當前棧幀的操做數棧中並更新PC寄存器中下一條須要執行的字節碼指令。
  • 操做數棧中的元素的數據類型必須與字節碼指令的序列嚴格匹配,這由編譯器在編譯期間進行驗證,同時在類加載過程當中的類驗證階段的數據流分析階段要再次驗證。
  • 另外,咱們說Java虛擬機的解釋引擎是基於棧的執行引擎,其中的棧指的就是操做數棧。

操做數棧代碼追蹤

結合上圖結合下面的圖來看一下一個方法(棧幀)的執行過程
①15入棧;②存儲15,15進入局部變量表
注意:局部變量表的0號位被構造器佔用,這裏的15從局部變量表1號開始

 

 

③壓入8;④8出棧,存儲8進入局部變量表;

 

 

⑤從局部變量表中把索引爲1和2的是數據取出來,放到操做數棧;⑥iadd相加操做

 

 

⑦iadd操做結果23出棧⑧將23存儲在局部變量表索引爲3的位置上istore_3

棧頂緩存技術ToS(Top-of-Stack Cashing)

  • 基於棧式架構的虛擬機所使用的零地址指令(即不考慮地址,單純入棧出棧)更加緊湊,但完成一項操做的時候必然須要使用更多的入棧和出棧指令,這同時也就意味着將須要更多的指令分派(instruction dispatch)次數和內存讀/寫次數
  • 因爲操做數是存儲在內存中的,所以頻繁地執行內存讀/寫操做必然會影響執行速度。爲了解決這個問題,HotSpot JVM的設計者們提出了棧頂緩存技術,將棧頂元素所有緩存在物理CPU的寄存器中,以此下降對內存的讀/寫次數,提高執行引擎的執行效率

 

 

動態連接(Dynamic Linking)

1.運行時常量池位於方法區(注意: JDK1.7 及以後版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開闢了一塊區域存放運行時常量池。)

字節碼中的常量池結構以下:

爲何須要常量池呢?
常量池的做用,就是爲了提供一些符號和常量,便於指令的識別。下面提供一張測試類的運行時字節碼文件格式

 

 

 

 

 

2.每個棧幀內部都包含一個指向運行時常量池Constant pool或該棧幀所屬方法的引用。包含這個引用的目的就是爲了支持當前方法的代碼可以實現動態連接。好比invokedynamic指令

3.在Java源文件被編譯成字節碼文件中時,全部的變量和方法引用都做爲符號引用(symbolic Refenrence)保存在class字節碼文件(javap反編譯查看)的常量池裏。好比:描述一個方法調用了另外的其餘方法時,就是經過常量池中指向方法的符號引用來表示的,那麼動態連接的做用就是爲了將這些符號引用(#)最終轉換爲調用方法的直接引用。

方法的調用

在JVM中,將符號引用轉換爲調用方法的直接引用與方法的綁定機制相關

  • 靜態連接
    當一個 字節碼文件被裝載進JVM內部時,若是被調用的目標方法在編譯期可知,且運行期保持不變時。這種狀況下將調用方法的符號引用轉換爲直接引用的過程稱之爲靜態連接。
  • 動態連接
    若是被調用的方法在編譯期沒法被肯定下來,也就是說,只可以在程序運行期將調用方法的符號引用轉換爲直接引用,因爲這種引用轉換過程具有動態性,所以也就被稱之爲動態連接。

對應的方法的綁定機制爲:早起綁定(Early Binding)和晚期綁定(Late Bingding)。綁定是一個字段、方法或者類在符號引用被替換爲直接引用的過程,這僅僅發生一次。

  • 早期綁定
    早期綁定就是指被調用的目標方法若是在編譯期可知,且運行期保持不變時,便可將這個方法與所屬的類型進行綁定,這樣一來,因爲明確了被調用的目標方法到底是哪個,所以也就可使用靜態連接的方式將符號引用轉換爲直接引用。
  • 晚期綁定
    若是被調用的方法在編譯期沒法被肯定下來,只可以在程序運行期根據實際的類型綁定相關的方法,這種綁定方式也就被稱之爲晚期綁定。

隨着高級語言的橫空出世,相似於java同樣的基於面向對象的編程語言現在愈來愈多,儘管這類編程語言在語法風格上存在必定的差異,可是它們彼此之間始終保持着一個共性,那就是都支持封裝,集成和多態等面向對象特性,既然這一類的編程語言具有多態特性,那麼天然也就具有早期綁定和晚期綁定兩種綁定方式。
Java中任何一個普通的方法其實都具有虛函數的特徵,它們至關於C++語言中的虛函數(C++中則須要使用關鍵字virtual來顯式定義)。若是在Java程序中不但願某個方法擁有虛函數的特徵時,則可使用關鍵字final來標記這個方法。

虛方法和非虛方法

子類對象的多態性使用前提:

①類的繼承關係(父類的聲明)②方法的重寫(子類的實現)

實際開發編寫代碼中用的接口,實際執行是導入的的三方jar包已經實現的功能

非虛方法

  • 若是方法在編譯器就肯定了具體的調用版本,這個版本在運行時是不可變的。這樣的方法稱爲非虛方法
  • 靜態方法、私有方法、final方法、實例構造器(實例已經肯定,this()表示本類的構造器)、父類方法(super調用)都是非虛方法

其餘全部體現多態特性的方法稱爲虛方法

虛擬機中提供瞭如下幾條方法調用指令

普通調用指令:
1.invokestatic:調用靜態方法,解析階段肯定惟一方法版本;
2.invokespecial:調用<init>方法、私有及父類方法,解析階段肯定惟一方法版本;
3.invokevirtual調用全部虛方法;
4.invokeinterface:調用接口方法;
動態調用指令(Java7新增):
5.invokedynamic:動態解析出須要調用的方法,而後執行 .
前四條指令固化在虛擬機內部,方法的調用執行不可人爲干預,而invokedynamic指令則支持由用戶肯定方法版本。

其中invokestatic指令和invokespecial指令調用的方法稱爲非虛方法

其中invokevirtual(final修飾的除外,JVM會把final方法調用也歸爲invokevirtual指令,但要注意final方法調用不是虛方法invokeinterface指令調用的方法稱稱爲虛方法。

/**
 * 解析調用中非虛方法、虛方法的測試
 */
class Father {
    public Father(){
        System.out.println("Father默認構造器");
    }

    public static void showStatic(String s){
        System.out.println("Father show static"+s);
    }

    public final void showFinal(){
        System.out.println("Father show final");
    }

    public void showCommon(){
        System.out.println("Father show common");
    }

}

public class Son extends Father{
    public Son(){
        super();
    }

    public Son(int age){
        this();
    }

    public static void main(String[] args) {
        Son son = new Son();
        son.show();
    }

    //不是重寫的父類方法,由於靜態方法不能被重寫
    public static void showStatic(String s){
        System.out.println("Son show static"+s);
    }

    private void showPrivate(String s){
        System.out.println("Son show private"+s);
    }

    public void show(){
        //invokestatic
        showStatic(" 大頭兒子");
        //invokestatic
        super.showStatic(" 大頭兒子");
        //invokespecial
        showPrivate(" hello!");
        //invokespecial
        super.showCommon();
        //invokevirtual 由於此方法聲明有final 不能被子類重寫,因此也認爲該方法是非虛方法
        showFinal();
        //虛方法以下
        //invokevirtual
        showCommon();//沒有顯式加super,被認爲是虛方法,由於子類可能重寫showCommon
        info();

        MethodInterface in = null;
        //invokeinterface  不肯定接口實現類是哪個 須要重寫
        in.methodA();

    }

    public void info(){

    }

}

interface MethodInterface {
    void methodA();
}

 

關於invokedynamic指令

  • JVM字節碼指令集一直比較穩定,一直到java7才增長了一個invokedynamic指令,這是Java爲了實現【動態類型語言】支持而作的一種改進
  • 可是java7中並無提供直接生成invokedynamic指令的方法,須要藉助ASM這種底層字節碼工具來產生invokedynamic指令.直到Java8的Lambda表達式的出現,invokedynamic指令的生成,在java中才有了直接生成方式
  • Java7中增長的動態語言類型支持的本質是對java虛擬機規範的修改,而不是對java語言規則的修改,這一塊相對來說比較複雜,增長了虛擬機中的方法調用,最直接的受益者就是運行在java平臺的動態語言的編譯器

動態類型語言和靜態類型語言

  • 動態類型語言和靜態類型語言二者的卻別就在於對類型的檢查是在編譯期仍是在運行期,知足前者就是靜態類型語言,反之則是動態類型語言。
  • 直白來講 靜態語言是判斷變量自身的類型信息;動態類型語言是判斷變量值的類型信息,變量沒有類型信息,變量值纔有類型信息,這是動態語言的一個重要特徵
  • Java是靜態類型語言(儘管lambda表達式爲其增長了動態特性),js,python是動態類型語言.
Java:String info = "硅谷";//靜態語言

JS:var name = "硅谷「;var name = 10;//動態語言

Pythom: info = 130;//更加完全的動態語言

 

方法重寫的本質

  • 1 找到操做數棧的第一個元素所執行的對象的實際類型,記做C。
  • 2.若是在類型C中找到與常量池中的描述符、簡單名稱都相符的方法,則進行訪問權限校驗,若是經過則返回這個方法的直接引用,查找過程結束;若是不經過,則返回java.lang.IllegalAccessError異常。
  • 3.不然,按照繼承關係從下往上依次對c的各個父類進行第二步的搜索和驗證過程。
  • 4.若是始終沒有找到合適的方法,則拋出java.lang.AbstractMethodError異常。 IllegalAccessError介紹 程序視圖訪問或修改一個屬性或調用一個方法,這個屬性或方法,你沒有權限訪問。通常的,這個會引發編譯器異常。這個錯誤若是發生在運行時,就說明一個類發生了不兼容的改變。

虛方法表

  • 在面向對象編程中,會很頻繁期使用到動態分派,若是在每次動態分派的過程當中都要從新在累的方法元數據中搜索合適的目標的話就可能影響到執行效率。所以,爲了提升性能,jvm採用在類的方法區創建一個虛方法表(virtual method table)(非虛方法不會出如今表中)來實現。使用索引表來代替查找。
  • 每一個類中都有一個虛方法表,表中存放着各個方法的實際入口。
  • 那麼虛方法表何時被建立? 虛方法表會在類加載的連接階段被建立 並開始初始化,類的變量初始值準備完成以後,jvm會把該類的虛方法表也初始化完畢。

 舉個例子:咱們定義三個類、一個Friendly接口

 

 

interface Friendly{
  void sayHello();
  void sayGoodbye();
}

Dog類的虛方法表

class Dog{
  public void sayHello(){
  }
  public String toString(){
    return "Dog";
  }
}

 

 

可卡犬虛方法表:可卡犬如果使用toString方法無需向上找Object類,只需找到Dog類便可;這是一個效率的提高

 

class CockerSpaniel extends Dog implements Friendly{
  public void sayHello(){
    super.sayHello();
  }
  public void sayGoodbye(){
  }
}

 

貓類的虛方法表:

class Cat implements Friendly{

  public void eat(){

  }

  public void sayHello(){

  }

  public void sayGoodbye(){

  }

  protected void finalize(){

  }

  public String toString(){

  }

}

 

方法返回地址(Return Address)

 

  • 存放調用該方法的PC寄存器的值。
  • 一個方法的結束,有兩種方式:
    • 正常執行完成
    • 出現未處理的異常,非正常退出
  • 不管經過哪一種方式退出,在方法退出後都返回到該方法被調用的位置。方法正常退出時,調用者(方法的調用者可能也是一個方法)的pc計數器的值做爲返回地址,即調用該方法的指令的下一條指令的地址。而經過異常退出時,返回地址是要經過異常表來肯定,棧幀中通常不會保存這部分信息。
  • 本質上,方法的退出就是當前棧幀出棧的過程。此時,須要恢復上層方法的局部變量表、操做數棧、將返回值入調用者棧幀的操做數棧、設置PC寄存器值等,讓調用者方法繼續執行下去。
  • 正常完成出口和異常完成出口的區別在於:經過異常完成出口退出的不會給他的上層調用者產生任何的返回值。

當一個方法開始執行後,只有兩種方式能夠退出這個方法

1.執行引擎遇到任意一個方法返回的字節碼指令(return),會有返回值傳遞給上層的方法調用者,簡稱正常完成出口;

  • 一個方法在正常調用完成以後究竟須要使用哪個返回指令還須要根據方法返回值的實際數據類型而定
  • 在字節碼指令中,返回指令包含ireturn(當返回值是boolena、byte、char、short和int類型時使用)、lreturn、freturn、dreturn以及areturn(引用類型的)
  • 另外還有一個return指令供聲明爲void的方法、實例初始化方法、類和接口的初始化方法使用

 

 

2.在方法執行的過程當中遇到了異常(Exception),而且這個異常沒有在方法內進行處理,也就是隻要在本方法的異常表中沒有搜素到匹配的異常處理器,就會致使方法退出,簡稱異常完成出口
方法執行過程當中拋出異常時的異常處理,存儲在一個異常處理表,方便在發生異常的時候找處處理異常的代碼。

咱們寫一個demo演示:

 

 

 

字節碼當中的異常處理表:下表的行號不是上圖的代碼的行號,而是其對應字節碼當中的行號

 

 在字節碼當中的4~11行是可能存在異常的代碼,11表明字節碼中可以處理該異常的位置是第11行也就是上圖中的第72行

棧幀當中的一些附加信息

棧幀中還容許攜帶與java虛擬機實現相關的一些附加信息。例如,對程序調試提供支持的信息。(不少資料都忽略了附加信息)

 

 

 


 

參考教程:尚硅谷官方--宋紅康

參考書目:深刻理解JVM--周志明

相關文章
相關標籤/搜索