1.棧幀的內部結構
每個棧幀中存儲着:
- 局部變量表(Local Variables)
- 操作數棧(Operand Stack)(或表達式棧)
- 動態鏈接(Dynamic Linking)(或指向運行時常量池的方法引用)
- 方法返回地址(Return Address)(或方法正常退出或者異常退出的定義)
- 一些附加信息
並行每個線程下的棧都是私有的,因此每個線程都有自己各自的棧,並且每個棧里面都有很多棧幀,棧幀的大小主要由局部變量表和操作數棧決定的
- 局部變量表也被稱之為局部變量數組或本地變量表
- 定義為一個數字數組,主要用於存儲方法參數和定義在方法體內的局部變量**,這些數據類型包括各類基本數據類型、對象引用(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)
- 參數值的存放總是從局部變量數組索引 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也相當於一個變量)
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
棧幀中的局部變量表中的槽位是可以重用的,如果一個局部變量過了其作用域,那么在其作用域之后申明新的局部變量變就很有可能會復用過期局部變量的槽位,從而達到節省資源的目的。
public void test() { int a = 0; { int b = 0; b = a + 1; } //變量c使用之前已經銷毀的變量b占據的slot的位置 int c = a + 1; }
可以看到局部變量c重用了局部變量b的slot位置
變量的分類:
- 按照數據類型分:① 基本數據類型 ② 引用數據類型
- 按照在類中聲明的位置分:
- 成員變量:在使用前,都經歷過默認初始化賦值
- 類變量: linking的prepare階段:給類變量默認賦值
—> initial階段:給類變量顯式賦值即靜態代碼塊賦值 - 實例變量:隨着對象的創建,會在堆空間中分配實例變量空間,並進行默認賦值
- 類變量: linking的prepare階段:給類變量默認賦值
- 局部變量:在使用前,必須要進行顯式賦值!否則,編譯不通過
- 成員變量:在使用前,都經歷過默認初始化賦值
變量的賦值:
- 參數表分配完畢之后,再根據方法體內定義的變量的順序和作用域分配。
- 我們知道成員變量有兩次初始化的機會**,**第一次是在“准備階段”,執行系統初始化,對類變量設置零值,另一次則是在“初始化”階段,賦予程序員在代碼中定義的初始值。
- 和類變量初始化不同的是,局部變量表不存在系統初始化的過程,這意味着一旦定義了局部變量則必須人為的初始化,否則無法使用。
- 在棧幀中,與性能調優關系最為密切的部分就是前面提到的局部變量表。在方法執行時,虛擬機使用局部變量表完成方法的傳遞。
- 局部變量表中的變量也是重要的垃圾回收根節點,只要被局部變量表中直接或間接引用的對象都不會被回收。