JVM-棧幀之局部變數表

1.棧幀的內部結構

每個棧幀中存儲著:

  • 局部變數表(Local Variables)
  • 操作數棧(Operand Stack)(或表達式棧)
  • 動態鏈接(Dynamic Linking)(或指向運行時常量池的方法引用)
  • 方法返回地址(Return Address)(或方法正常退出或者異常退出的定義)
  • 一些附加資訊
    在這裡插入圖片描述
    並行每個執行緒下的棧都是私有的,因此每個執行緒都有自己各自的棧,並且每個棧裡面都有很多棧幀,棧幀的大小主要由局部變數表和操作數棧決定的

2.局部變數表

2.1 什麼是局部變數表

  • 局部變數表也被稱之為局部變數數組或本地變數表
  • 定義為一個數字數組,主要用於存儲方法參數和定義在方法體內的局部變數**,這些數據類型包括各類基本數據類型、對象引用(reference),以及returnAddress返回值類型。
  • 由於局部變數表是建立在執行緒的棧上,是執行緒的私有數據,因此不存在數據安全問題
  • 局部變數表所需的容量大小是在編譯期確定下來的,並保存在方法的Code屬性的maximum local variables數據項中。在方法運行期間是不會改變局部變數表的大小的。
  • 方法嵌套調用的次數由棧的大小決定。一般來說,棧越大,方法嵌套調用次數越多。
    • 對一個函數而言,它的參數和局部變數越多,使得局部變數表膨脹,它的棧幀就越大,以滿足方法調用所需傳遞的資訊增大的需求。
    • 進而函數調用就會佔用更多的棧空間,導致其嵌套調用次數就會減少。
  • 局部變數表中的變數只在當前方法調用中有效。
    • 在方法執行時,虛擬機通過使用局部變數表完成參數值到參數變數列表的傳遞過程。
    • 當方法調用結束後,隨著方法棧幀的銷毀,局部變數表也會隨之銷毀。

局部變數表存放了編譯期可知的各種Java虛擬機基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它並不等同於對象本身,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或者其他與此對象相關的位置)和returnAddress 類型(指向了一條位元組碼指令的地址)。
=====================================
這些數據類型在局部變數表中的存儲空間以局部變數槽(Slot)來表示,其中64位長度的long和double類型的數據會佔用兩個變數槽,其餘的數據類型只佔用一個。局部變數表所需的記憶體空間在編譯期間完成分配,當進入一個方法時,這個方法需要在棧幀中分配多大的局部變數空間是完全確定的,在方法運行期間不會改變局部變數表的大小。請讀者注意,這裡說的「大小」是指變數槽的數量,虛擬機真正使用多大的記憶體空間(譬如按照1個變數槽佔用32個比特、64個比特,或者更多)來實現一個變數槽,這是完全由具體的虛擬機實現自行決定的事情。
=====================================
在《Java虛擬機規範》中,對這個記憶體區域規定了兩類異常狀況:如果執行緒請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;如果Java虛擬機棧容量可以動態擴展[2],當棧擴展時無法申請到足夠的記憶體會拋出OutOfMemoryError異常。
——–摘自《深入理解java虛擬機》

對於局部變數表所需的容量大小是在編譯期確定下來的這句話可以通過看位元組碼文件

源程式碼:

public class Example {
    public static void main(String[] args) {
        int a = 3;
        a++;
        testStatic();
        System.out.println(a);
        
    }

    public static void testStatic(){
        Date date = new Date();
        int count = 10;
        System.out.println(count);
    }
}

位元組碼:
在這裡插入圖片描述
可以看到 locals=2 說明了局部變數表的大小為2 ,而這兩個變數為data和count,所以說局部變數表所需的容量大小是在編譯期確定下來的。(此時程式碼只是進行了編譯還未運行)

通過使用jclasslib來看位元組碼,進行一些相關解釋。

1.位元組碼行號
在這裡插入圖片描述
位元組碼中左邊的數字表示的是有多少行位元組碼0~15也就是有16行。

2.方法異常資訊表
在這裡插入圖片描述
此為異常資訊表,當前方法沒有異常所以沒有異常表。

3、Misc(雜項)
在這裡插入圖片描述
4、行號表

Java程式碼的行號和位元組碼指令行號的對應關係
在這裡插入圖片描述

5、生效行數和剩餘有效行數(針對於位元組碼文件的行數)
在這裡插入圖片描述
圖中標記的地方表示的是該局部變數的作用域,初始PC(Start PC)為2表示該局部變數在位元組碼的第2行開始生效,位元組碼的第2行對應著java程式碼的第8行(由上一張圖可知),而int a的定義是在第7行,可以得知局部變數是從聲明的下一行生效的

長度(Length)表示剩餘有效行數,main方法位元組碼指令總共有16行,從2行開始生效,那麼剩下就是16-2 =14。

描述符(Descriptor)第一行 [Ljava/lang/String 表示args的引用類型(String[]),第二行 I 表示的是a的引用類型(int)

2.2 關於Slot的理解

  • 參數值的存放總是從局部變數數組索引 0 的位置開始,到數組長度-1的索引結束。
  • 局部變數表,最基本的存儲單元是Slot(變數槽),局部變數表中存放編譯期可知的各種基本數據類型(8種),引用類型(reference),returnAddress類型的變數。
  • 在局部變數表裡,32位以內的類型只佔用一個slot(包括returnAddress類型),64位的類型佔用兩個slot(long和double)。
  • byte、short、char在儲存前被轉換為int,boolean也被轉換為int,0表示false,非0表示true
  • long和double則佔據兩個slot
  • JVM會為局部變數表中的每一個Slot都分配一個訪問索引,通過這個索引即可成功訪問到局部變數表中指定的局部變數值
  • 當一個實例方法被調用的時候,它的方法參數和方法體內部定義的局部變數將會按照順序被複制到局部變數表中的每一個slot上
  • 如果需要訪問局部變數表中一個64bit的局部變數值時,只需要使用前一個索引即可。(比如:訪問long或double類型變數)
  • 如果當前幀是由構造方法或者實例方法創建的,那麼該對象引用this將會存放在index為0的slot處,其餘的參數按照參數表順序繼續排列。(this也相當於一個變數)

在這裡插入圖片描述

2.3 程式碼示例

public class Example {
    public int sum = 0;

    public static void main(String[] args) {

        new Example().test();

    }

    public void test() {
        this.sum++;
        double a = 3;
        long b = 4;
    }
}

在這裡插入圖片描述

  • 可以看到this存放在index = 0的位置
  • 64位的類型(long和double)佔用兩個slot,序號直接從1變成了3

注意:

  • this 不存在與 static 方法的局部變數表中,所以無法調用。
  • static 修飾的方法是屬於類的,該方法的調用者可能是一個類,而不是對象。 那麼,如果使用的是類來 調用 而不是對象,則 this 就無法指向合適的對象,所以 static 修飾的方法中不能使用 this

2.4 Slot的重複利用

棧幀中的局部變數表中的槽位是可以重用的,如果一個局部變數過了其作用域,那麼在其作用域之後申明新的局部變數變就很有可能會復用過期局部變數的槽位,從而達到節省資源的目的。

public void test() {
    int a = 0;
    {
        int b = 0;
        b = a + 1;
    }
    //變數c使用之前已經銷毀的變數b佔據的slot的位置
    int c = a + 1;
}

在這裡插入圖片描述
可以看到局部變數c重用了局部變數b的slot位置

2.5 靜態變數與局部變數的對比

變數的分類:

  • 按照數據類型分:① 基本數據類型 ② 引用數據類型
  • 按照在類中聲明的位置分:
    • 成員變數:在使用前,都經歷過默認初始化賦值
      • 類變數: linking的prepare階段:給類變數默認賦值
        —> initial階段:給類變數顯式賦值即靜態程式碼塊賦值
      • 實例變數:隨著對象的創建,會在堆空間中分配實例變數空間,並進行默認賦值
    • 局部變數:在使用前,必須要進行顯式賦值!否則,編譯不通過

變數的賦值:

  • 參數表分配完畢之後,再根據方法體內定義的變數的順序和作用域分配。
  • 我們知道成員變數有兩次初始化的機會**,**第一次是在「準備階段」,執行系統初始化,對類變數設置零值,另一次則是在「初始化」階段,賦予程式設計師在程式碼中定義的初始值。
  • 和類變數初始化不同的是,局部變數表不存在系統初始化的過程,這意味著一旦定義了局部變數則必須人為的初始化,否則無法使用。

補充說明

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