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階段:給類變數預設賦值
- 區域性變數:在使用前,必須要進行顯式賦值!否則,編譯不通過
- 成員變數:在使用前,都經歷過預設初始化賦值
變數的賦值:
- 參數列分配完畢之後,再根據方法體內定義的變數的順序和作用域分配。
- 我們知道成員變數有兩次初始化的機會**,**第一次是在“準備階段”,執行系統初始化,對類變數設定零值,另一次則是在“初始化”階段,賦予程式設計師在程式碼中定義的初始值。
- 和類變數初始化不同的是,區域性變數表不存在系統初始化的過程,這意味著一旦定義了區域性變數則必須人為的初始化,否則無法使用。
- 在棧幀中,與效能調優關係最為密切的部分就是前面提到的區域性變數表。在方法執行時,虛擬機器使用區域性變數表完成方法的傳遞。
- 區域性變數表中的變數也是重要的垃圾回收根節點,只要被區域性變數表中直接或間接引用的物件都不會被回收。