1. 概述
JVM 把內(nèi)存進(jìn)行了劃分,不同的內(nèi)存區(qū)域有不同的功能。有的內(nèi)存區(qū)域是線程私有的,比如 Java 虛擬機(jī)棧、本地方法棧和程序計數(shù)器,每一條線程都有自己獨立的空間。有的內(nèi)存區(qū)域是線程共享的,比如方法區(qū)和堆。
所以不同內(nèi)存區(qū)域的功能、作用域和生命周期是不同的。本文做一個詳細(xì)的分析。
根據(jù) JVM 虛擬機(jī)規(guī)范,內(nèi)存結(jié)構(gòu)如下:

JVM 虛擬機(jī)規(guī)范屬于概念模型,具體的實現(xiàn)各個廠商的會有所差異。比如方法區(qū)的設(shè)計,hotspot 在 1.7 之前使用永久代,1.7 后使用元空間。
本文主要分析 HotSpot 虛擬機(jī)的實現(xiàn)。
2. 程序計數(shù)器
JVM 支持多線程,采用時間片輪轉(zhuǎn)的方式實現(xiàn)多線程并發(fā)。一個內(nèi)核每一刻只能有一個線程執(zhí)行,多線程下需要線程上下文切換。為了確保切換過程中,不同的線程指令和數(shù)據(jù)不會發(fā)生混亂,需要單獨開辟內(nèi)存空間給每個線程,進(jìn)行線程隔離。這些區(qū)域包含了程序計數(shù)器、虛擬機(jī)棧、本地方法棧。這些都是線程私有內(nèi)存,生命周期和線程一致。
如果執(zhí)行的不是本地方法,程序計數(shù)器記錄當(dāng)前線程執(zhí)行的指令地址,字節(jié)碼解釋器通過改變該計數(shù)器的值,來決定選取下一個要執(zhí)行的指令。如果執(zhí)行的是本地方法,值為空(undefined)。
程序計數(shù)器的內(nèi)存空間非常小,是 JVM 規(guī)定的唯一不會發(fā)生內(nèi)存溢出(Out Of Memory)的區(qū)域。
3. Java 虛擬機(jī)棧
Java 虛擬機(jī)棧由棧幀組成,Java 虛擬機(jī)棧和其他常規(guī)語言的棧類似,存儲本地變量或部分計算結(jié)果,處理方法的調(diào)用和返回。虛擬機(jī)棧內(nèi)容不能進(jìn)行直接操作,只能用來進(jìn)行棧幀的入棧和出棧。方法的調(diào)用到執(zhí)行完成對應(yīng)的就是棧幀的入棧和出棧過程。
Java 虛擬機(jī)棧的生命周期和線程對應(yīng),在線程創(chuàng)建的同時創(chuàng)建,和程序計數(shù)器一樣都是線程私有內(nèi)存區(qū)域。

Java 虛擬機(jī)規(guī)范對虛擬機(jī)棧大小有這樣的描述:
- 可以使用固定大小或者動態(tài)擴(kuò)展和收縮。如果是固定大小,空間大小在棧創(chuàng)建的時候就會確定下來。
- 可以配置 Java 虛擬機(jī)棧的初始大小。
- 如果??臻g可以動態(tài)擴(kuò)展或者收縮,可以配置棧的最大值和最小值。
HotSpot 虛擬機(jī)棧的配置:
- -Xss,設(shè)置虛擬機(jī)棧大小,JDK1.5 之后默認(rèn)為 1M。棧深度受到這個堆棧大小的約束。在固定物理內(nèi)存下減小 Java 虛擬機(jī)棧大小可以產(chǎn)生更多線程,但是一個進(jìn)程的線程數(shù)量有約束,不能無限增加。
Java 虛擬機(jī)??赡軙l(fā)生的異常有:
- 如果線程請求需要的棧深度大于 JVM 限定的,會發(fā)生
StackOverflowError異常。 - 如果 JVM 大小可以動態(tài)擴(kuò)展,在擴(kuò)展的時候內(nèi)存不足,或者在創(chuàng)建新線程時內(nèi)存不夠創(chuàng)建虛擬機(jī)棧,均會發(fā)生
OutOfMemoryError異常。
3.1. 棧深度
方法的從調(diào)用到執(zhí)行完成,對應(yīng)了虛擬機(jī)棧的入棧到出棧的過程。
在編譯期就可以確認(rèn)局部變量表的大小和操作數(shù)棧的深度,并且寫入到方法表的 code 屬性中,運行期間不會發(fā)生改變。所以在編譯器每個棧幀的需要大小就可以確定了。棧深度由運行期決定。
具體的棧深度受虛擬機(jī)棧大小和棧幀大小的影響,要看使用了多少棧幀,棧幀大小多少。每個棧幀的大小不一定一樣,取決于各棧幀對應(yīng)方法的局部變量表和操作數(shù)棧大小等。
假設(shè)我們的虛擬機(jī)棧大小固定,棧幀數(shù)量達(dá)到最大值,也就是達(dá)到最大深度,深度大小和棧幀大小的示意圖如下:

上面的示意圖可以看出,在 Java 虛擬機(jī)棧大小固定的情況下,如果每個棧幀都很大,最大可用深度就會變小。
上面只是一個示意圖,實際上虛擬機(jī)棧深度沒這么小。默認(rèn)情況下 Java 虛擬機(jī)棧有 1M,平時開發(fā)時的棧幀也不會很大。
當(dāng)線程請求的棧深度大于虛擬機(jī)的所允許的棧深度會發(fā)生 StackOverflowError 異常。畢竟如果一個線程不斷地往虛擬機(jī)棧中加入棧幀,會消耗掉大量的內(nèi)存,影響到其他線程的執(zhí)行。
比如寫了一個遞歸方法,沒有設(shè)置退出條件,當(dāng)要超過該線程的虛擬機(jī)棧達(dá)到最大深度會發(fā)生異常。
3.2. 棧幀
棧幀用來存儲方法執(zhí)行需要用到的數(shù)據(jù)。同時還可以執(zhí)行動態(tài)鏈接,返回值給方法,分發(fā)異常。所以一個棧幀一般會劃分成以下幾個區(qū)域:局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口。
棧幀的生命周期和方法對應(yīng),在方法調(diào)用的時候就會創(chuàng)建新的棧幀,當(dāng)方法執(zhí)行結(jié)束時棧幀銷毀棧幀。即使是因為未捕獲異常退出方法,棧幀也會被銷毀。棧幀的內(nèi)存由 JVM 虛擬機(jī)棧分配。每個棧幀有自己獨立的局部變量表、操作數(shù)棧、指向運行時常量池的引用。
棧幀的內(nèi)容可擴(kuò)展,比如加入調(diào)試信息。
在編譯期就可以根據(jù)棧幀對應(yīng)的方法代碼,確定局部變量表和操作數(shù)棧的大小。棧幀的具體大小依賴于 JVM 虛擬機(jī)的實現(xiàn)。編譯期決定了大小,方法被調(diào)用時分配內(nèi)存。
線程在同一時刻只會處理一個棧幀,被稱為當(dāng)前幀,位于 Java 虛擬棧的棧頂。該幀對應(yīng)的方法被稱為當(dāng)前方法,定義該方法的類被稱為當(dāng)前類。方法的執(zhí)行會操作當(dāng)前幀的局部變量表和操作數(shù)棧。
調(diào)用新方法時,當(dāng)前幀暫停,新的棧幀加入到虛擬機(jī)棧的棧頂并成為新的當(dāng)前幀,開始處理新方法。當(dāng)方法結(jié)束調(diào)用,當(dāng)前幀出棧,返回處理結(jié)果,回到上一個棧幀,上一個棧幀成為當(dāng)前幀,繼續(xù)操作局部變量表和操作數(shù)棧。
棧幀屬于當(dāng)前線程私有,不會被其他線程引用到。
3.2.1. 局部變量表
每一個棧幀都會有一個局部變量表,大小在編譯期就決定,用來記錄方法執(zhí)行需要用到的請求參數(shù)、局部變量,如果不是靜態(tài)方法的話,還會存儲 this 指針來表示當(dāng)前對象實例。
局部變量的存儲基本單位為 變量槽(Variable Slot)。單個 Slot 可以存儲 boolean,byte,char,short,int,float,reference 或者 returnAddress。兩個 Slot 可以存儲 long 和 double。虛擬機(jī)規(guī)范沒有對 Slot 的物理內(nèi)存大小做出明確規(guī)定,可以隨著處理器、操作系統(tǒng)和虛擬機(jī)的不同而變化。但因為 int、float 等都可以用 32 位的物理內(nèi)存存放,所以一個 Slot 的物理內(nèi)存必須大于 32 位。
局部變量表采用 索引 進(jìn)行尋址。第一個局部變量的索引為 0。在實例方法中,始終使用局部變量 0 用來表示當(dāng)前對象實例,在 Java 中就是 this 指針。所以實例方法的局部變量的索引總是從 1 開始。
long 和 double 比較特殊,需要使用兩個連續(xù)的 Slot 存儲。這樣會占用兩個索引,取值小的那個。比如一個 double 存入局部變量表,它的索引值是 n,其實占用了 n 和 n+1 兩個索引,而 n+1索引是無法加載的。下一個局部變量的索引為 n+2。虛擬機(jī)規(guī)范并沒有要求 n 一定是偶數(shù),所以在在局部變量表中 long 和 double 并不一定是要 64 位對齊的。不同 JVM 的實現(xiàn),可以選擇合適的方式實現(xiàn)兩個局部變量存儲 long 和 double。
這里做個實驗,創(chuàng)建一個空方法,請求參數(shù)包含所有基礎(chǔ)數(shù)據(jù)類型和一個 String 引用類型,方法內(nèi)有一個 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ù)據(jù)存儲到方法區(qū),然后方法被調(diào)用的時候載入到新創(chuàng)建的棧幀中。
可以看到編譯期已經(jīng)確認(rèn)了表中每個局部變量的索引和大小。局部變量表的大小已經(jīng)寫入到 Code 屬性: locals=13 。
這 13 個基本單位是如何計算出來的?我們上面的案例,所有方法參數(shù)一共需要的基本單位數(shù) 1 + 1 + 1 + 1 + 1 + 2 + 1 + 2 + 1 = 11 ,一個局部變量 str 占用 1 個 Slot,有 12 個基本單位了。還有一個 Slot 呢?
這個是實例方法,加入了 this 指針用來表示當(dāng)前對象實例的引用,在 Slot 0 中:
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Loblee/demo/jvm/stack/SimpleObject;
this 指針占用 1 個 Slot,所以局部變量表總體大小為 13 個 Slot。
因為 this 指針是通過參數(shù)默認(rèn)傳遞給方法的,應(yīng)該歸到方法參數(shù)中,所以實際該方法有 10 個參數(shù),也寫入到了 code 屬性:args_size=10。
從反編譯的局部變量表還可以看到索引的設(shè)計,show 中參數(shù) f 為 long 類型,索引到 Slot 6,因為占用兩個 Slot,下一個變量 h 索引到 Slot 8。
JVM 對局部變量表進(jìn)行了優(yōu)化,變量槽 Slot 是可以復(fù)用的。
如果是靜態(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ù)棧
每一個棧幀都有一個后進(jìn)先出(LIFO)的操作數(shù)棧。操作數(shù)棧應(yīng)用于字節(jié)碼執(zhí)行引擎中,JVM 描述字節(jié)碼執(zhí)行引擎是基于 “?!?的,指的就是操作數(shù)棧。
操作數(shù)棧的每個條目可以保存 JVM 任何類型的值,long 和 double 占據(jù)深度的兩個單位,其他類型占據(jù)一個單位。操作數(shù)棧的最大深度由編譯期通過方法要執(zhí)行的字節(jié)碼計算出來,并記錄在 Code 屬性中。
棧幀剛創(chuàng)建時,操作數(shù)棧為空。JVM 提供了一系列字節(jié)碼指令,將數(shù)據(jù)從局部變量表加載到操作數(shù)棧中。還有一些指令,從操作數(shù)棧中讀取操作數(shù),進(jìn)行處理,然后把結(jié)果入棧。操作數(shù)棧還可以用來準(zhǔn)備參數(shù)傳遞給方法,或者接收方法返回結(jié)果。比如,指令 iadd 用來對兩個 int 值進(jìn)行相加。之前的指令已經(jīng)將兩個 int 值壓入到操作數(shù)棧中了,iadd 將兩個 int 值出棧,相加后將和入棧。
操作數(shù)棧中的數(shù)據(jù),必須用合適的類型的字節(jié)碼指令進(jìn)行操作。比如入棧兩個 int 值,不能當(dāng)做 long 處理。入棧 float 不能使用 iadd 指令進(jìn)行相加。有少量的 JVM 指令不關(guān)心值的類型,這些指令無法修改值。在類加載流程中,類文件的校驗階段,會強(qiáng)制實施。
設(shè)計了一個 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 存儲計算結(jié)果到 Slot 3。實際為存儲 c 到局部變量表。
4: iload_1 加載 Slot 1。實際為從局部變量表加載 a。
5: iload_2 加載 Slot 2。實際為從局部變量表加載 b。
6: isub 執(zhí)行減法。實際為 a - b。
7: istore 4 存儲計算結(jié)果到 Slot 4。實際為存儲 d 到局部變量表。
9: iload_3 加載 Slot 3。實際為從局部變量表加載 c。
10: iload 4 加載 Slot 4。實際為從局部變量表加載 d。
12: iadd 執(zhí)行加法。實際為 c + d。
13: istore 5 存儲計算結(jié)果到 Slot 5。實際為存儲 e 到局部變量表。
15: iload 5 加載 Slot 5 的數(shù)據(jù)。實際為從局部變量表加載 e。
17: ireturn 返回計算結(jié)果
我們傳入 a = 1, b = 2 進(jìn)行計算 calculate(1, 2),第一個加法操作操作數(shù)棧的變化如下:

這里的代碼是可以優(yōu)化的,因為局部變量 e 沒有做其他計算,可以直接返回。如果直接返回結(jié)果會有什么效果?代碼如下:
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)鏈接會將這些符號引用轉(zhuǎn)換成直接引用,比如在內(nèi)存中的具體偏移地址。
如果對應(yīng)的類還沒有被加載,會觸發(fā)該類的加載流程。
符號引用記錄在類常量池中,是一個由字面量組成的字符串,和具體地址無關(guān)。比如所有對象的類構(gòu)造方法的符號引用為 java/lang/Object."<init>":()V 。編譯并不知道運行時的地址,所以用符號引用代替。
動態(tài)鏈接又稱動態(tài)綁定。除了該方式,還有種發(fā)生在類文件加載過程中,這個這個階段就把符號引用轉(zhuǎn)換為直接引用,這樣的方式為饑餓方式或者靜態(tài)綁定。
靜態(tài)綁定和動態(tài)綁定都可以歸為是類加載機(jī)制中的 解析(Resolution) 的一部分。

可以看出類加載機(jī)制中的環(huán)節(jié)是有可能交叉進(jìn)行的。比如解析可能發(fā)生在準(zhǔn)備階段后,靜態(tài)綁定。也可能延遲到初始化后,在棧幀創(chuàng)建后進(jìn)行動態(tài)綁定。
綁定只發(fā)生一次,綁定后不再更改。
3.2.4. 方法正常結(jié)束
方法調(diào)用結(jié)束,沒有發(fā)生異常。這里指直接返回結(jié)果或者是顯式調(diào)用 throw 拋出異常。
被調(diào)用方法的結(jié)果需要傳遞給調(diào)用者方法。被調(diào)用的方法會執(zhí)行和方法返回相關(guān)的指令,這些指令和返回值的類型對應(yīng)。
當(dāng)前棧會被復(fù)原為調(diào)用者方法的執(zhí)行狀態(tài),包括局部變量表和操作數(shù)棧的數(shù)據(jù),程序計數(shù)器會跳過剛剛調(diào)用方法的指令指向下一條。被調(diào)用方法的返回值被加入到操作數(shù)棧中,程序繼續(xù)運行。
3.2.5. 方法異常結(jié)束
方法內(nèi)部發(fā)生了異常,而且沒有被捕獲,方法會被終止,并且沒有返回值給調(diào)用者。
4. 堆
堆由 JVM 所有的線程共享,一般情況下是 JVM 內(nèi)存區(qū)域中最大的一塊。按照 JVM 虛擬機(jī)規(guī)范,堆是一個用來存儲類對象實例或者數(shù)組的運行時數(shù)據(jù)區(qū)。
在 HopSpot 上,類對象實例不一定就是放在堆中,應(yīng)用了 JIT(Just-In-Time) 技術(shù),進(jìn)行逃逸分析(Escape Analysis)和標(biāo)量替換(Scalar Replacement)。符合條件的對象實例會在棧上分配。
JVM 啟動的時候堆就會創(chuàng)建。堆內(nèi)對象實例不會顯式釋放,由自動內(nèi)存管理系統(tǒng),也就是垃圾收集器進(jìn)行回收,是垃圾收集器主要管理區(qū)域。JVM 規(guī)范沒有說明垃圾收集器應(yīng)該是怎樣的,具體由實現(xiàn)由 JVM 廠商來提供。
比如 HotSpot 虛擬機(jī)中,垃圾回收器采用分代回收算法,會將堆進(jìn)行進(jìn)一步細(xì)分,分為新生代和老生代。新生代還可細(xì)分為 Eden 、From Survivor 和 To Survivor。這實際上是為了能夠更好地服務(wù)于垃圾回收。HotSpot 在 JDK 1.7 中堆還有一個永久代,其實是 JVM 規(guī)范中方法區(qū)的實現(xiàn),在 JDK1.8 移除。
HotSpot 的 JDK 1.7 堆圖示:

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

新生代和老年代的內(nèi)存分配流程:
- 優(yōu)先 Eden 分配,Eden 空間不足會觸發(fā) Minor GC。
- Minor GC 后,Eden + S0 還存活的對象移動到 S1 中,清空 S0。
- S1 放不下,存活次數(shù)達(dá)到要求的對象移動到老年代。
- 大對象直接分配到老年代。
- 老年代內(nèi)存不足會發(fā)生 Major GC
- 進(jìn)行垃圾回收后,Eden 仍然沒有足夠的空間,拋出
OutOfMemory異常。
Java 虛擬機(jī)規(guī)范對堆大小有這樣的描述:
- 可以是固定大小,也可以動態(tài)的擴(kuò)展和收縮。
- 堆的內(nèi)存不一定要連續(xù)。(邏輯上連續(xù))
- 可以配置本地方法棧初始大小,如果可動態(tài)擴(kuò)展和收縮,可配置最大值和最小值。
主流虛擬機(jī)都是采用可動態(tài)擴(kuò)展和收縮的方式實現(xiàn)的。堆內(nèi)存物理上可以不連續(xù),但是邏輯上需要連續(xù)。
HotPot 虛擬機(jī)的堆內(nèi)存配置:
-Xms,初始大小,默認(rèn)物理內(nèi)存的 1/64。
-Xmx,最大內(nèi)存,默認(rèn)物理內(nèi)存的 1/4。
-Xmn,新生代大小,因為持久代的大小一般默認(rèn)為 64M,在整個堆固定的情況下,增大新生代會相應(yīng)地減少老年代的大小。官方推薦
-XX:NewSize,新生代最小空間大小。
-XX:MaxNewSize,新生代最大空間大小。
-XX:NewRatio,新生代和老年代的比例,新生代和老年代的默認(rèn)比例為 1:2。
-XX:SurvivorRatio,Eden 和 Survivor 的比例,默認(rèn)為 Eden:S0:S1 = 8:1:1,即 survivor = 1/10 新生代大小。
HotSpot 采用的就是動態(tài)擴(kuò)展和收縮的方式,根據(jù)堆的空閑情況,當(dāng)空閑大于 70%,會減少至 -Xms;空閑小于 40%,會增大到 -Xmx。所以服務(wù)器如果配置 -Xms = -Xmx,可以避免堆自動擴(kuò)展。
堆會發(fā)生的異常:
- 如果程序請求的堆內(nèi)存大于 JVM 內(nèi)存管理系統(tǒng)能提供的最大值,會拋出
OutOfMemoryError異常。
5. 方法區(qū)
方法區(qū)由 JVM 所有線程共享。方法區(qū)類似一個用來存儲編譯后的代碼的區(qū)域。主要用來存儲加載的類信息,運行時常量池,類和方法的數(shù)據(jù),即時編譯后的代碼等。
JVM 啟動的時候方法區(qū)就會創(chuàng)建。
根據(jù) JVM 虛擬機(jī)規(guī)范,方法區(qū)邏輯上是堆的一部分,實現(xiàn)上可以選擇不進(jìn)行垃圾回收,并且沒有要求方法區(qū)的位置等。所以在方法區(qū)的具體實現(xiàn)各個虛擬機(jī)又不同的方式。雖然 JVM 虛擬機(jī)規(guī)范把方法區(qū)邏輯上劃給了堆,為了和實際堆進(jìn)行了區(qū)分,方法區(qū)還叫做 “非堆”。
Java 虛擬機(jī)規(guī)范對方法區(qū)大小的描述:
- 可以是固定大小,也可以動態(tài)的擴(kuò)展和收縮。
- 方法區(qū)的內(nèi)存不一定要連續(xù)。
- 用戶或者開發(fā)者能夠配置方法區(qū)初始大小,如果方法區(qū)可以動態(tài)擴(kuò)展或收縮,需要提供方法區(qū)的最大值和最小值。
HotSpot 在 JDK1.7 中方法區(qū)內(nèi)存大小配置:
-XX:PermSize,最小可分配空間,初始分配空間。
-XX:MaxPermSize,最大可分配空間,默認(rèn)大小為 64M(64 位 JVM 默認(rèn)為 85M)
在 JDK1.8 使用了元空間后,方法區(qū)的大小配置:
- -XX:MetaspaceSize,初始空間大小。
- -XX:MaxMetaspaceSize,最大空間大小,默認(rèn)是沒有限制的。
方法區(qū)可能發(fā)生的異常:
- 如果方法區(qū)請求的內(nèi)存無法被滿足,拋出
OutOfMemoryError異常。
5.1. 去永久代過程
HotSpot 虛擬機(jī)在 JDK1.7 采用永久代,在堆中分配內(nèi)存。在 JDK1.8 后使用元空間,使用本地內(nèi)存。
從 JDK1.7 開始 “去永久代”,JDK 1.7 將靜態(tài)變量、字符串常量池移動到堆內(nèi)存中,JDK1.8 去掉永久代,將類信息、即時編譯后的代碼等移動到了元空間。

之所以要進(jìn)行去永久代,主要還是該方案存在很多問題,留下很多 bug。主要有:
- 字符串存在永久代,容易發(fā)生內(nèi)存溢出。
- 類信息比較難確定大小,永久代的大小難以指定,太小永久代容易 OOM,太大老年代容易 OOM。
- 永久代 GC 回收復(fù)雜,效率低。
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 文件中的常量池如下。

運行時,在進(jìn)行類加載時,類常量池會被載入到 JVM 方法區(qū)。
JVM 虛擬機(jī)規(guī)范沒有約束運行時常量池只能放編譯期的常量,虛擬機(jī)的實現(xiàn)可以自行支持。比如 HotSpot 虛擬機(jī), Java 調(diào)用 String.intern() 方法,可以在運行期把常量加入池中。
在 HotSpot JDK 1.7 之后,對常量池進(jìn)行了優(yōu)化:字符串常量池被放在了 JVM 堆中,運行時常量池的字面量也存在 JVM 堆中,而符號引用被移動到了本地內(nèi)存。
以下的異常可能會發(fā)生:
- 當(dāng)創(chuàng)建一個 class 或者 interface 時,如果運行時常量池構(gòu)造需要的內(nèi)存超過 JVM 所能提供的,拋出
OutOfMemoryError異常。
7. 本地方法棧
JVM 的實現(xiàn)可能需要使用 "C 棧" 去支持本地方法調(diào)用。有可能使用 C 之類的語言,實現(xiàn) JVM 指令的解釋器,也會使用到本地方法棧。本地方法棧和 Java 虛擬機(jī)棧類似,只是這里提供的是本地方法服務(wù)。虛擬機(jī)規(guī)范沒有明確指出本地方法棧使用什么語言、數(shù)據(jù)結(jié)構(gòu)等,不同廠商的虛擬機(jī)又不同的實現(xiàn)。比如 HotSpot 虛擬機(jī)把本地方法棧和 Java 虛擬機(jī)棧合并了。
本地方法棧的生命周期線程對應(yīng),線程創(chuàng)建的時候創(chuàng)建。如果 JVM 不需要調(diào)用本地方法,可以不需要本地方法棧。
JVM 規(guī)范對本地方法棧大小的描述
- 可以使用固定大小,或者動態(tài)擴(kuò)展和收縮。如果是固定大小,當(dāng)棧被創(chuàng)建的時候能夠獨立選擇。
- 可以配置本地方法棧初始大小,如果可動態(tài)擴(kuò)展和收縮,可配置最大值和最小值。
以下異??赡馨l(fā)生:
- 如果線程請求的棧深度大于系統(tǒng)規(guī)定的,報
StackOverflowError。 - 如果本地方法??梢詣討B(tài)擴(kuò)展,沒有足夠的內(nèi)存擴(kuò)展。或者創(chuàng)建新的線程沒有足夠的內(nèi)存創(chuàng)建本地方法棧,拋出
OutOfMemoryError異常。
8. 參考資料
- Java Language and Virtual Machine Specifications
- 深入理解 Java 虛擬機(jī)(周志明)