JVM_虛擬機棧詳解


虛擬機棧

1. 虛擬機棧出現的背景

  • 由於跨平臺性的設計,Java 的指令都是根據棧來設計的。不同平臺的 CPU 架構不同,所以不能設計爲基於寄存器的。

  • 優點:跨平臺、指令集小、編譯器容易實現

  • 缺點:性能下降、實現同樣的功能需要更多的指令

1.1 內存中的棧與堆

棧是運行時的單位,而堆是存儲的單位。
即:

  • 棧解決程序的運行問題,即程序如何執行,或者說如何處理數據
  • 堆解決的好似數據存儲的問題,即數據怎麼放,放在哪兒

2 虛擬機棧基本內容

2.1 Java 虛擬機棧是什麼?

Java 虛擬機棧(Java Virtual Machine Stack),早期也叫 Java 棧。
每個線程在創建時都會創建一個虛擬機棧,其內部保存一個個的棧幀(Stack Frame),對應着一次次的 Java 方法調用。
是線程私有的

2.2 生命週期

生命週期和線程一致

2.3 作用

主管 Java 程序的運行,它保存方法的局部變量(8種基本數據類型、對象的引用地址)、部分結果,並參與方法的調用和返回。
> 局部變量 VS 成員變量(或屬性)
> 基本數據變量 VS 引用類型變量(類、數組、接口)

2.4 棧的優點
  1. 棧是一種快速有效的分配存儲方式,訪問速度僅次於程序計數器
  2. JVM 直接對 JAVA 棧的操作只有兩個:
    • 每個方法執行,伴隨着進棧(入棧、壓棧)
    • 執行結束後的出棧工作
  3. 對於棧來說 不存在垃圾回收 問題
2.5 棧中可能出現的異常

Java 虛擬機規範允許 Java 棧的大小是動態的或者是固定不變的

  1. 如果採用固定大小的 Java 虛擬機棧,那每一個線程的 Java 虛擬機棧容量可以在線程創建的時候獨立選定。如果線程請求分配的棧容量超過 Java 虛擬機棧允許的最大容量,Java 虛擬機將會拋出一個 StackOverflowError 異常。
  2. 如果 Java 虛擬機棧可以動態擴展,並且在嘗試擴展的時候無法申請到足夠的內存,或者在創建新的線程時沒有足夠的內存去創建對應的虛擬機棧,那 Java 虛擬機將會拋出一個 OutOfMemoryError 異常。
2.6 設置棧內存大小

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

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

3. 棧幀的內部結構

每個棧幀中存儲着:

  • 局部變量表(Local Variables)
  • 操作數棧(Operand Stack)(或表達式棧)
  • 動態鏈接(Dynamic Linking)(或指向運行時常量池的方法引用)
  • 方法返回地址(Return Address) (或方法正常退出或者異常退出的定義)
  • 一些附加信息

棧幀的內部結構

3.1 局部變量表
  • 局部變量表也被稱之爲局部變量數組或本地變量表

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

  • 由於局部變量表是建立在線程的棧上,是線程的私有數據,因此 不存在數據安全問題

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

  • 方法嵌套調用的次數有由棧的大小決定 。一般來說,棧越大,方法嵌套調用次數越多。對於一個函數而言,它的參數和局部變量越多,使得局部變量表膨脹,它的棧幀就越大,以滿足方法調用所需傳遞的信息增大的需求。進而函數調用就會佔用更多的棧空間,導致其嵌套調用次數就會減少

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

  • 關於 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

  • 補充說明

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

  • 每個獨立的棧幀中除了包含局部變量表以外,還包含一個後進先出的操作數棧,也可以稱爲 表達式棧

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

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

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

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

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

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

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

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

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

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

在這裏插入圖片描述
爲什麼需要常量池呢?
常量池的作用,就是爲了提供一些符號和常量,便於指令的識別。

3.4 棧幀內部結構

在這裏插入圖片描述

3.5 方法的調用

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

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

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

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

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

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

3.5.1 虛方法與非虛方法

非虛方法

  • 如果方法在編譯器就確定了具體的調用版本,這個版本在運行時是不可變的。這樣的方法稱之爲 非虛方法
  • 靜態方法、私有方法、final 方法、實例構造器、父類方法都是非虛方法
  • 其他方法稱爲虛方法

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

  1. 普通調用指令:
    • invokestatic 調用靜態方法,解析階段確定唯一方法版本
    • invokespecial 調用< init > 方法、私有及父類方法,解析解讀那確定唯一方法版本
    • invokevirtual 調用所有虛方法
    • invokeinterface 調用接口方法
      2.動態調用指令:
    • invokedynamic 動態解析出需要調用的方法,然後執行

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

3.5.2 關於 invokedynamic 指令
  • JVM 字節碼指令集一直比較穩定,一直到 Java7 中才增加了一個 invokedynamic 指令,這是 Java 爲了實現「動態類型語言」支持而做的一種改進
  • 但是在 Java7 中並沒有提供直接生成 invokedynamic 指令的方法,需要藉助 ASM 這種底層字節碼工具來產生 invokedynamic 指令。直到 Java8 的 Lambda 表達式的出現,invokedynamic 指令的生成,在 Java 中才有了直接的生成方式
  • Java7中增加了動態語言類型支持的本質是對 Java 虛擬機規範的修改,而不是對 Java 語言規則的修改,這一塊相對來講比較複雜,增加了虛擬機中的方法調用,最直接的受益者就是運行在 Java 平來的動態語言的編譯器
  • 靜態類型語言是判斷變量自身的類型信息;動態類型語言是判斷變量值的類型信息,變量沒有類型信息,變量值纔有類型信息
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章