JVM 系列 - 內(nèi)存區(qū)域 - Java 虛擬機棧(三)

特點

  • Java 虛擬機棧(Java Virtual Machine Stacks)是線程私有的,生命周期隨著線程,線程啟動而產(chǎn)生,線程結(jié)束而消亡。
  • Java 虛擬機棧描述的是 Java 方法執(zhí)行的內(nèi)存模型,用于存儲棧幀。線程啟動時會創(chuàng)建虛擬機棧,每個方法在執(zhí)行時會在虛擬機棧中創(chuàng)建一個棧幀,用于存儲局部變量表、操作數(shù)棧、動態(tài)連接、方法返回地址、附加信息等信息。每個方法從調(diào)用到執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機棧中的入棧(壓棧)到出棧(彈棧)的過程。
  • Java 虛擬機棧使用的內(nèi)存不需要保證是連續(xù)的。
  • Java 虛擬機規(guī)范即允許 Java 虛擬機棧被實現(xiàn)成固定大?。?code>-Xss),也允許通過計算結(jié)果動態(tài)來擴容和收縮大小。如果采用固定大小的 Java 虛擬機棧,那每個線程的 Java 虛擬機棧容量可以在線程創(chuàng)建的時候就已經(jīng)確定。

Java 虛擬機棧會出現(xiàn)的異常

  • 如果線程請求分配的棧容量超過了 Java 虛擬機棧允許的最大容量,Java 虛擬機將會拋出 StackOverflowError 異常。
  • 如果 Java 虛擬機棧可以動態(tài)擴展,并且在嘗試擴展的時候無法申請到足夠的內(nèi)存,或者在創(chuàng)建新的線程時沒有足夠的內(nèi)存去創(chuàng)建對應(yīng)的虛擬機棧,那 Java 虛擬機將拋出一個 OutOfMemoryError 異常。

Java 虛擬機棧執(zhí)行過程

可以參考一下這篇文章:https://blog.csdn.net/azhegps/article/details/54092466


棧幀(Stack Frame)

  • 棧幀存在于 Java 虛擬機棧中,是 Java 虛擬機棧中的單位元素,每個線程中調(diào)用同一個方法或者不同的方法,都會創(chuàng)建不同的棧幀(可以簡單理解為,一個線程調(diào)用一個方法創(chuàng)建一個棧幀),所以,調(diào)用的方法鏈越多,創(chuàng)建的棧幀越多(代表作:遞歸)。在 Running 的線程,只有當(dāng)前棧幀有效(Java 虛擬機棧中棧頂?shù)臈?,與當(dāng)前棧幀相關(guān)聯(lián)的方法稱為當(dāng)前方法。每調(diào)用一個新的方法,被調(diào)用方法對應(yīng)的棧幀就會被放到棧頂(入棧),也就是成為新的當(dāng)前棧幀。當(dāng)一個方法執(zhí)行完成退出的時候,此方法對應(yīng)的棧幀也相應(yīng)銷毀(出棧)。
    棧幀結(jié)構(gòu)如圖:
    棧幀結(jié)構(gòu)

局部變量表(Local Variable Table)

  • 每個棧幀中都包含一組稱為局部變量表的變量列表,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。在 Java 程序編譯成 Class 文件時,在 Class 文件格式屬性表中 Code 屬性的 max_locals(局部變量表所需的存儲空間,單位是 Slot) 數(shù)據(jù)項中確定了需要分配的局部變量表的最大容量。
  • 局部變量表的容量以變量槽(Variable Slot)為最小單位,不過 Java 虛擬機規(guī)范中并沒有明確規(guī)定每個 Slot 所占據(jù)的內(nèi)存空間大小,只是有導(dǎo)向性地說明每個 Slot 都應(yīng)該存放的8種類型: byte、short、int、float、char、boolean、reference(對象引用就是存到這個棧幀中的局部變量表里的,這里的引用指的是局部變量的對象引用,而不是成員變量的引用。成員變量的對象引用是存儲在 Java 堆(Heap)中)、returnAddress(虛擬機數(shù)據(jù)類型,Sun JDK 1.4.2版本之前使用 jsr/ret 指令用于進行異常處理,后續(xù)版本已廢棄這種實現(xiàn)方式,目前使用異常處理器表代替)類型的數(shù)據(jù),這8種類型的數(shù)據(jù),都可以使用32位或者更小的空間去存儲。Java 虛擬機規(guī)范允許 Slot 的長度可以隨著處理器、操作系統(tǒng)或者虛擬機的不同而發(fā)生變化。對于64位的數(shù)據(jù)類型,虛擬機會以高位在前的方式為其分配兩個連續(xù)的 Slot 空間。即 long 和 double 兩種類型。做法是將 long 和 double 類型速寫分割為32位讀寫的做法。不過由于局部變量表建立在線程的堆棧上,是線程的私有數(shù)據(jù),無論讀寫兩個連續(xù)的 Slot 是否是原子操作,都不會引起數(shù)據(jù)安全問題。
  • Java 虛擬機通過索引定位的方式使用局部變量表,索引值的范圍是從0開始到局部變量表最大的 Slot 數(shù)量。如果是32位數(shù)據(jù)類型的數(shù)據(jù),索引 n 就表示使用第 n 個 Slot,如果是64位數(shù)據(jù)類型的變量,則說明要使用第 n 和第 n+1 兩個 Slot。
  • 在方法執(zhí)行過程中,Java 虛擬機是使用局部變量表完成參數(shù)值到參數(shù)變量列表的傳遞過程。如果是實例方法(非 static 方法),那么局部變量表中的第0位索引的 Slot 默認(rèn)是用來傳遞方法所屬對象實例的引用,在方法中可以通過關(guān)鍵字 this 來訪問這個隱含的參數(shù)。其余參數(shù)按照參數(shù)表的順序來排列,占用從1開始的局部變量 Slot,參數(shù)表分配完畢后,再根據(jù)方法體內(nèi)部定義的變量順序和作用域分配其余的 Slot。
  • 局部變量表中的 Slot 是可重用的,方法體中定義的變量,其作用域并不一定會覆蓋整個方法體,如果當(dāng)前字節(jié)碼程序計數(shù)器的值已經(jīng)超過了某個變量的作用域,那么這個變量相應(yīng)的 Slot 就可以交給其他變量去使用,節(jié)省??臻g,但也有可能會影響到系統(tǒng)的垃圾收集行為。
  • 局部變量無初始值(實例變量和類變量都會被賦予初始值),類變量有兩次賦初始值的過程,一次在準(zhǔn)備階段,賦予系統(tǒng)初始值;另外一次在初始化階段,賦予開發(fā)者定義的值。因此即使在初始化階段開發(fā)者沒有為類變量賦值也沒有關(guān)系,類變量仍然具有一個確定的默認(rèn)值。但局部變量就不一樣了,如果一個局部變量定義了但沒有賦初始值是不能使用的。

使用一段代碼說明一下局部變量表:

// java 代碼
public int test() {
    int x = 0;
    int y = 1;
    return x + y;
}

// javac 編譯后的字節(jié)碼,使用 javap -v 查看
public int test();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: iconst_1
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: ireturn
      LineNumberTable:
        line 7: 0
        line 8: 2
        line 9: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       8     0  this   Lcom/alibaba/uc/TestClass;
            2       6     1     x   I
            4       4     2     y   I

對應(yīng)上面的解釋說明,通過 LocalVariableTable 也可以看出來:
Code 屬性:
stack(int x(1個棧深度)+ int y(1個棧深度))=2, locals(this(1 Slot)+ int x(1 Slot)+ int y(1 Slot))=3, args_size(非 static 方法,this 隱含參數(shù))=1

驗證 Slot 復(fù)用,運行以下代碼時,在 VM 參數(shù)中添加 -verbose:gc

public void test() {
   {
      byte[] placeholder = new byte[64 * 1024 * 1024];
   }
   int a = 0; // 當(dāng)這段代碼注釋掉時,System.gc() 執(zhí)行后,也并不會回收這64MB內(nèi)存。當(dāng)這段代碼執(zhí)行時,內(nèi)存被回收了
   System.gc();
}

局部變量表中的 Slot 是否還存在關(guān)于 placeholder 數(shù)組對象的引用。當(dāng) int a = 0; 不執(zhí)行時,代碼雖然已經(jīng)離開了 placeholder 的作用域,但是后續(xù)并沒有任何對局部變量表的讀寫操作,placeholder 原本所占用的 Slot 還沒有被其他變量所復(fù)用,所以 placeholder 作為 GC Roots(所有 Java 線程當(dāng)前活躍的棧幀里指向 Java 堆里的對象的引用) 仍然是可達對象。當(dāng) int a = 0; 執(zhí)行時,placeholder 的 Slot 被變量 a 復(fù)用,所以 GC 觸發(fā)時,placeholder 變成了不可達對象,即可被 GC 回收。

操作數(shù)棧(Operand Stack)

  • 操作數(shù)棧是一個后入先出(Last In First Out)棧,方法的執(zhí)行操作在操作數(shù)棧中完成,每一個字節(jié)碼指令往操作數(shù)棧進行寫入和提取的過程,就是入棧和出棧的過程。
  • 同局部變量表一樣,操作數(shù)棧的最大深度也是Java 程序編譯成 Class 文件時被寫入到 Class 文件格式屬性表的 Code 屬性的 max_stacks 數(shù)據(jù)項中。
  • 操作數(shù)棧的每一個元素可以是任意的 Java 數(shù)據(jù)類型,32位數(shù)據(jù)類型所占的棧容量為1,64位數(shù)據(jù)類型所占的棧容量為2,在方法執(zhí)行的任何時候,操作數(shù)棧的深度都不會超過在 max_stacks 數(shù)據(jù)項中設(shè)定的最大值(指的是進入操作數(shù)棧的 “同一批操作” 的數(shù)據(jù)類型的棧容量的和)。
  • 當(dāng)一個方法剛剛執(zhí)行的時候,這個方法的操作數(shù)棧是空的,在方法執(zhí)行的過程中,通過一些字節(jié)碼指令從局部變量表或者對象實例字段中復(fù)制常量或者變量值到操作數(shù)棧中,也提供一些指令向操作數(shù)棧中寫入和提取值,及結(jié)果入棧,也用于存放調(diào)用方法需要的參數(shù)及接受方法返回的結(jié)果。例如,整數(shù)加法的字節(jié)碼指令 iadd(使用 iadd 指令時,相加的兩個元素也必須是 int 型) 在運行的時候?qū)⒉僮鲾?shù)棧中最接近棧頂?shù)膬蓚€ int 數(shù)值元素出棧相加,然后將相加結(jié)果入棧。
    以下代碼會以什么形式進入操作數(shù)棧?
// java 代碼
public void test() {
     byte a = 1;
     short b = 1;
     int c = 1;
     long d = 1L;
     float e = 1F;
     double f = 1D;
     char g = 'a';
     boolean h = true;
}

// 字節(jié)碼指令
0: iconst_1   // 把 a 壓入操作數(shù)棧棧頂
1: istore_1   // 將棧頂?shù)?a 存入局部變量表索引為1的 Slot
2: iconst_1  // 把 b 壓入操作數(shù)棧棧頂
3: istore_2   // 將棧頂?shù)?b 存入局部變量表索引為2的 Slot
4: iconst_1   // 把 c 壓入操作數(shù)棧棧頂
5: istore_3    // 將棧頂?shù)?c 存入局部變量表索引為3的 Slot
6: lconst_1   // 把 d 壓入操作數(shù)棧棧頂
7: lstore        4   // 將棧頂?shù)?d 存入局部變量表索引為4的 Slot,由于 long 是64位,所以占2個 Slot
9: fconst_1   // 把 e 壓入操作數(shù)棧棧頂
10: fstore        6   // 將棧頂?shù)?e 存入局部變量表索引為6的 Slot
12: dconst_1   // 把 f 壓入操作數(shù)棧棧頂
13: dstore        7   // 將棧頂?shù)?f 存入局部變量表索引為4的 Slot,由于 double 是64位,所以占2個 Slot
15: bipush        97   // 把 g 壓入操作數(shù)棧棧頂
17: istore        9   // 將棧頂?shù)?g 存入局部變量表索引為9的 Slot
19: iconst_1   // 把 h 壓入操作數(shù)棧棧頂
20: istore        10   // 將棧頂?shù)?h 存入局部變量表索引為10的 Slot

從上面字節(jié)碼指令可以看出來,除了 long、double、float 類型使用的字節(jié)碼指令不是 iconstistore,其他類型都是使用這兩個字節(jié)碼指令操作,說明 byte、short、char、boolean 進入操作數(shù)棧時,都會被轉(zhuǎn)化成 int 型。

  • 在概念模型中,兩個棧幀作為虛擬機棧的元素,是完全相互獨立的。但在大多虛擬機實現(xiàn)會做一些優(yōu)化,令兩個棧幀出現(xiàn)一部分重疊。讓下面的棧幀的部分操作數(shù)棧與上面棧幀的部分局部變量表重疊在一起,這樣在進行方法調(diào)用時就可以共用一部分?jǐn)?shù)據(jù),無需進行額外的參數(shù)復(fù)制傳遞。


    棧幀共享
  • Java 虛擬機的解釋執(zhí)行引擎稱為 “基于棧的執(zhí)行引擎”,其中所指的 “?!?就是操作數(shù)棧。

動態(tài)連接(Dynamic Linking)

  • 每個棧幀都包含一個指向運行時常量池(JVM 運行時數(shù)據(jù)區(qū)域)中該棧幀所屬性方法的引用,持有這個引用是為了支持方法調(diào)用過程中的動態(tài)連接。
  • 在 Class 文件格式的常量池(存儲字面量和符號引用)中存有大量的符號引用(1.類的全限定名,2.字段名和屬性,3.方法名和屬性),字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號引用為參數(shù)。這些符號引用一部分會在類加載過程的解析階段的時候轉(zhuǎn)化為直接引用(指向目標(biāo)的指針、相對偏移量或者是一個能夠直接定位到目標(biāo)的句柄),這種轉(zhuǎn)化稱為靜態(tài)解析。另外一部分將在每一次的運行期期間轉(zhuǎn)化為直接引用,這部分稱為動態(tài)連接。
    看看以下代碼的 Class 文件格式的常量池:
// java 代碼
 public Test test() {
    return new Test();
 }

// 字節(jié)碼指令
Constant pool:
   #1 = Methodref          #4.#19         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#20         // com/alibaba/uc/Test.i:I
   #3 = Class              #21            // com/alibaba/uc/Test
   #4 = Class              #22            // java/lang/Object
   #5 = Utf8               i
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/alibaba/uc/Test;
  #14 = Utf8               test
  #15 = Utf8               ()I
  #16 = Utf8               <clinit>
  #17 = Utf8               SourceFile
  #18 = Utf8               Test.java
  #19 = NameAndType        #7:#8          // "<init>":()V
  #20 = NameAndType        #5:#6          // i:I
  #21 = Utf8               com/alibaba/uc/Test
  #22 = Utf8               java/lang/Object

public int test();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: getstatic     #2                  // Field i:I
         3: areturn
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  this   Lcom/alibaba/uc/Test;

從上面字節(jié)碼指令看出 0: getstatic #2 // Field i:I 這行字節(jié)碼指令指向 Constant pool 中的 #2,而 #2 中指向了 #3 和 #20 為符號引用,在類加載過程的解析階段會被轉(zhuǎn)化為直接引用(指向方法區(qū)的指針)。

方法返回地址

  • 當(dāng)一個方法開始執(zhí)行后,只有兩種方式可以退出這個方法。第一種方式是執(zhí)行引擎遇到任意一個方法返回的字節(jié)碼指令(例如:areturn),這時候可能會有返回值傳遞給上層的方法調(diào)用者(調(diào)用當(dāng)前方法的方法稱為調(diào)用者),是否有返回值和返回值的類型將根據(jù)遇到何種方法返回指令來決定,這種退出方法的方式稱為正常完成出口(Normal Method Invocation Completion)。
  • 另外一種退出方式是,在方法執(zhí)行過程中遇到了異常,并且這個異常沒有在方法體內(nèi)得到處理,無論是Java虛擬機內(nèi)部產(chǎn)生的異常,還是代碼中使用 athrow 字節(jié)碼指令產(chǎn)生的異常,只要在本方法的異常處理器表中沒有搜索到匹配的異常處理器,就會導(dǎo)致方法退出,這種退出方法的方式稱為異常完成出口(Abrupt Method Invocation Completion)。一個方法使用異常完成出口的方式退出,是不會給它的上層調(diào)用者產(chǎn)生任何返回值的。
  • 無論采用何種退出方式,在方法退出之后,都需要返回到方法被調(diào)用的位置,程序才能繼續(xù)執(zhí)行,方法返回時可能需要在棧幀中保存一些信息,用來幫助恢復(fù)它的上層方法的執(zhí)行狀態(tài)。一般來說,方法正常退出時,調(diào)用者的程序計數(shù)器的值可以作為返回地址,棧幀中很可能會保存這個計數(shù)器值。而方法異常退出時,返回地址是要通過異常處理器表來確定的,棧幀中一般不會保存這部分信息。
  • 方法退出的過程實際上就等同于把當(dāng)前棧幀出棧,因此退出時可能執(zhí)行的操作有:恢復(fù)上層方法的局部變量表和操作數(shù)棧,把返回值(如果有的話)壓入調(diào)用者棧幀的操作數(shù)棧中,調(diào)整程序計數(shù)器的值以指向方法調(diào)用指令后面的一條指令等。

簡述:

虛擬機會使用針對每種返回類型的操作來返回,返回值將從操作數(shù)棧出棧并且入棧到調(diào)用方法的方法棧幀中,當(dāng)前棧幀出棧,被調(diào)用方法的棧幀變成當(dāng)前棧幀,程序計數(shù)器將重置為調(diào)用這個方法的指令的下一條指令。

附加信息

虛擬機規(guī)范允許具體的虛擬機實現(xiàn)增加一些規(guī)范里沒有描述的信息到棧幀中,例如與調(diào)試相關(guān)的信息,這部分信息完全取決于具體的虛擬機實現(xiàn)。在實際開發(fā)中,一般會把動態(tài)連接,方法返回地址與其它附加信息全部歸為一類,稱為棧幀信息。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 第6章類文件結(jié)構(gòu) 6.1 概述 6.2 無關(guān)性基石 6.3 Class類文件的結(jié)構(gòu) java虛擬機不和包括java...
    kennethan閱讀 1,068評論 0 2
  • 第二部分 自動內(nèi)存管理機制 第二章 java內(nèi)存異常與內(nèi)存溢出異常 運行數(shù)據(jù)區(qū)域 程序計數(shù)器:當(dāng)前線程所執(zhí)行的字節(jié)...
    小明oh閱讀 1,275評論 0 2
  • 棧幀(Stack Frame)是用于支持虛擬機進行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機運行時數(shù)據(jù)區(qū)中的虛擬機棧...
    Java架構(gòu)_師閱讀 352評論 0 1
  • 概述 執(zhí)行引擎是Java虛擬機最核心的組成部分之一?!疤摂M機”是一個相對于“物理機”的概念,這兩個機器都有代碼執(zhí)行...
    胡二囧閱讀 990評論 2 2
  • 男孩圣狄雅各 牧羊人就像船員或旅行推銷員一樣,終究會在某個村莊里遇見某個人,讓他們忘了四處游蕩的生活多么無憂無慮。...
    煮酒說故事閱讀 530評論 0 0

友情鏈接更多精彩內(nèi)容