1. 概述
JVM 把內(nèi)存進(jìn)行了劃分闪湾,不同的內(nèi)存區(qū)域有不同的功能。有的內(nèi)存區(qū)域是線程私有的绩卤,比如 Java 虛擬機(jī)棧途样、本地方法棧和程序計數(shù)器,每一條線程都有自己獨(dú)立的空間濒憋。有的內(nèi)存區(qū)域是線程共享的何暇,比如方法區(qū)和堆。
所以不同內(nèi)存區(qū)域的功能凛驮、作用域和生命周期是不同的裆站。本文做一個詳細(xì)的分析。
根據(jù) JVM 虛擬機(jī)規(guī)范黔夭,內(nèi)存結(jié)構(gòu)如下:
JVM 虛擬機(jī)規(guī)范屬于概念模型宏胯,具體的實(shí)現(xiàn)各個廠商的會有所差異。比如方法區(qū)的設(shè)計本姥,hotspot 在 1.7 之前使用永久代肩袍,1.7 后使用元空間。
本文主要分析 HotSpot 虛擬機(jī)的實(shí)現(xiàn)婚惫。
2. 程序計數(shù)器
JVM 支持多線程了牛,采用時間片輪轉(zhuǎn)的方式實(shí)現(xiàn)多線程并發(fā)。一個內(nèi)核每一刻只能有一個線程執(zhí)行辰妙,多線程下需要線程上下文切換鹰祸。為了確保切換過程中,不同的線程指令和數(shù)據(jù)不會發(fā)生混亂密浑,需要單獨(dú)開辟內(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ī)棧的初始大小席噩。
- 如果棧空間可以動態(tài)擴(kuò)展或者收縮贤壁,可以配置棧的最大值和最小值悼枢。
HotSpot 虛擬機(jī)棧的配置:
- -Xss,設(shè)置虛擬機(jī)棧大小脾拆,JDK1.5 之后默認(rèn)為 1M馒索。棧深度受到這個堆棧大小的約束。在固定物理內(nèi)存下減小 Java 虛擬機(jī)棧大小可以產(chǎn)生更多線程名船,但是一個進(jìn)程的線程數(shù)量有約束绰上,不能無限增加。
Java 虛擬機(jī)椙眨可能會發(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 屬性中祈秕,運(yùn)行期間不會發(fā)生改變渺贤。所以在編譯器每個棧幀的需要大小就可以確定了。棧深度由運(yùn)行期決定踢步。
具體的棧深度受虛擬機(jī)棧大小和棧幀大小的影響癣亚,要看使用了多少棧幀丑掺,棧幀大小多少获印。每個棧幀的大小不一定一樣,取決于各棧幀對應(yīng)方法的局部變量表和操作數(shù)棧大小等街州。
假設(shè)我們的虛擬機(jī)棧大小固定兼丰,棧幀數(shù)量達(dá)到最大值,也就是達(dá)到最大深度唆缴,深度大小和棧幀大小的示意圖如下:
上面的示意圖可以看出鳍征,在 Java 虛擬機(jī)棧大小固定的情況下,如果每個棧幀都很大面徽,最大可用深度就會變小艳丛。
上面只是一個示意圖,實(shí)際上虛擬機(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ī)棧分配氏义。每個棧幀有自己獨(dú)立的局部變量表、操作數(shù)棧图云、指向運(yùn)行時常量池的引用惯悠。
棧幀的內(nèi)容可擴(kuò)展,比如加入調(diào)試信息竣况。
在編譯期就可以根據(jù)棧幀對應(yīng)的方法代碼克婶,確定局部變量表和操作數(shù)棧的大小。棧幀的具體大小依賴于 JVM 虛擬機(jī)的實(shí)現(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)前對象實(shí)例。
局部變量的存儲基本單位為 變量槽(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侣颂。在實(shí)例方法中,始終使用局部變量 0 用來表示當(dāng)前對象實(shí)例枪孩,在 Java 中就是 this 指針憔晒。所以實(shí)例方法的局部變量的索引總是從 1 開始。
long 和 double 比較特殊蔑舞,需要使用兩個連續(xù)的 Slot 存儲拒担。這樣會占用兩個索引,取值小的那個攻询。比如一個 double 存入局部變量表从撼,它的索引值是 n,其實(shí)占用了 n 和 n+1 兩個索引钧栖,而 n+1索引是無法加載的低零。下一個局部變量的索引為 n+2。虛擬機(jī)規(guī)范并沒有要求 n 一定是偶數(shù)拯杠,所以在在局部變量表中 long 和 double 并不一定是要 64 位對齊的掏婶。不同 JVM 的實(shí)現(xiàn),可以選擇合適的方式實(shí)現(xiàn)兩個局部變量存儲 long 和 double潭陪。
這里做個實(shí)驗雄妥,創(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 呢痊焊?
這個是實(shí)例方法盏袄,加入了 this 指針用來表示當(dāng)前對象實(shí)例的引用,在 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ù)中垄惧,所以實(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í)施。
設(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 表示索引)箫津。實(shí)際為從局部變量表加載 a。
1: iload_2 加載 Slot 2宰啦。實(shí)際為從局部變量表加載 a苏遥。
2: iadd 執(zhí)行加法。實(shí)際為 a + b赡模。
3: istore_3 存儲計算結(jié)果到 Slot 3田炭。實(shí)際為存儲 c 到局部變量表。
4: iload_1 加載 Slot 1漓柑。實(shí)際為從局部變量表加載 a教硫。
5: iload_2 加載 Slot 2。實(shí)際為從局部變量表加載 b辆布。
6: isub 執(zhí)行減法瞬矩。實(shí)際為 a - b。
7: istore 4 存儲計算結(jié)果到 Slot 4锋玲。實(shí)際為存儲 d 到局部變量表景用。
9: iload_3 加載 Slot 3。實(shí)際為從局部變量表加載 c惭蹂。
10: iload 4 加載 Slot 4伞插。實(shí)際為從局部變量表加載 d割粮。
12: iadd 執(zhí)行加法。實(shí)際為 c + d媚污。
13: istore 5 存儲計算結(jié)果到 Slot 5穆刻。實(shí)際為存儲 e 到局部變量表。
15: iload 5 加載 Slot 5 的數(shù)據(jù)杠步。實(shí)際為從局部變量表加載 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)鏈接
每一個幀都包含了一個指向運(yùn)行時常量池的引用弃鸦,用來實(shí)現(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
烹困。編譯并不知道運(yùn)行時的地址,所以用符號引用代替乾吻。
動態(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ù)運(yùn)行。
3.2.5. 方法異常結(jié)束
方法內(nèi)部發(fā)生了異常,而且沒有被捕獲,方法會被終止毁靶,并且沒有返回值給調(diào)用者。
4. 堆
堆由 JVM 所有的線程共享拆祈,一般情況下是 JVM 內(nèi)存區(qū)域中最大的一塊。按照 JVM 虛擬機(jī)規(guī)范,堆是一個用來存儲類對象實(shí)例或者數(shù)組的運(yùn)行時數(shù)據(jù)區(qū)。
在 HopSpot 上宫补,類對象實(shí)例不一定就是放在堆中,應(yīng)用了 JIT(Just-In-Time) 技術(shù)曾我,進(jìn)行逃逸分析(Escape Analysis)和標(biāo)量替換(Scalar Replacement)。符合條件的對象實(shí)例會在棧上分配健民。
JVM 啟動的時候堆就會創(chuàng)建抒巢。堆內(nèi)對象實(shí)例不會顯式釋放,由自動內(nèi)存管理系統(tǒng)秉犹,也就是垃圾收集器進(jìn)行回收蛉谜,是垃圾收集器主要管理區(qū)域。JVM 規(guī)范沒有說明垃圾收集器應(yīng)該是怎樣的崇堵,具體由實(shí)現(xiàn)由 JVM 廠商來提供型诚。
比如 HotSpot 虛擬機(jī)中,垃圾回收器采用分代回收算法鸳劳,會將堆進(jìn)行進(jìn)一步細(xì)分狰贯,分為新生代和老生代。新生代還可細(xì)分為 Eden 、From Survivor 和 To Survivor涵紊。這實(shí)際上是為了能夠更好地服務(wù)于垃圾回收傍妒。HotSpot 在 JDK 1.7 中堆還有一個永久代,其實(shí)是 JVM 規(guī)范中方法區(qū)的實(shí)現(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ò)展和收縮的方式實(shí)現(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ū)域扼雏。主要用來存儲加載的類信息坚嗜,運(yùn)行時常量池,類和方法的數(shù)據(jù)诗充,即時編譯后的代碼等苍蔬。
JVM 啟動的時候方法區(qū)就會創(chuàng)建。
根據(jù) JVM 虛擬機(jī)規(guī)范蝴蜓,方法區(qū)邏輯上是堆的一部分银室,實(shí)現(xiàn)上可以選擇不進(jìn)行垃圾回收,并且沒有要求方法區(qū)的位置等励翼。所以在方法區(qū)的具體實(shí)現(xiàn)各個虛擬機(jī)又不同的方式。雖然 JVM 虛擬機(jī)規(guī)范把方法區(qū)邏輯上劃給了堆辜荠,為了和實(shí)際堆進(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. 運(yùn)行時常量池
運(yùn)行時常量池是 class 文件的常量池在運(yùn)行時的表示住闯。主要有字面量和符號引用瓜浸。
要理解運(yùn)行時常量池澳淑,我們得先了解 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 文件中的常量池如下杠巡。
運(yùn)行時,在進(jìn)行類加載時雇寇,類常量池會被載入到 JVM 方法區(qū)氢拥。
JVM 虛擬機(jī)規(guī)范沒有約束運(yùn)行時常量池只能放編譯期的常量,虛擬機(jī)的實(shí)現(xiàn)可以自行支持谢床。比如 HotSpot 虛擬機(jī)兄一, Java 調(diào)用 String.intern()
方法,可以在運(yùn)行期把常量加入池中识腿。
在 HotSpot JDK 1.7 之后出革,對常量池進(jìn)行了優(yōu)化:字符串常量池被放在了 JVM 堆中,運(yùn)行時常量池的字面量也存在 JVM 堆中渡讼,而符號引用被移動到了本地內(nèi)存骂束。
以下的異常可能會發(fā)生:
- 當(dāng)創(chuàng)建一個 class 或者 interface 時成箫,如果運(yùn)行時常量池構(gòu)造需要的內(nèi)存超過 JVM 所能提供的展箱,拋出
OutOfMemoryError
異常。
7. 本地方法棧
JVM 的實(shí)現(xiàn)可能需要使用 "C 棧" 去支持本地方法調(diào)用蹬昌。有可能使用 C 之類的語言混驰,實(shí)現(xiàn) JVM 指令的解釋器,也會使用到本地方法棧皂贩。本地方法棧和 Java 虛擬機(jī)棧類似栖榨,只是這里提供的是本地方法服務(wù)。虛擬機(jī)規(guī)范沒有明確指出本地方法棧使用什么語言明刷、數(shù)據(jù)結(jié)構(gòu)等婴栽,不同廠商的虛擬機(jī)又不同的實(shí)現(xiàn)。比如 HotSpot 虛擬機(jī)把本地方法棧和 Java 虛擬機(jī)棧合并了辈末。
本地方法棧的生命周期線程對應(yīng)愚争,線程創(chuàng)建的時候創(chuàng)建。如果 JVM 不需要調(diào)用本地方法挤聘,可以不需要本地方法棧轰枝。
JVM 規(guī)范對本地方法棧大小的描述
- 可以使用固定大小,或者動態(tài)擴(kuò)展和收縮组去。如果是固定大小狸膏,當(dāng)棧被創(chuàng)建的時候能夠獨(dú)立選擇。
- 可以配置本地方法棧初始大小添怔,如果可動態(tài)擴(kuò)展和收縮湾戳,可配置最大值和最小值。
以下異彻懔希可能發(fā)生:
- 如果線程請求的棧深度大于系統(tǒng)規(guī)定的砾脑,報
StackOverflowError
。 - 如果本地方法棸樱可以動態(tài)擴(kuò)展韧衣,沒有足夠的內(nèi)存擴(kuò)展」荷#或者創(chuàng)建新的線程沒有足夠的內(nèi)存創(chuàng)建本地方法棧畅铭,拋出
OutOfMemoryError
異常。
8. 參考資料
- Java Language and Virtual Machine Specifications
- 深入理解 Java 虛擬機(jī)(周志明)