天天看點

JVM 深入淺出 :一文看懂 JVM 記憶體結構

文章目錄

    • 1. 概述
    • 2. 程式計數器
    • 3. Java 虛拟機棧
      • 3.1. 棧深度
      • 3.2. 棧幀
        • 3.2.1. 局部變量表
        • 3.2.2. 操作數棧
        • 3.2.3. 動态連結
        • 3.2.4. 方法正常結束
        • 3.2.5. 方法異常結束
    • 4. 堆
    • 5. 方法區
      • 5.1. 去永久代過程
    • 6. 運作時常量池
    • 7. 本地方法棧
    • 8. 參考資料

1. 概述

JVM 把記憶體進行了劃分,不同的記憶體區域有不同的功能。有的記憶體區域是線程私有的,比如 Java 虛拟機棧、本地方法棧和程式計數器,每一條線程都有自己獨立的空間。有的記憶體區域是線程共享的,比如方法區和堆。

是以不同記憶體區域的功能、作用域和生命周期是不同的。本文做一個詳細的分析。

根據 JVM 虛拟機規範,記憶體結構如下:

JVM 深入淺出 :一文看懂 JVM 記憶體結構

JVM 虛拟機規範屬于概念模型,具體的實作各個廠商的會有所差異。比如方法區的設計,hotspot 在 1.7 之前使用永久代,1.7 後使用元空間。

本文主要分析 HotSpot 虛拟機的實作。

2. 程式計數器

JVM 支援多線程,采用時間片輪轉的方式實作多線程并發。一個核心每一刻隻能有一個線程執行,多線程下需要線程上下文切換。為了確定切換過程中,不同的線程指令和資料不會發生混亂,需要單獨開辟記憶體空間給每個線程,進行線程隔離。這些區域包含了程式計數器、虛拟機棧、本地方法棧。這些都是線程私有記憶體,生命周期和線程一緻。

如果執行的不是本地方法,程式計數器記錄目前線程執行的指令位址,位元組碼解釋器通過改變該計數器的值,來決定選取下一個要執行的指令。如果執行的是本地方法,值為空(undefined)。

程式計數器的記憶體空間非常小,是 JVM 規定的唯一不會發生記憶體溢出(Out Of Memory)的區域。

3. Java 虛拟機棧

Java 虛拟機棧由棧幀組成,Java 虛拟機棧和其他正常語言的棧類似,存儲本地變量或部分計算結果,處理方法的調用和傳回。虛拟機棧内容不能進行直接操作,隻能用來進行棧幀的入棧和出棧。方法的調用到執行完成對應的就是棧幀的入棧和出棧過程。

Java 虛拟機棧的生命周期和線程對應,線上程建立的同時建立,和程式計數器一樣都是線程私有記憶體區域。

JVM 深入淺出 :一文看懂 JVM 記憶體結構

Java 虛拟機規範對虛拟機棧大小有這樣的描述:

  • 可以使用固定大小或者動态擴充和收縮。如果是固定大小,空間大小在棧建立的時候就會确定下來。
  • 可以配置 Java 虛拟機棧的初始大小。
  • 如果棧空間可以動态擴充或者收縮,可以配置棧的最大值和最小值。

HotSpot 虛拟機棧的配置:

  • -Xss,設定虛拟機棧大小,JDK1.5 之後預設為 1M。棧深度受到這個堆棧大小的限制。在固定實體記憶體下減小 Java 虛拟機棧大小可以産生更多線程,但是一個程序的線程數量有限制,不能無限增加。

Java 虛拟機棧可能會發生的異常有:

  • 如果線程請求需要的棧深度大于 JVM 限定的,會發生

    StackOverflowError

    異常。
  • 如果 JVM 大小可以動态擴充,在擴充的時候記憶體不足,或者在建立新線程時記憶體不夠建立虛拟機棧,均會發生

    OutOfMemoryError

    異常。

3.1. 棧深度

方法的從調用到執行完成,對應了虛拟機棧的入棧到出棧的過程。

在編譯期就可以确認局部變量表的大小和操作數棧的深度,并且寫入到方法表的 code 屬性中,運作期間不會發生改變。是以在編譯器每個棧幀的需要大小就可以确定了。棧深度由運作期決定。

具體的棧深度受虛拟機棧大小和棧幀大小的影響,要看使用了多少棧幀,棧幀大小多少。每個棧幀的大小不一定一樣,取決于各棧幀對應方法的局部變量表和操作數棧大小等。

假設我們的虛拟機棧大小固定,棧幀數量達到最大值,也就是達到最大深度,深度大小和棧幀大小的示意圖如下:

JVM 深入淺出 :一文看懂 JVM 記憶體結構

上面的示意圖可以看出,在 Java 虛拟機棧大小固定的情況下,如果每個棧幀都很大,最大可用深度就會變小。

上面隻是一個示意圖,實際上虛拟機棧深度沒這麼小。預設情況下 Java 虛拟機棧有 1M,平時開發時的棧幀也不會很大。

當線程請求的棧深度大于虛拟機的所允許的棧深度會發生

StackOverflowError

異常。畢竟如果一個線程不斷地往虛拟機棧中加入棧幀,會消耗掉大量的記憶體,影響到其他線程的執行。

比如寫了一個遞歸方法,沒有設定退出條件,當要超過該線程的虛拟機棧達到最大深度會發生異常。

3.2. 棧幀

棧幀用來存儲方法執行需要用到的資料。同時還可以執行動态連結,傳回值給方法,分發異常。是以一個棧幀一般會劃分成以下幾個區域:局部變量表、操作數棧、動态連結、方法出口。

棧幀的生命周期和方法對應,在方法調用的時候就會建立新的棧幀,當方法執行結束時棧幀銷毀棧幀。即使是因為未捕獲異常退出方法,棧幀也會被銷毀。棧幀的記憶體由 JVM 虛拟機棧配置設定。每個棧幀有自己獨立的局部變量表、操作數棧、指向運作時常量池的引用。

棧幀的内容可擴充,比如加入調試資訊。

在編譯期就可以根據棧幀對應的方法代碼,确定局部變量表和操作數棧的大小。棧幀的具體大小依賴于 JVM 虛拟機的實作。編譯期決定了大小,方法被調用時配置設定記憶體。

線程在同一時刻隻會處理一個棧幀,被稱為目前幀,位于 Java 虛拟棧的棧頂。該幀對應的方法被稱為目前方法,定義該方法的類被稱為目前類。方法的執行會操作目前幀的局部變量表和操作數棧。

調用新方法時,目前幀暫停,新的棧幀加入到虛拟機棧的棧頂并成為新的目前幀,開始處理新方法。當方法結束調用,目前幀出棧,傳回處理結果,回到上一個棧幀,上一個棧幀成為目前幀,繼續操作局部變量表和操作數棧。

棧幀屬于目前線程私有,不會被其他線程引用到。

3.2.1. 局部變量表

每一個棧幀都會有一個局部變量表,大小在編譯期就決定,用來記錄方法執行需要用到的請求參數、局部變量,如果不是靜态方法的話,還會存儲

this

指針來表示目前對象執行個體。

局部變量的存儲基本機關為 變量槽(Variable Slot)。單個 Slot 可以存儲 boolean,byte,char,short,int,float,reference 或者 returnAddress。兩個 Slot 可以存儲 long 和 double。虛拟機規範沒有對 Slot 的實體記憶體大小做出明确規定,可以随着處理器、作業系統和虛拟機的不同而變化。但因為 int、float 等都可以用 32 位的實體記憶體存放,是以一個 Slot 的實體記憶體必須大于 32 位。

局部變量表采用 索引 進行尋址。第一個局部變量的索引為 0。在執行個體方法中,始終使用局部變量 0 用來表示目前對象執行個體,在 Java 中就是 this 指針。是以執行個體方法的局部變量的索引總是從 1 開始。

long 和 double 比較特殊,需要使用兩個連續的 Slot 存儲。這樣會占用兩個索引,取值小的那個。比如一個 double 存入局部變量表,它的索引值是 n,其實占用了 n 和 n+1 兩個索引,而 n+1索引是無法加載的。下一個局部變量的索引為 n+2。虛拟機規範并沒有要求 n 一定是偶數,是以在在局部變量表中 long 和 double 并不一定是要 64 位對齊的。不同 JVM 的實作,可以選擇合适的方式實作兩個局部變量存儲 long 和 double。

這裡做個實驗,建立一個空方法,請求參數包含所有基礎資料類型和一個 String 引用類型,方法内有一個 String 局部變量。

public void show(boolean a, byte b, char c, short d, int e, long f, float h, double i, String j) {
	String str = "str";
}
           

使用

javap -v

檢視

show

方法在 class 檔案中的局部變量表。

public void show(boolean, byte, char, short, int, long, float, double, java.lang.String);
    descriptor: (ZBCSIJFDLjava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=13, args_size=10
         0: ldc           #2                  // String str
         2: astore        12
         4: return
      LineNumberTable:
        line 14: 0
        line 15: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Loblee/demo/jvm/stack/SimpleObject;
            0       5     1     a   Z
            0       5     2     b   B
            0       5     3     c   C
            0       5     4     d   S
            0       5     5     e   I
            0       5     6     f   J
            0       5     8     h   F
            0       5     9     i   D
            0       5    11     j   Ljava/lang/String;
            4       1    12   str   Ljava/lang/String;
           

這個方法的為局部變量表

LocalVariableTable

,類加載後會作為方法的中繼資料存儲到方法區,然後方法被調用的時候載入到新建立的棧幀中。

可以看到編譯期已經确認了表中每個局部變量的索引和大小。局部變量表的大小已經寫入到

Code

屬性:

locals=13

這 13 個基本機關是如何計算出來的?我們上面的案例,所有方法參數一共需要的基本機關數

1 + 1 + 1 + 1 + 1 + 2 + 1 + 2 + 1 = 11

,一個局部變量 str 占用 1 個 Slot,有 12 個基本機關了。還有一個 Slot 呢?

這個是執行個體方法,加入了 this 指針用來表示目前對象執行個體的引用,在 Slot 0 中:

LocalVariableTable:
Start  Length  Slot  Name   Signature
	0       5     0  this   Loblee/demo/jvm/stack/SimpleObject;
           

this 指針占用 1 個 Slot,是以局部變量表總體大小為 13 個 Slot。

因為 this 指針是通過參數預設傳遞給方法的,應該歸到方法參數中,是以實際該方法有 10 個參數,也寫入到了 code 屬性:

args_size=10

從反編譯的局部變量表還可以看到索引的設計,

show

中參數 f 為 long 類型,索引到 Slot 6,因為占用兩個 Slot,下一個變量 h 索引到 Slot 8。

JVM 對局部變量表進行了優化,變量槽 Slot 是可以複用的。

如果是靜态方法的話就不存在 this 引用了。比如我們建立一個靜态方法

staticShow

public static void staticShow(boolean a, byte b, char c) {
	String str = "str";
}
           

使用

javap -v

檢視局部變量表如下:

LocalVariableTable:
Start  Length  Slot  Name   Signature
	0       8     0     a   Z
	0       8     1     b   B
	0       8     2     c   C
	3       5     3  str1   Ljava/lang/String;
	7       1     4  str2   Ljava/lang/String;
           

3.2.2. 操作數棧

每一個棧幀都有一個後進先出(LIFO)的操作數棧。操作數棧應用于位元組碼執行引擎中,JVM 描述位元組碼執行引擎是基于 “棧” 的,指的就是操作數棧。

操作數棧的每個條目可以儲存 JVM 任何類型的值,long 和 double 占據深度的兩個機關,其他類型占據一個機關。操作數棧的最大深度由編譯期通過方法要執行的位元組碼計算出來,并記錄在 Code 屬性中。

棧幀剛建立時,操作數棧為空。JVM 提供了一系列位元組碼指令,将資料從局部變量表加載到操作數棧中。還有一些指令,從操作數棧中讀取操作數,進行處理,然後把結果入棧。操作數棧還可以用來準備參數傳遞給方法,或者接收方法傳回結果。比如,指令

iadd

用來對兩個 int 值進行相加。之前的指令已經将兩個 int 值壓入到操作數棧中了,

iadd

将兩個 int 值出棧,相加後将和入棧。

操作數棧中的資料,必須用合适的類型的位元組碼指令進行操作。比如入棧兩個 int 值,不能當做 long 處理。入棧 float 不能使用

iadd

指令進行相加。有少量的 JVM 指令不關心值的類型,這些指令無法修改值。在類加載流程中,類檔案的校驗階段,會強制實施。

設計了一個

calculate

方法來做一些加減法計算:

public int calculate(int a, int b) {
	int c = a + b;
	int d = a - b;
	int e = c + d;
	return e;
}
           

反編譯得到:

public int calculate(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=6, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: istore_3
         4: iload_1
         5: iload_2
         6: isub
         7: istore        4
         9: iload_3
        10: iload         4
        12: iadd
        13: istore        5
        15: iload         5
        17: ireturn
           

可以看到操作數棧深度最大為 2,本地變量表大小 6 個 Slot(索引 0 - 5)。這些位元組碼的解讀如下:

0: iload_1				加載 Slot 1(從局部變量表加載,1 表示索引)。實際為從局部變量表加載 a。
         1: iload_2				加載 Slot 2。實際為從局部變量表加載 a。
         2: iadd				執行加法。實際為 a + b。
         3: istore_3			存儲計算結果到 Slot 3。實際為存儲 c 到局部變量表。
         4: iload_1				加載 Slot 1。實際為從局部變量表加載 a。
         5: iload_2				加載 Slot 2。實際為從局部變量表加載 b。
         6: isub				執行減法。實際為 a - b。
         7: istore        4		存儲計算結果到 Slot 4。實際為存儲 d 到局部變量表。
         9: iload_3				加載 Slot 3。實際為從局部變量表加載 c。
        10: iload         4		加載 Slot 4。實際為從局部變量表加載 d。
        12: iadd				執行加法。實際為 c + d。
        13: istore        5		存儲計算結果到 Slot 5。實際為存儲 e 到局部變量表。
        15: iload         5		加載 Slot 5 的資料。實際為從局部變量表加載 e。
        17: ireturn				傳回計算結果
           

我們傳入

a = 1, b = 2

進行計算

calculate(1, 2)

,第一個加法操作操作數棧的變化如下:

JVM 深入淺出 :一文看懂 JVM 記憶體結構

這裡的代碼是可以優化的,因為局部變量 e 沒有做其他計算,可以直接傳回。如果直接傳回結果會有什麼效果?代碼如下:

public int calculate(int a, int b) {
	int c = a + b;
	int d = a - b;
	return c + d;
}
           

檢視位元組碼如下:

public int calculate(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=5, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: istore_3
         4: iload_1
         5: iload_2
         6: isub
         7: istore        4
         9: iload_3
        10: iload         4
        12: iadd
        13: ireturn
           

局部變量表少了一個 Slot,也就是原本 e 的存儲空間。要執行的位元組碼指令也少了 3 條。是以平時開發過程中要注意優化,可以提高性能。

3.2.3. 動态連結

每一個幀都包含了一個指向運作時常量池的引用,用來實作位元組碼中的 動态連結(Dynamic Linking)。類檔案中包含了一些字段和方法的符号引用。動态連結會将這些符号引用轉換成直接引用,比如在記憶體中的具體偏移位址。

如果對應的類還沒有被加載,會觸發該類的加載流程。

符号引用記錄在類常量池中,是一個由字面量組成的字元串,和具體位址無關。比如所有對象的類構造方法的符号引用為

java/lang/Object."<init>":()V

。編譯并不知道運作時的位址,是以用符号引用代替。

動态連結又稱動态綁定。除了該方式,還有種發生在類檔案加載過程中,這個這個階段就把符号引用轉換為直接引用,這樣的方式為饑餓方式或者靜态綁定。

靜态綁定和動态綁定都可以歸為是類加載機制中的 解析(Resolution) 的一部分。

JVM 深入淺出 :一文看懂 JVM 記憶體結構

可以看出類加載機制中的環節是有可能交叉進行的。比如解析可能發生在準備階段後,靜态綁定。也可能延遲到初始化後,在棧幀建立後進行動态綁定。

綁定隻發生一次,綁定後不再更改。

3.2.4. 方法正常結束

方法調用結束,沒有發生異常。這裡指直接傳回結果或者是顯式調用 throw 抛出異常。

被調用方法的結果需要傳遞給調用者方法。被調用的方法會執行和方法傳回相關的指令,這些指令和傳回值的類型對應。

目前棧會被複原為調用者方法的執行狀态,包括局部變量表和操作數棧的資料,程式計數器會跳過剛剛調用方法的指令指向下一條。被調用方法的傳回值被加入到操作數棧中,程式繼續運作。

3.2.5. 方法異常結束

方法内部發生了異常,而且沒有被捕獲,方法會被終止,并且沒有傳回值給調用者。

4. 堆

堆由 JVM 所有的線程共享,一般情況下是 JVM 記憶體區域中最大的一塊。按照 JVM 虛拟機規範,堆是一個用來存儲類對象執行個體或者數組的運作時資料區。

在 HopSpot 上,類對象執行個體不一定就是放在堆中,應用了 JIT(Just-In-Time) 技術,進行逃逸分析(Escape Analysis)和标量替換(Scalar Replacement)。符合條件的對象執行個體會在棧上配置設定。

JVM 啟動的時候堆就會建立。堆内對象執行個體不會顯式釋放,由自動記憶體管理系統,也就是垃圾收集器進行回收,是垃圾收集器主要管理區域。JVM 規範沒有說明垃圾收集器應該是怎樣的,具體由實作由 JVM 廠商來提供。

比如 HotSpot 虛拟機中,垃圾回收器采用分代回收算法,會将堆進行進一步細分,分為新生代和老生代。新生代還可細分為 Eden 、From Survivor 和 To Survivor。這實際上是為了能夠更好地服務于垃圾回收。HotSpot 在 JDK 1.7 中堆還有一個永久代,其實是 JVM 規範中方法區的實作,在 JDK1.8 移除。

HotSpot 的 JDK 1.7 堆圖示:

JVM 深入淺出 :一文看懂 JVM 記憶體結構

HopSpot 的 JDK 1.8 堆圖示,永久代(PermGen)被移除,使用元空間(Metaspace)存儲類資訊。

JVM 深入淺出 :一文看懂 JVM 記憶體結構

新生代和老年代的記憶體配置設定流程:

  • 優先 Eden 配置設定,Eden 空間不足會觸發 Minor GC。
  • Minor GC 後,Eden + S0 還存活的對象移動到 S1 中,清空 S0。
  • S1 放不下,存活次數達到要求的對象移動到老年代。
  • 大對象直接配置設定到老年代。
  • 老年代記憶體不足會發生 Major GC
  • 進行垃圾回收後,Eden 仍然沒有足夠的空間,抛出

    OutOfMemory

    異常。

Java 虛拟機規範對堆大小有這樣的描述:

  • 可以是固定大小,也可以動态的擴充和收縮。
  • 堆的記憶體不一定要連續。(邏輯上連續)
  • 可以配置本地方法棧初始大小,如果可動态擴充和收縮,可配置最大值和最小值。

主流虛拟機都是采用可動态擴充和收縮的方式實作的。堆記憶體實體上可以不連續,但是邏輯上需要連續。

HotPot 虛拟機的堆記憶體配置:

  • -Xms,初始大小,預設實體記憶體的 1/64。
  • -Xmx,最大記憶體,預設實體記憶體的 1/4。
  • -Xmn,新生代大小,因為持久代的大小一般預設為 64M,在整個堆固定的情況下,增大新生代會相應地減少老年代的大小。官方推薦
  • -XX:NewSize,新生代最小空間大小。
  • -XX:MaxNewSize,新生代最大空間大小。
  • -XX:NewRatio,新生代和老年代的比例,新生代和老年代的預設比例為 1:2。
  • -XX:SurvivorRatio,Eden 和 Survivor 的比例,預設為 Eden:S0:S1 = 8:1:1,即 survivor = 1/10 新生代大小。

HotSpot 采用的就是動态擴充和收縮的方式,根據堆的空閑情況,當空閑大于 70%,會減少至 -Xms;空閑小于 40%,會增大到 -Xmx。是以伺服器如果配置 -Xms = -Xmx,可以避免堆自動擴充。

堆會發生的異常:

  • 如果程式請求的堆記憶體大于 JVM 記憶體管理系統能提供的最大值,會抛出

    OutOfMemoryError

    異常。

5. 方法區

方法區由 JVM 所有線程共享。方法區類似一個用來存儲編譯後的代碼的區域。主要用來存儲加載的類資訊,運作時常量池,類和方法的資料,即時編譯後的代碼等。

JVM 啟動的時候方法區就會建立。

根據 JVM 虛拟機規範,方法區邏輯上是堆的一部分,實作上可以選擇不進行垃圾回收,并且沒有要求方法區的位置等。是以在方法區的具體實作各個虛拟機又不同的方式。雖然 JVM 虛拟機規範把方法區邏輯上劃給了堆,為了和實際堆進行了區分,方法區還叫做 “非堆”。

Java 虛拟機規範對方法區大小的描述:

  • 可以是固定大小,也可以動态的擴充和收縮。
  • 方法區的記憶體不一定要連續。
  • 使用者或者開發者能夠配置方法區初始大小,如果方法區可以動态擴充或收縮,需要提供方法區的最大值和最小值。

HotSpot 在 JDK1.7 中方法區記憶體大小配置:

  • -XX:PermSize,最小可配置設定空間,初始配置設定空間。
  • -XX:MaxPermSize,最大可配置設定空間,預設大小為 64M(64 位 JVM 預設為 85M)

在 JDK1.8 使用了元空間後,方法區的大小配置:

  • -XX:MetaspaceSize,初始空間大小。
  • -XX:MaxMetaspaceSize,最大空間大小,預設是沒有限制的。

方法區可能發生的異常:

  • 如果方法區請求的記憶體無法被滿足,抛出

    OutOfMemoryError

    異常。

5.1. 去永久代過程

HotSpot 虛拟機在 JDK1.7 采用永久代,在堆中配置設定記憶體。在 JDK1.8 後使用元空間,使用本地記憶體。

從 JDK1.7 開始 “去永久代”,JDK 1.7 将靜态變量、字元串常量池移動到堆記憶體中,JDK1.8 去掉永久代,将類資訊、即時編譯後的代碼等移動到了元空間。

JVM 深入淺出 :一文看懂 JVM 記憶體結構

之是以要進行去永久代,主要還是該方案存在很多問題,留下很多 bug。主要有:

  • 字元串存在永久代,容易發生記憶體溢出。
  • 類資訊比較難确定大小,永久代的大小難以指定,太小永久代容易 OOM,太大老年代容易 OOM。
  • 永久代 GC 回收複雜,效率低。

6. 運作時常量池

運作時常量池是 class 檔案的常量池在運作時的表示。主要有字面量和符号引用。

要了解運作時常量池,我們得先了解 class 的常量池。

建立類 ObjectA 和 Object B,其中 ObjectA 如下:

public class ObjectA {

    private ObjectB b;

    public void setB(ObjectB b) {
        this.b = b;
    }
    
    public ObjectB getB() {
        return b;
    }
}
           

編譯後使用

javap -v

檢視 class 檔案中的常量池如下。

JVM 深入淺出 :一文看懂 JVM 記憶體結構

運作時,在進行類加載時,類常量池會被載入到 JVM 方法區。

JVM 虛拟機規範沒有限制運作時常量池隻能放編譯期的常量,虛拟機的實作可以自行支援。比如 HotSpot 虛拟機, Java 調用

String.intern()

方法,可以在運作期把常量加入池中。

在 HotSpot JDK 1.7 之後,對常量池進行了優化:字元串常量池被放在了 JVM 堆中,運作時常量池的字面量也存在 JVM 堆中,而符号引用被移動到了本地記憶體。

以下的異常可能會發生:

  • 當建立一個 class 或者 interface 時,如果運作時常量池構造需要的記憶體超過 JVM 所能提供的,抛出

    OutOfMemoryError

    異常。

7. 本地方法棧

JVM 的實作可能需要使用 “C 棧” 去支援本地方法調用。有可能使用 C 之類的語言,實作 JVM 指令的解釋器,也會使用到本地方法棧。本地方法棧和 Java 虛拟機棧類似,隻是這裡提供的是本地方法服務。虛拟機規範沒有明确指出本地方法棧使用什麼語言、資料結構等,不同廠商的虛拟機又不同的實作。比如 HotSpot 虛拟機把本地方法棧和 Java 虛拟機棧合并了。

本地方法棧的生命周期線程對應,線程建立的時候建立。如果 JVM 不需要調用本地方法,可以不需要本地方法棧。

JVM 規範對本地方法棧大小的描述

  • 可以使用固定大小,或者動态擴充和收縮。如果是固定大小,當棧被建立的時候能夠獨立選擇。
  • 可以配置本地方法棧初始大小,如果可動态擴充和收縮,可配置最大值和最小值。

以下異常可能發生:

  • 如果線程請求的棧深度大于系統規定的,報

    StackOverflowError

  • 如果本地方法棧可以動态擴充,沒有足夠的記憶體擴充。或者建立新的線程沒有足夠的記憶體建立本地方法棧,抛出

    OutOfMemoryError

    異常。

8. 參考資料

  • Java Language and Virtual Machine Specifications
  • 深入了解 Java 虛拟機(周志明)