開發與維運

Java虛擬機(六):Java虛擬機棧-棧幀

Ⅰ. 棧幀內部結構

每個棧幀存儲著:1.局部變量表(Local Variables)、2.操作數棧(Operand Stack)、3.動態鏈接(Dynamic Linking)、4.方法返回地址(Return Address)、5.一些附加信息

Ⅱ. 局部變量表

一、局部變量表基本理解

1.局部變量表也被稱為局部變量數組或本地方法表

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

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

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

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

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

查看Java虛擬機棧的棧幀的局部變量表

方法一:使用javap -v xxx.class查看

方法二:使用IDEA插件jclasslib查看

二、局部變量表中變量槽slot

局部變量表中變量槽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;
    
    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:this佔0號、a單獨佔1個槽號、c重複使用了b的槽號

靜態變量與局部變量的對比

變量的分類:

  • 按照數據類型分:

    • ①基本數據類型
    • ②引用數據類型
  • 按照在類中聲明的位置分:

    • ①成員變量:在使用前,都經歷過默認初始化賦值

      • static修飾:類變量:類加載鏈接的準備preparation階段給類變量默認賦0值--->>初始化階段initialization給類變量顯式賦值即靜態代碼塊賦值
      • 不被static修飾:實例變量:隨著對象的創建,會在堆空間分配實例變量空間,並進行默認賦值
    • ②局部變量:在使用前,必須要進行顯式賦值的!否則,編譯不通過

Ⅲ. 操作數棧

1.每一個獨立的棧幀中除了包含局部變量表以外,還包含一個後進先出的操作數棧

2.操作數棧,在方法執行過程中,根據字節碼指令,往棧中寫入數據或提取數據,即入棧或出棧

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

比如:執行復制、交換、求和等操作

操作數棧的特點

  • 操作數棧,主要用於保存計算過程的中間結果,同時作為計算過程中變量臨時的存儲空間
  • 操作數棧就是JVM執行引擎的一個工作區,當一個方法開始執行的時候,一個新的棧幀也會隨之被創建出來,這個方法的操作數棧是空的
  • 每一個操作數棧都會擁有一個明確的棧深度用於存儲數值,其所需的最大深度在編譯器就定義好了,保存在方法的code屬性中,為max_stack的值。
  • 棧中的任何一個元素都是可以任意的Java數據類型

    • 32bit的類型佔用一個棧單位深度
    • 64bit的類型佔用兩個棧深度單位
  • 操作數棧並非採用訪問索引的方式來進行數據訪問的,而是隻能通過標準的入棧push和出棧pop操作來完成一次數據訪問
  • 如果被調用的方法帶有返回值的話,其返回值將會被壓入當前棧幀的操作數棧中,並更新PC寄存器中下一條需要執行的字節碼指令
  • 操作數棧中的元素的數據類型必須與字節碼指令的序列嚴格匹配,這由編譯器在編譯期間進行驗證,同時在類加載過程中的類驗證階段的數據流分析階段要再次驗證
  • 另外,我們說Java虛擬機的解釋引擎是基於棧的執行引擎,其中的棧指的就是操作數棧

操作數棧具體演示

下面我們以此代碼進行演示

①15入棧、②存儲15,15進入局部變量表

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

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

⑦iadd操作結果23出棧並且操作數棧清空、⑧將23存儲在局部變量表索引為3的位置上istore_3

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

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

Ⅳ. 動態鏈接

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來標記這個方法

虛方法和非虛方法

非虛方法

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

其他所有體現多態特性的方法稱為虛方法:子類對象的多態性使用前提:①類的繼承關係(父類的聲明)、②方法的重寫(子類的實現)

JVM虛擬機中的方法調用指令

普通調用指令:
1.invokestatic:調用靜態方法,解析階段確定唯一方法版本
2.invokespecia:調用方法、私有及父類方法,解析階段確定唯一方法版本
3.invokevirtual:調用所有虛方法
4.invokeinterface:調用接口方法
5.動態調用指令(invokedynamic,Java7新增):動態解析出需要調用的方法,然後執行 :

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

前四條指令固化在虛擬機內部,方法的調用執行不可人為干預,而invokedynamic指令則支持由用戶確定方法版本

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

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

動態語言和靜態語言

  • 動態類型語言和靜態類型語言兩者的卻別就在於對類型的檢查是在編譯期還是在運行期,滿足前者就是靜態類型語言,反之則是動態類型語言
  • 直白來說 靜態語言是判斷變量自身的類型信息;動態類型語言是判斷變量值的類型信息,變量沒有類型信息,變量值才有類型信息,這是動態語言的一個重要特徵
  • Java是靜態類型語言(儘管lambda表達式為其增加了動態特性)
  • Js、Python是動態類型語言
Java:String info = "Ali";   //靜態語言

JS:var name = "Ali“;        //動態語言

Python: info = 150;         //更加徹底的動態語言

Ⅴ. 方法返回地址

  • 存放調用該方法的PC寄存器的值(使程序繼續運行)
  • 一個方法的結束,有兩種方式:

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

Ⅵ. 棧幀中的一些附件信息

棧幀中還允許攜帶與Java虛擬機實現相關的一些附加信息 例如,對程序調試提供支持的信息

Leave a Reply

Your email address will not be published. Required fields are marked *