JVM---虛擬機棧

虛擬機棧

虛擬機棧出現的背景

由於跨平臺性的設計,Java的指令都是根據棧來設計的。不同平臺CPU架構不同,所以不能設計爲基於寄存器的。優點是跨平臺,指令集小,編譯器容易實現,缺點是性能下降,實現同樣的功能需要更多的指令。

內存中的棧與堆

棧是運行時的單位,而堆是存儲的單位。
即:棧解決程序的運行問題,即程序如何執行,或者說如何處理數據。堆解決的是數據存儲的問題,即數據怎麼放、放在哪兒。

Java虛擬機棧是什麼?
  • Java虛擬機棧(Java Virtual Machine Stack) ,早期也叫Java棧。每個線程在創建時都會創建一個虛擬機棧,其內部保存一個個的棧幀(Stack Frame) ,對應着一次次的Java方法調用。
    • 是線程私有的
  • 生命週期
    • 生命週期和線程一-致。
  • 作用
    • 主管Java程序的運行,它保存方法的局部變量(8種基本數據類型、對象的引用地址)、部分結果,並參與方法的調用和返回。
      • 局部變量vs成員變量(或屬性)
      • 基本數據變量 vs引用類型變量(類、數組、接口)讓天下沒負難學的
棧的特點(優點)
  • 棧是一種快速有效的分配存儲方式,訪問速度僅次於程序計數器。

  • JVM直接對Java棧的操作只有兩個:

    • 每個方法執行,伴隨着進棧(入棧、壓棧)
    • 執行結束後的出棧工作
  • 對於棧來說不存在垃圾回收問題

設置棧內存大小,
我們可以使用參數-Xss選項來設置線程的最大棧空間,棧的大小直接決定了函數調用的
最大可達深度。

public class StackDeepTest
private static int count = 0;
public static void recursion ( ){
    count++ ;
    recursion() ;
}
public static void main (String args [] ) {
    try {
    recursion () ;
    } catch (Throwable e) {
    System. out.println("deep of calling = " + count) ;
    e.printStackTrace () ;
    }
}
棧的存儲單位

棧中存儲什麼?

  • 每個線程都有自己的棧,棧中的數據都是以棧幀(Stack Frame)的格式存在。
  • 在這個線程上正在執行的每個方法都各自對應一個棧幀( Stack Frame)。
  • 棧幀是一一個內存區塊,是一個數據集,維繫着方法執行過程中的各種數據信息。
棧運行原理
  • JVM直接對Java棧的操作只有兩個,就是對棧幀的壓棧和出棧,遵循“先進後出”/“後進先出”原則。
  • 在一條活動線程中,一一個時間點上,只會有一個活動的棧幀。即只有當前正在執行的方法的棧幀(棧項棧幀)是有效的,這個棧幀被稱爲當前棧幀(Current Frame) ,與當前棧幀相對應的方法就是當前方法(Current Method),定義這個方法的類就是當前類(Current Class)
  • 執行引擎運行的所有字節碼指令只針對當前棧幀進行操作。
  • 如果在該方法中調用了其他方法,對應的新的棧幀會被創建出來,放在棧的頂端,成爲新的當前幀。
  • 不同線程中所包含的棧幀是不允許存在相互引用的,即不可能在一個棧幀之中引用另外一個線程的棧幀。
  • 如果當前方法調用了其他方法,方法返回之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,接着,虛擬機會丟棄當前棧幀,使得前一個棧幀重新成爲當前棧幀。
  • Java方法有兩種返回函數的方式,一種是正常的函數返回,使用return指令;另外一種是拋出異常。不管使用哪種方式,都會導致棧幀被彈出。
棧幀內部的結構

每個棧幀中存儲着:

  • 局部變量表(Local variables)
  • 操作數棧(operand Stack) ( 或表達式棧)
  • 動態鏈接(Dynamic Linking)(或指向運行時常量池的方法引用)
  • 方法返回地址(Return Address) (或方法正常退出或者異常退出的定義)
  • 一些附加信息
**局部變量表(Local Variables) **
  • 局部變量表也被稱之爲局部變量數組或本地變量表
  • 定義爲一個數字數組,主要用於存儲方法參數和定義在方法體內的局部變量,這些數據類型包括各類基本數據類型、對象引用(reference) ,以及returnAddress類型。
  • 由於局部變量表是建立在線程的棧上,是線程的私有數據,因此不存在數據安全問題
  • 局部變量表所需的容量大小是在編譯期確定下來的,並保存在方法的Code屬性的maximum local variables數據項中。在方法運行期間是不會改變局部變量表的大小的。

在這裏插入圖片描述

Length是變量的作用範圍

關於Slot理解

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

  • 局部變量表[最基本的存儲單元是Slot (變量槽)

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

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

    • byte、short 、char在存儲前被轉換爲int,boolean 也被轉換爲int,0表示false,非0表示true。
    • long和double 則佔據兩個Slot。
  • JVM會爲局部變量表中的每一一個slot都分配一個訪問索引,通過這個素引即可成功訪問到局部變量表中指定的局部變量值

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

  • 如果需要訪問局部變量表中一個64bit的局部變量值時,只需要使用前一個索引即可。(比如:訪問long或double類型變量)

  • 如果當前幀是由構造方法或者實例方法創建的,那麼該對象引用thi s將會存放在index爲0的
    slot處,其餘的參數按照參數表順序繼續排列。
    在這裏插入圖片描述

Slot的重複利用

public void test()
{
    int a = 10;
    {
        int b = a + 15;
        b = a + a;
        a =b;
    }
    int c = 20;
}

其中b變量在局部變量表中的作用域是代碼塊中,出了代碼塊後它在局部變量表中的位置被c佔據

在這裏插入圖片描述

舉例:靜態變量與局部變量的對比

  • 參數表分配完畢之後,再根據方法體內定義的變量的順序和作用域分配。

  • 我們知道類變量表有兩次初始化的機會,第一次是在“準備階段”,執行系統初始化,對類變量設置零值,另一次則是在“初始化”階段,賦予程序員在代碼中定義的初始值。

  • 和類變量初始化不同的是,局部變量表不存在系統初始化的過程,這意味着一旦定義了局部變量則必須人爲的初始化,否則無法使用。

    public void test() {
        int i;
        System. out. println(i);
    }
    

    這樣的代碼是錯誤的,沒有賦值不能夠使用。

補充說明

  • 在棧幀中,與性能調優關係最爲密切的部分就是前面提到的局部變量表。在方法執行時,虛擬機使用局部變量表完成方法的傳遞。
  • 局部變量表中的變量也是重要的垃圾回收根節點,只要被局部變量表中直接或間接引用的對象都不會被回收。
操作數棧(Operand Stack)
  • 每一個獨立的棧幀中除了包含局部變量表以外,還包含一個後進先出(Last-In-First-Out)的操作數棧,也可以稱之爲表達式棧(Expression Stack) 。

  • 操作數棧,在方法執行過程中,根據字節碼指令,往棧中寫入數據或提取數據,即入棧(push) /出棧(pop)。

    • 某些字節碼指令將值壓入操作數棧,其佘的字節碼指令將操作數取出棧。使用它們後再把結果壓入棧。
    • 比如:執行復制、交換、求和等操作
  • 操作數棧,主要用於保存計算過程的中間結果,同時作爲計算過程中變量臨時的存儲空間。

  • 操作數棧就是JVM執行引擎的一一個工作區,當一個方法剛開始執行的時候, .一個新的棧幀也會隨之被創建出來,這個方法的操作數棧是空的。

  • 每一個操作數棧都會擁有一個明確的棧深度用於存儲數值,其所需的最大深度在編譯期就定義好了,保存在方法的Code屬性中,爲max stack的值。

  • 棧中的任何一個元素都是可以任意的Java數據類型。

    • 32bit的類型佔用一個棧單位深度
    • 64bit的類型佔用兩個棧單位深度
  • 操作數棧並非採用訪問索引的方式來進行數據訪問的,而是隻能通過標準的入棧(push)和出棧(pop)操作來完成一次數據訪問。

  • 如果被調用的方法帶有返回值的話,其返回值將會被壓入當前棧幀的操作數棧中,並更新PC寄存器中下一條需要執行的字節碼指令。

  • 操作數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配,這由編譯器在編譯器期間進行驗證,同時在類加載過程中的類檢驗階段的數據流分析階段要再次驗證。

  • 另外,我們說Java虛擬機的解釋引擎是基於棧的執行引擎,其中的棧指的就是操作數棧。

棧頂緩存( Top-of-StackCashing)技術

前面提過,基於棧式架構的虛擬機所使用的零地址指令更加緊湊,但完成一項操作的時候必然需要使用更多的入棧和出棧指令,這同時也就意味着將需要更多的指令分派(instruction dispatch) 次數和內存讀/寫次數。
由於操作數是存儲在內存中的,因此頻繁地執行內存讀/寫操作必然會影響執行速度。爲了解決這個問題,HotSpot JVM的 設計者們提出了棧頂緩存(ToS,Top-of-Stack Cashing) 技術,將棧項元素全部緩存在物理CPU的寄存器中,以此降低對內存的讀/寫次數,提升執行引擎的執行效率。

動態鏈接(DynamiC Linking)
  • 每一個棧幀內部都包含一個指向運往時常量池該棧幀所屬方法的引用。包含這個引用的目的就是爲了支持當前方法的代碼能夠實現動態鏈接(Dynamic Linking)。比如: invokedynamic指令
  • 在Java源文件被編譯到字節碼文件中時,所有的變量和方法引用都作爲符號引用(Symbolic Reference) 保存在class文件的常量池裏。比如:描述-一個方法調用了另外的其他方法時,就是通過常量池中指向方法的符號引用來表示的,那麼動態鏈接的作用就是爲了將這些符號引用轉換爲調用方法的直接引用。
方法的調用

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

  • 靜態鏈接:
    當一個字節碼文件被裝載進JVM內部時,如果被調用的目標方法在編譯期可知,且運行期保持不變時。這種情況下將調用方法的符號引用轉換爲直接引用的過程稱之爲靜態鏈接。
  • 動態鏈接:
    如果被調用的方法在編譯期無法被確定下來,也就是說,只能夠在程序運行期將調用方法的符號引用轉換爲直接引用,由於這種引用轉換過程具備動態性,因此也就被稱之爲動態鏈接。

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

  • 早期綁定:
    早期綁定就是指被調用的目標方法如果在編譯期可知,且運行期保持不變時,即可將這個方法與所屬的類型進行綁定,這樣一來,由於明確了被調用的目標方法究竟是哪一個,因此也就可以使用靜態鏈接的方式將符號引用轉換爲直接引用。
  • 晚期綁定:
    如果被調用的方法在編譯期無法被確定下來,只能夠在程序運行期根據實際的類型綁定相關的方法,這種綁定方式也就被稱之爲晚期綁定。

隨着高級語言的橫空出世,類似於Java一樣的基於面向對象的編程語言如今越來越多,儘管這類編程語言在語法風格上存在一定的差別,但是它們彼此之間始終保持着一個共性,那就是都支持封裝、繼承和多態等面向對象特性,既然這一類的編程語言具備多態特性,那麼自然也就具備早期綁定和晚期綁定兩種綁定方式。

Java中任何一個普通的方法其實都具備虛函數的特徵,它們相當於C++語言中的虛函數(C++中則需要使用關鍵字virtual來顯式定義)。如果在Java程序中不希望某個方法擁有虛函數的特徵時,則可以使用關鍵字final來標記這個方法。

非虛方法:

  • 如果方法在編譯期就確定了具體的調用版木,這個版木在運行時是不可變的這樣的方法稱爲非虛方法。

  • 靜態方法、私有方法、final方法、實例構造器、父類方法都是非虛方法。

  • 其他方法稱爲虛方法。

虛擬機中提供了以下幾條方法調用指令:

  • 普通調用指令:

    • invokestatic: 調用靜態方法,解析階段確定唯一方法版本

    • invokespecial: 調用<init>方法、 私有及父類方法,解析階段確定唯一方法版本

    • invokevirtual: 調用所有虛方法

    • invokeinterface: 調用接口方法

      前兩個非虛方法

  • 動態調用指令:

    • invokedynamic:動態解析出需要調用的方法,然後執行
  • 前四條指令固化在虛擬機內部,方法的調用執行不可人爲干預,而invokedynamic指 令則支持由用戶確定方法版本。其中invokestatic指令和invokespecial指令調用的方法稱爲非虛方法,其餘的(final修飾的除外)稱爲虛方法。

方法的調用:關於invokedynamic指令

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

動態類型語言和靜態類型語言
動態類型語言和靜態類型語言兩者的區別就在於對類型的檢查是在編譯期還是在運行期,滿足前者就是靜態類型語言,反之是動態類型語言。

說的再直白一點就是,靜態類型語言是判斷變量自身的類型信息;動態類型語言是判斷變量值的類型信息,變量沒有類型信息,變量值纔有類型信息,這是動態語言的一一個重要特徵。

Java語言中方法重寫的本質:
1.找到操作數棧頂的第一個元素所執行的對象的實際類型,記作C.
2.如果在類型C中找到與常量中的描述符合簡單名稱都相符的方法,則進行訪問權限校驗,如果通過則返回這個方法的直接引用,查找過程結束;如果不通過,則返回java. lang. IllegalAccessError異常。
3.否則,按照繼承關係從下往,上依次對C的各個父類進行第2步的搜索和驗證過程。
4.如果始終沒有找到合適的方法,則拋出java.lang . AbstractMethodError異常。

IllegalAccessError介紹:
程序試圖訪問或修改-一個屬性或調用一個方法,這個屬性或方法,你沒有權限訪問。一般
的,這個會引起編譯器異常。這個錯誤如果發生在運行時,就說明-一個類發生了不兼容的
改變。

方法的調用:虛方法表

  • 在面向對象的編程中,會很頻繁的使用到動態分派,如果在每次動態分派的過程中都要重新在類的方法元數據中搜索合適的目標的話就可能影響到執行效率。因此,爲了提高性能,JVM採用在類的方法區建立一個虛方法表.(virtual method table) (非虛方法不會出現在表中)來實現。使用索引表來代替查找。
  • 每個類中都有一個虛方法表,表中存放着各個方法的實際入口。
  • 那麼虛方法表什麼時候被創建?
    虛方法表會在類加載的鏈接階段被創建並開始初始化,類的變量初始值準備完成之後,JVM會把該類的方法表也初始化完畢。
方法返回地址
  • 存放調用該方法的pc寄存器的值。
  • 一個方法的結束,有兩種方式:
    • 正常執行完成
    • 出現未處理的異常,非正常退出
  • 無論通過哪種方式退出,在方法退出後都返回到該方法被調用的位置。方法正常退出時,調用者的pc計數器的值作爲返回地址,即調用該方法的指令的下一條指令的地址。而通過異常退出的,返回地址是要通過異常表來確定,棧幀中一般不會保存這部分信息。

當一個方法開始執行後,只有兩種方式可以退出這個方法:
1、執行引擎遇到任意一個方法返回的字節碼指令(return) ,會有返回值傳遞給.上層的方法調用者,簡稱正常完成出口;

  • 一個方法在正常調用完成之後究竟需要使用哪一個返回指令還需要根據方法返回值的實際數據類型而定。
  • 在字節碼指令中,返回指令包含irejflurn (當返回值是boolean、byte、char、shor t和int類型時使用)、lreturn、 freturn、 dreturn以及areturn,另外還有一個return指令供聲明爲void的方法、實例初始化方法、類和接口的初始化方法使用。
虛擬機棧的面試題目
  • 舉例棧溢出的情況?
  • 調整棧大小,就能保證不出現溢出嗎?
  • 分配的棧內存越大越好嗎?
  • 垃圾回收是否會涉及到虛擬機棧?
  • 方法中定義的局部變量是否線程安全?
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章