深入理解 JVM 內存結構

1. 概述

JVM 把內存進行了劃分,不同的內存區(qū)域有不同的功能。有的內存區(qū)域是線程私有的,比如 Java 虛擬機棧、本地方法棧和程序計數(shù)器,每一條線程都有自己獨立的空間。有的內存區(qū)域是線程共享的,比如方法區(qū)和堆。

所以不同內存區(qū)域的功能、作用域和生命周期是不同的。本文做一個詳細的分析。

根據 JVM 虛擬機規(guī)范,內存結構如下:

JVM-內存區(qū)域

JVM 虛擬機規(guī)范屬于概念模型,具體的實現(xiàn)各個廠商的會有所差異。比如方法區(qū)的設計,hotspot 在 1.7 之前使用永久代,1.7 后使用元空間。

本文主要分析 HotSpot 虛擬機的實現(xiàn)。

2. 程序計數(shù)器

JVM 支持多線程,采用時間片輪轉的方式實現(xiàn)多線程并發(fā)。一個內核每一刻只能有一個線程執(zhí)行,多線程下需要線程上下文切換。為了確保切換過程中,不同的線程指令和數(shù)據不會發(fā)生混亂,需要單獨開辟內存空間給每個線程,進行線程隔離。這些區(qū)域包含了程序計數(shù)器、虛擬機棧、本地方法棧。這些都是線程私有內存,生命周期和線程一致。

如果執(zhí)行的不是本地方法,程序計數(shù)器記錄當前線程執(zhí)行的指令地址,字節(jié)碼解釋器通過改變該計數(shù)器的值,來決定選取下一個要執(zhí)行的指令。如果執(zhí)行的是本地方法,值為空(undefined)。

程序計數(shù)器的內存空間非常小,是 JVM 規(guī)定的唯一不會發(fā)生內存溢出(Out Of Memory)的區(qū)域。

3. Java 虛擬機棧

Java 虛擬機棧由棧幀組成,Java 虛擬機棧和其他常規(guī)語言的棧類似,存儲本地變量或部分計算結果,處理方法的調用和返回。虛擬機棧內容不能進行直接操作,只能用來進行棧幀的入棧和出棧。方法的調用到執(zhí)行完成對應的就是棧幀的入棧和出棧過程。

Java 虛擬機棧的生命周期和線程對應,在線程創(chuàng)建的同時創(chuàng)建,和程序計數(shù)器一樣都是線程私有內存區(qū)域。

JVM-虛擬機棧

Java 虛擬機規(guī)范對虛擬機棧大小有這樣的描述:

  • 可以使用固定大小或者動態(tài)擴展和收縮。如果是固定大小,空間大小在棧創(chuàng)建的時候就會確定下來。
  • 可以配置 Java 虛擬機棧的初始大小。
  • 如果??臻g可以動態(tài)擴展或者收縮,可以配置棧的最大值和最小值。

HotSpot 虛擬機棧的配置:

  • -Xss,設置虛擬機棧大小,JDK1.5 之后默認為 1M。棧深度受到這個堆棧大小的約束。在固定物理內存下減小 Java 虛擬機棧大小可以產生更多線程,但是一個進程的線程數(shù)量有約束,不能無限增加。

Java 虛擬機??赡軙l(fā)生的異常有:

  • 如果線程請求需要的棧深度大于 JVM 限定的,會發(fā)生 StackOverflowError 異常。
  • 如果 JVM 大小可以動態(tài)擴展,在擴展的時候內存不足,或者在創(chuàng)建新線程時內存不夠創(chuàng)建虛擬機棧,均會發(fā)生 OutOfMemoryError 異常。

3.1. 棧深度

方法的從調用到執(zhí)行完成,對應了虛擬機棧的入棧到出棧的過程。

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

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

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

JVM-虛擬機棧深度

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

上面只是一個示意圖,實際上虛擬機棧深度沒這么小。默認情況下 Java 虛擬機棧有 1M,平時開發(fā)時的棧幀也不會很大。

當線程請求的棧深度大于虛擬機的所允許的棧深度會發(fā)生 StackOverflowError 異常。畢竟如果一個線程不斷地往虛擬機棧中加入棧幀,會消耗掉大量的內存,影響到其他線程的執(zhí)行。

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

3.2. 棧幀

棧幀用來存儲方法執(zhí)行需要用到的數(shù)據。同時還可以執(zhí)行動態(tài)鏈接,返回值給方法,分發(fā)異常。所以一個棧幀一般會劃分成以下幾個區(qū)域:局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口。

棧幀的生命周期和方法對應,在方法調用的時候就會創(chuàng)建新的棧幀,當方法執(zhí)行結束時棧幀銷毀棧幀。即使是因為未捕獲異常退出方法,棧幀也會被銷毀。棧幀的內存由 JVM 虛擬機棧分配。每個棧幀有自己獨立的局部變量表、操作數(shù)棧、指向運行時常量池的引用。

棧幀的內容可擴展,比如加入調試信息。

在編譯期就可以根據棧幀對應的方法代碼,確定局部變量表和操作數(shù)棧的大小。棧幀的具體大小依賴于 JVM 虛擬機的實現(xiàn)。編譯期決定了大小,方法被調用時分配內存。

線程在同一時刻只會處理一個棧幀,被稱為當前幀,位于 Java 虛擬棧的棧頂。該幀對應的方法被稱為當前方法,定義該方法的類被稱為當前類。方法的執(zhí)行會操作當前幀的局部變量表和操作數(shù)棧。

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

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

3.2.1. 局部變量表

每一個棧幀都會有一個局部變量表,大小在編譯期就決定,用來記錄方法執(zhí)行需要用到的請求參數(shù)、局部變量,如果不是靜態(tài)方法的話,還會存儲 this 指針來表示當前對象實例。

局部變量的存儲基本單位為 變量槽(Variable Slot)。單個 Slot 可以存儲 boolean,byte,char,short,int,float,reference 或者 returnAddress。兩個 Slot 可以存儲 long 和 double。虛擬機規(guī)范沒有對 Slot 的物理內存大小做出明確規(guī)定,可以隨著處理器、操作系統(tǒng)和虛擬機的不同而變化。但因為 int、float 等都可以用 32 位的物理內存存放,所以一個 Slot 的物理內存必須大于 32 位。

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

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

這里做個實驗,創(chuàng)建一個空方法,請求參數(shù)包含所有基礎數(shù)據類型和一個 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 ,類加載后會作為方法的元數(shù)據存儲到方法區(qū),然后方法被調用的時候載入到新創(chuàng)建的棧幀中。

可以看到編譯期已經確認了表中每個局部變量的索引和大小。局部變量表的大小已經寫入到 Code 屬性: locals=13 。

這 13 個基本單位是如何計算出來的?我們上面的案例,所有方法參數(shù)一共需要的基本單位數(shù) 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 指針是通過參數(shù)默認傳遞給方法的,應該歸到方法參數(shù)中,所以實際該方法有 10 個參數(shù),也寫入到了 code 屬性:args_size=10。

從反編譯的局部變量表還可以看到索引的設計,show 中參數(shù) f 為 long 類型,索引到 Slot 6,因為占用兩個 Slot,下一個變量 h 索引到 Slot 8。

JVM 對局部變量表進行了優(yōu)化,變量槽 Slot 是可以復用的。

如果是靜態(tài)方法的話就不存在 this 引用了。比如我們創(chuàng)建一個靜態(tài)方法 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. 操作數(shù)棧

每一個棧幀都有一個后進先出(LIFO)的操作數(shù)棧。操作數(shù)棧應用于字節(jié)碼執(zhí)行引擎中,JVM 描述字節(jié)碼執(zhí)行引擎是基于 “?!?的,指的就是操作數(shù)棧。

操作數(shù)棧的每個條目可以保存 JVM 任何類型的值,long 和 double 占據深度的兩個單位,其他類型占據一個單位。操作數(shù)棧的最大深度由編譯期通過方法要執(zhí)行的字節(jié)碼計算出來,并記錄在 Code 屬性中。

棧幀剛創(chuàng)建時,操作數(shù)棧為空。JVM 提供了一系列字節(jié)碼指令,將數(shù)據從局部變量表加載到操作數(shù)棧中。還有一些指令,從操作數(shù)棧中讀取操作數(shù),進行處理,然后把結果入棧。操作數(shù)棧還可以用來準備參數(shù)傳遞給方法,或者接收方法返回結果。比如,指令 iadd 用來對兩個 int 值進行相加。之前的指令已經將兩個 int 值壓入到操作數(shù)棧中了,iadd 將兩個 int 值出棧,相加后將和入棧。

操作數(shù)棧中的數(shù)據,必須用合適的類型的字節(jié)碼指令進行操作。比如入棧兩個 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

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

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

我們傳入 a = 1, b = 2 進行計算 calculate(1, 2),第一個加法操作操作數(shù)棧的變化如下:

JVM-操作數(shù)棧和局部變量表變化

這里的代碼是可以優(yōu)化的,因為局部變量 e 沒有做其他計算,可以直接返回。如果直接返回結果會有什么效果?代碼如下:

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

查看字節(jié)碼如下:

  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 的存儲空間。要執(zhí)行的字節(jié)碼指令也少了 3 條。所以平時開發(fā)過程中要注意優(yōu)化,可以提高性能。

3.2.3. 動態(tài)鏈接

每一個幀都包含了一個指向運行時常量池的引用,用來實現(xiàn)字節(jié)碼中的 動態(tài)鏈接(Dynamic Linking)。類文件中包含了一些字段和方法的符號引用。動態(tài)鏈接會將這些符號引用轉換成直接引用,比如在內存中的具體偏移地址。

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

符號引用記錄在類常量池中,是一個由字面量組成的字符串,和具體地址無關。比如所有對象的類構造方法的符號引用為 java/lang/Object."<init>":()V 。編譯并不知道運行時的地址,所以用符號引用代替。

動態(tài)鏈接又稱動態(tài)綁定。除了該方式,還有種發(fā)生在類文件加載過程中,這個這個階段就把符號引用轉換為直接引用,這樣的方式為饑餓方式或者靜態(tài)綁定。

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

JVM-類加載機制

可以看出類加載機制中的環(huán)節(jié)是有可能交叉進行的。比如解析可能發(fā)生在準備階段后,靜態(tài)綁定。也可能延遲到初始化后,在棧幀創(chuàng)建后進行動態(tài)綁定。

綁定只發(fā)生一次,綁定后不再更改。

3.2.4. 方法正常結束

方法調用結束,沒有發(fā)生異常。這里指直接返回結果或者是顯式調用 throw 拋出異常。

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

當前棧會被復原為調用者方法的執(zhí)行狀態(tài),包括局部變量表和操作數(shù)棧的數(shù)據,程序計數(shù)器會跳過剛剛調用方法的指令指向下一條。被調用方法的返回值被加入到操作數(shù)棧中,程序繼續(xù)運行。

3.2.5. 方法異常結束

方法內部發(fā)生了異常,而且沒有被捕獲,方法會被終止,并且沒有返回值給調用者。

4. 堆

堆由 JVM 所有的線程共享,一般情況下是 JVM 內存區(qū)域中最大的一塊。按照 JVM 虛擬機規(guī)范,堆是一個用來存儲類對象實例或者數(shù)組的運行時數(shù)據區(qū)。

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

JVM 啟動的時候堆就會創(chuàng)建。堆內對象實例不會顯式釋放,由自動內存管理系統(tǒng),也就是垃圾收集器進行回收,是垃圾收集器主要管理區(qū)域。JVM 規(guī)范沒有說明垃圾收集器應該是怎樣的,具體由實現(xiàn)由 JVM 廠商來提供。

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

HotSpot 的 JDK 1.7 堆圖示:

JVM-Heap-1.7

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

JVM-Heap-1.8

新生代和老年代的內存分配流程:

  • 優(yōu)先 Eden 分配,Eden 空間不足會觸發(fā) Minor GC。
  • Minor GC 后,Eden + S0 還存活的對象移動到 S1 中,清空 S0。
  • S1 放不下,存活次數(shù)達到要求的對象移動到老年代。
  • 大對象直接分配到老年代。
  • 老年代內存不足會發(fā)生 Major GC
  • 進行垃圾回收后,Eden 仍然沒有足夠的空間,拋出 OutOfMemory 異常。

Java 虛擬機規(guī)范對堆大小有這樣的描述:

  • 可以是固定大小,也可以動態(tài)的擴展和收縮。
  • 堆的內存不一定要連續(xù)。(邏輯上連續(xù))
  • 可以配置本地方法棧初始大小,如果可動態(tài)擴展和收縮,可配置最大值和最小值。

主流虛擬機都是采用可動態(tài)擴展和收縮的方式實現(xiàn)的。堆內存物理上可以不連續(xù),但是邏輯上需要連續(xù)。

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 采用的就是動態(tài)擴展和收縮的方式,根據堆的空閑情況,當空閑大于 70%,會減少至 -Xms;空閑小于 40%,會增大到 -Xmx。所以服務器如果配置 -Xms = -Xmx,可以避免堆自動擴展。

堆會發(fā)生的異常:

  • 如果程序請求的堆內存大于 JVM 內存管理系統(tǒng)能提供的最大值,會拋出 OutOfMemoryError 異常。

5. 方法區(qū)

方法區(qū)由 JVM 所有線程共享。方法區(qū)類似一個用來存儲編譯后的代碼的區(qū)域。主要用來存儲加載的類信息,運行時常量池,類和方法的數(shù)據,即時編譯后的代碼等。

JVM 啟動的時候方法區(qū)就會創(chuàng)建。

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

Java 虛擬機規(guī)范對方法區(qū)大小的描述:

  • 可以是固定大小,也可以動態(tài)的擴展和收縮。
  • 方法區(qū)的內存不一定要連續(xù)。
  • 用戶或者開發(fā)者能夠配置方法區(qū)初始大小,如果方法區(qū)可以動態(tài)擴展或收縮,需要提供方法區(qū)的最大值和最小值。

HotSpot 在 JDK1.7 中方法區(qū)內存大小配置:

  • -XX:PermSize,最小可分配空間,初始分配空間。

  • -XX:MaxPermSize,最大可分配空間,默認大小為 64M(64 位 JVM 默認為 85M)

在 JDK1.8 使用了元空間后,方法區(qū)的大小配置:

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

方法區(qū)可能發(fā)生的異常:

  • 如果方法區(qū)請求的內存無法被滿足,拋出 OutOfMemoryError 異常。

5.1. 去永久代過程

HotSpot 虛擬機在 JDK1.7 采用永久代,在堆中分配內存。在 JDK1.8 后使用元空間,使用本地內存。

從 JDK1.7 開始 “去永久代”,JDK 1.7 將靜態(tài)變量、字符串常量池移動到堆內存中,JDK1.8 去掉永久代,將類信息、即時編譯后的代碼等移動到了元空間。

JVM-方法區(qū)到元空間

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

  • 字符串存在永久代,容易發(fā)生內存溢出。
  • 類信息比較難確定大小,永久代的大小難以指定,太小永久代容易 OOM,太大老年代容易 OOM。
  • 永久代 GC 回收復雜,效率低。

6. 運行時常量池

運行時常量池是 class 文件的常量池在運行時的表示。主要有字面量和符號引用。

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

創(chuàng)建類 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-Constant Pool

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

JVM 虛擬機規(guī)范沒有約束運行時常量池只能放編譯期的常量,虛擬機的實現(xiàn)可以自行支持。比如 HotSpot 虛擬機, Java 調用 String.intern() 方法,可以在運行期把常量加入池中。

在 HotSpot JDK 1.7 之后,對常量池進行了優(yōu)化:字符串常量池被放在了 JVM 堆中,運行時常量池的字面量也存在 JVM 堆中,而符號引用被移動到了本地內存。

以下的異??赡軙l(fā)生:

  • 當創(chuàng)建一個 class 或者 interface 時,如果運行時常量池構造需要的內存超過 JVM 所能提供的,拋出 OutOfMemoryError 異常。

7. 本地方法棧

JVM 的實現(xiàn)可能需要使用 "C 棧" 去支持本地方法調用。有可能使用 C 之類的語言,實現(xiàn) JVM 指令的解釋器,也會使用到本地方法棧。本地方法棧和 Java 虛擬機棧類似,只是這里提供的是本地方法服務。虛擬機規(guī)范沒有明確指出本地方法棧使用什么語言、數(shù)據結構等,不同廠商的虛擬機又不同的實現(xiàn)。比如 HotSpot 虛擬機把本地方法棧和 Java 虛擬機棧合并了。

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

JVM 規(guī)范對本地方法棧大小的描述

  • 可以使用固定大小,或者動態(tài)擴展和收縮。如果是固定大小,當棧被創(chuàng)建的時候能夠獨立選擇。
  • 可以配置本地方法棧初始大小,如果可動態(tài)擴展和收縮,可配置最大值和最小值。

以下異??赡馨l(fā)生:

  • 如果線程請求的棧深度大于系統(tǒng)規(guī)定的,報 StackOverflowError 。
  • 如果本地方法??梢詣討B(tài)擴展,沒有足夠的內存擴展?;蛘邉?chuàng)建新的線程沒有足夠的內存創(chuàng)建本地方法棧,拋出 OutOfMemoryError異常。

8. 參考資料

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

相關閱讀更多精彩內容

  • 第二部分 自動內存管理機制 第二章 java內存異常與內存溢出異常 運行數(shù)據區(qū)域 程序計數(shù)器:當前線程所執(zhí)行的字節(jié)...
    小明oh閱讀 1,298評論 0 2
  • 內存溢出和內存泄漏的區(qū)別 內存溢出:out of memory,是指程序在申請內存時,沒有足夠的內存空間供其使用,...
    Aimerwhy閱讀 808評論 0 1
  • 參考原地址 JVM內存模型 Java虛擬機(Java Virtual Machine=JVM)的內存空間分為五個部...
    流年劃破容顏_cc55閱讀 372評論 0 0
  • 《深入理解Java虛擬機》筆記_第一遍 先取看完這本書(JVM)后必須掌握的部分。 第一部分 走近 Java 從傳...
    xiaogmail閱讀 5,481評論 1 34
  • 無題 人生就是一場聚會, 接著是一場分離, 然后又是下一次的聚會, 下一次的分離, 反反復復。 每一次的聚會, 都...
    朝花夕拾木本易閱讀 1,293評論 13 14

友情鏈接更多精彩內容