??內(nèi)存是非常重要的系統(tǒng)資源,是硬盤和CPU的中間倉(cāng)庫(kù)及橋梁,承載著操作系統(tǒng)和應(yīng)用程序的實(shí)時(shí)運(yùn)行敬察。JVM內(nèi)存布局規(guī)定了Java在運(yùn)行過程中內(nèi)存申請(qǐng)、分配尔当、管理的策略莲祸,保證了JVM的高效穩(wěn)定運(yùn)行。不同的JVM對(duì)于內(nèi)存的劃分方式和管理機(jī)制存在著部分差異椭迎。結(jié)合JVM虛擬機(jī)規(guī)范锐帜,來學(xué)習(xí)一 下經(jīng)典的JVM內(nèi)存布局。
??話不多說侠碧,先來一圖(截圖來至阿里的<碼出高效:java開發(fā)手冊(cè)>)抹估。上圖就是jdk8之后的jvm經(jīng)典布局缠黍,接下來主要詳細(xì)分析各個(gè)分區(qū)的功能及作用弄兜。
Stacks(虛擬機(jī)棧)
??棧( Stack )是一個(gè)先進(jìn)后出的數(shù)據(jù)結(jié)構(gòu),就像子彈的彈夾,最后壓入的子彈先發(fā)射瓷式,壓在底部的子彈最后發(fā)射替饿,撞針只能訪問位于頂部的那一顆子彈。相對(duì)于基于寄存器的運(yùn)行環(huán)境來說贸典,JVM 是基于棧結(jié)構(gòu)的運(yùn)行環(huán)境视卢。棧結(jié)構(gòu)移植性更好,可控性更強(qiáng)廊驼。JVM中的虛擬機(jī)棧是描述Java方法執(zhí)行的內(nèi)存區(qū)域据过,它是線程私有的(每一條Java虛擬機(jī)線程都有自己私有的Java虛擬機(jī)棧,這個(gè)棧與線程同時(shí)創(chuàng)建)妒挎。
??棧中的元素用于支持虛擬機(jī)進(jìn)行方法調(diào)用绳锅,每個(gè)方法從開始調(diào)用到執(zhí)行完成的過程,就是棧幀從入棧到出棧的過程酝掩。在活動(dòng)線程中鳞芙,只有位于棧頂?shù)膸攀怯行У模Q為當(dāng)前棧幀。正在執(zhí)行的方法稱為當(dāng)前方法原朝,棧幀是方法運(yùn)行的基本結(jié)構(gòu)驯嘱。在執(zhí)行引擎運(yùn)行時(shí),所有指令都只能針對(duì)當(dāng)前棧幀進(jìn)行操作喳坠。StackOverflowError表示請(qǐng)求的棧溢出鞠评,導(dǎo)致內(nèi)存耗盡,通常出現(xiàn)在遞歸方法中丙笋。JVM能夠橫掃千軍, 虛擬機(jī)棧就是它的心腹大將谢澈,當(dāng)前方法的棧幀,都是正在戰(zhàn)斗的戰(zhàn)場(chǎng)御板,其中的操作棧是參與戰(zhàn)斗的士兵锥忿。
- 局部表量表
??每個(gè)棧幀(見上圖)內(nèi)部都包含一組稱為局部變量表的變量列表。棧幀中局部變量表的長(zhǎng)度由編譯期決定怠肋,并且存儲(chǔ)于類或接口的二進(jìn)制表示之中敬鬓,即通過方法的code屬性保存及提供給棧幀使用。
??一個(gè)局部量可以保存一個(gè)類型為boolean笙各、byte钉答、char、short杈抢、int数尿、float、reference或returnAddress的數(shù)據(jù)惶楼。兩個(gè)局部變量可以保存一個(gè)類型為long或double的數(shù)據(jù)右蹦。
??局部變量使用索引來進(jìn)行定位訪問。首個(gè)局部變量的索引值為0歼捐。局部變量的索引值是個(gè)整數(shù)何陆,它大于等于0,且小于局部變量表的長(zhǎng)度豹储。
??long和double類型的數(shù)據(jù)占用兩個(gè)連續(xù)的局部變量贷盲,這兩種類型的數(shù)據(jù)值采用兩個(gè)局部變量中較小的索引值來定位。例如剥扣,將一個(gè)double類型的值存儲(chǔ)在索引值為n的局部變量中巩剖,實(shí)際上的意思是索引值為n和n+1的兩個(gè)局部變量都用來存儲(chǔ)這個(gè)值。然而钠怯,索引值為n+1的局部變量是無法直接讀取的佳魔,但是可能會(huì)被寫人。不過呻疹,如果進(jìn)行了這種操作,那將會(huì)導(dǎo)致局部變量n的內(nèi)容失效吃引。前面提及的局部變量索引值n并不要求一定是偶數(shù)筹陵,Java虛擬機(jī)也不要求double和long類型數(shù)據(jù)采用64位對(duì)齊的方式連續(xù)地存儲(chǔ)在局部變量表中。虛擬機(jī)實(shí)現(xiàn)者可以自由地選擇適當(dāng)?shù)姆绞侥鞒撸ㄟ^兩個(gè)局部變量來存儲(chǔ)一個(gè)double或long類型的值朦佩。
??Java虛擬機(jī)使用局部變量表來完成方法調(diào)用時(shí)的參數(shù)傳遞。當(dāng)調(diào)用類方法時(shí)庐氮,它的參數(shù)將會(huì)依次傳遞到局部變量表中從0開始的連續(xù)位置上语稠。當(dāng)調(diào)用實(shí)例方法時(shí),第0個(gè)局部變量一定用來存儲(chǔ)該實(shí)例方法所在對(duì)象的引用(即Java語言中的this關(guān)鍵字)弄砍。后續(xù)的其他參數(shù)將會(huì)傳遞至局部變量表中從1開始的連續(xù)位置上仙畦。
- 操作數(shù)棧
??每個(gè)棧幀(見上圖)內(nèi)部都包含一個(gè)稱為操作數(shù)棧的后進(jìn)先出( Last-In-First-Out,LIFO)棧。棧幀中操作數(shù)棧的最大深度由編譯期決定音婶,并且通過方法的code屬性保存及提供給棧幀使用慨畸。
??棧幀在剛剛創(chuàng)建時(shí),操作數(shù)棧是空的衣式。Java虛擬機(jī)提供一些字節(jié)碼指令來從局部變量表或者對(duì)象實(shí)例的字段中復(fù)制常量或變量值到操作數(shù)棧中(如:iload寸士,aload,getfield等)碴卧,也提供了一些指令用于從操作數(shù)棧取走數(shù)據(jù)(將一個(gè)數(shù)值從操作數(shù)棧存儲(chǔ)到局部變量表弱卡。如:istore,astore等)住册、操作數(shù)據(jù)(iadd婶博,isub等)以及把操作結(jié)果重新人棧。在調(diào)用方法時(shí)荧飞,操作數(shù)棧也用來準(zhǔn)備調(diào)用方法的參數(shù)以及接收方法返回結(jié)果凡人。
??例如,iadd字節(jié)碼指令的作用是將兩個(gè)int類型的數(shù)值相加,它要求在執(zhí)行之前操作數(shù)棧的棧頂已經(jīng)存在兩個(gè)由前面的其他指令所放人的int類型數(shù)值。在執(zhí)行iadd指令時(shí)垢箕,兩個(gè)int類型數(shù)值從操作棧中出棧划栓,相加求和兑巾,然后將求和結(jié)果重新入棧条获。在操作數(shù)棧中,一項(xiàng)運(yùn)算常由多個(gè)子運(yùn)算( subcomputation) 嵌套進(jìn)行蒋歌,一個(gè)子運(yùn)算過程的結(jié)果可以被其他外圍運(yùn)算所使用帅掘。
操作數(shù)棧與局部變量表之間傳遞參數(shù)示例:
static class VmStacks {
/******************方法下方字節(jié)碼是通過javap -v class文件獲得*********************/
/**
* 該方法主要演示jvm對(duì)方法調(diào)用過程
*/
public int directInvoke() {
return addI(1, 2);
}
public int directInvoke();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1 // 最大棧深度為3,局部變量表個(gè)數(shù)為1
0: aload_0 // 將this從局部變量表壓入操作數(shù)棧堂油,因?yàn)樵摲椒閷?shí)例方法修档,故局部變量表slot為0的就是實(shí)例本身(this)
1: iconst_1 // 將常量值1壓入操作數(shù)棧
2: iconst_2 // 將常量值2壓入操作數(shù)棧
3: invokevirtual #2 // Method addI:(II)I 調(diào)用addI(int x, int y)方法
6: ireturn 返回int類型的值
LineNumberTable:
line 86: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lmayfly/core/util/CodeByteTest$VmStacks;
1. 因?yàn)樵摲椒閷?duì)象實(shí)例方法,故方法調(diào)用的第一步是將當(dāng)前實(shí)例的自身引用this壓人操作數(shù)棧中
(如果是類方法府框,即static方法則沒有該步驟)吱窝。傳遞給方法的int類型參數(shù)值1和2隨后人棧。
2. 當(dāng)調(diào)用addI方法(即invokevirtual #2 指令)時(shí),Java虛擬機(jī)會(huì)創(chuàng)建一個(gè)新的棧幀院峡,
傳遞給addI方法的參數(shù)值會(huì)成為新棧幀中對(duì)應(yīng)局部變量的初始值兴使。即由directInvoke
方法推人操作數(shù)棧的this和兩個(gè)傳遞給addI方法的參數(shù)1與2,會(huì)作為addI方法棧幀的
第0照激、1发魄、2個(gè)局部變量。
3. 當(dāng)addI方法執(zhí)行結(jié)束俩垃、方法返回時(shí)励幼,int類型的返回值被壓入方法調(diào)用者的棧幀的操作數(shù)棧,
即directInvoke方法的操作數(shù)棧中口柳。而這個(gè)返回值又會(huì)立即返回給directInvoke的調(diào)用者苹粟。
directInvoke方法的返回過程由directInvoke方法中的ireturn指令實(shí)現(xiàn)。由addI方法所返回的
int類型值會(huì)壓入當(dāng)前操作數(shù)棧的棧頂跃闹,而ireturn指令則會(huì)把當(dāng)前操作數(shù)棧的棧頂值(此處就是addI的返回值)
壓人directInvoke方法的調(diào)用者的操作數(shù)棧六水。然后跳轉(zhuǎn)至調(diào)用directInvoke的那個(gè)方法的下一條指令繼續(xù)執(zhí)行,
并將調(diào)用者的棧幀重新設(shè)為當(dāng)前棧幀辣卒。Java虛擬機(jī)對(duì)不同數(shù)據(jù)類型(包括聲明為void,即沒有返回值的方法)
的返回值提供了不同的方法返回指令掷贾,各種不同返回值類型的方法都使用這一組返回指令來返回。
/**********************************************************************************/
public int addI(int x, int y) {
int z = y++; // 純粹為了演示 i++與++i之間字節(jié)碼的區(qū)別荣茫,無其他意義
return ++x + y;
}
public int addI(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=3
0: iload_2
1: iinc 2, 1
4: istore_3
5: iinc 1, 1
8: iload_1
9: iload_2
10: iadd
11: ireturn
LineNumberTable:
line 71: 0
line 72: 5
LocalVariableTable:
Start Length Slot Name Signature
0 12 0 this Lmayfly/core/util/CodeByteTest$VmStacks;
0 12 1 x I
0 12 2 y I
5 7 3 z I
1. 方法調(diào)用者(如上個(gè)方法directInvoke)將操作棧上的變量出棧并傳遞給該方法棧幀的局部變量表中的的
0想帅,1,2位置的slot上啡莉。
2. 在0~4索引(指令操作碼在數(shù)組中的下標(biāo)港准,該數(shù)組以字節(jié)形式來存儲(chǔ)當(dāng)前方法的java虛擬機(jī)代碼,
也可以認(rèn)為是相對(duì)于方法起始處的字節(jié)偏移量)上的三條字節(jié)碼表示的是int z = y++ 該行代碼;
iload_ 2 從局部變量表的第2號(hào)抽屜里取出一個(gè)數(shù)咧欣,壓入棧頂浅缸,下一步直接在抽屜(局部變量表中的slot)
里實(shí)現(xiàn)+1的操作,而這個(gè)操作對(duì)棧頂元素的值沒有影響魄咕。所以istore_ 3只是把棧頂元素賦值給z衩椒,即z == y而不是y+1后的值;
3. 索引5~8表示++x, iinc 1, 1先在第1號(hào)抽屜里執(zhí)行+1操作哮兰,然后通過iload_ 1 把第1號(hào)抽屜里的數(shù)壓入棧頂毛萌,
所以操作數(shù)棧中存入的是+1之后的值。
4. 接著將2號(hào)抽屜的數(shù)值(即y)壓入棧頂喝滞,隨后執(zhí)行iadd指令阁将,將棧頂?shù)膬蓚€(gè)元素彈出棧相加后,
并將相加后的結(jié)果重新壓入棧頂并return給調(diào)用者右遭。
/**********************************************************************************/
}
- 動(dòng)態(tài)鏈接
??每個(gè)棧幀內(nèi)部都包含一個(gè)指向當(dāng)前方法所在類型的運(yùn)行時(shí)常量池的引用做盅,以便對(duì)當(dāng)前方法的代碼實(shí)現(xiàn)動(dòng)態(tài)鏈接缤削。在class文件里面,一個(gè)方法若要調(diào)用其他方法吹榴,或者訪問成員變量僻他,則需要通過符號(hào)引用(symbolic reference) 來表示,動(dòng)態(tài)鏈接的作用就是將這些以符號(hào)引用所表示的方法轉(zhuǎn)換為對(duì)實(shí)際方法的直接引用腊尚。類加載的過程中將要解析尚未被解析的符號(hào)引用吨拗,并且將對(duì)變量的訪問轉(zhuǎn)化為變量在程度運(yùn)行時(shí),位于存儲(chǔ)結(jié)構(gòu)中的正確偏移量婿斥。由于對(duì)其他類中的方法和變量進(jìn)行了晚期綁定(latebinding),所以即便那些類發(fā)生變化劝篷,也不會(huì)影響調(diào)用它們的方法。
- 方法返回地址
??方法執(zhí)行時(shí)有兩種退出情況:第一民宿,正常退出娇妓,即正常執(zhí)行到任何方法的返回字節(jié)碼指令花嘶,如RETURN顽照、IRETURN、ARETURN等;第二臼氨,異常退出志群。無論何種退出情況着绷,都將返回至方法當(dāng)前被調(diào)用的位置。方法退出的過程相當(dāng)于彈出當(dāng)前棧幀锌云,退出可能有三種方式:
- 返回值壓入上層調(diào)用棧幀荠医。
- 異常信息拋給能夠處理的棧幀。
- PC計(jì)數(shù)器指向方法調(diào)用后的下一條指令桑涎。
PC程序計(jì)數(shù)器
??Java虛擬機(jī)可以支持多條線程同時(shí)執(zhí)行彬向,每一條Java虛擬機(jī)線程都有自的pc ( program counter) 寄存器。在任意時(shí)刻攻冷,一條Java 虛擬機(jī)線程只會(huì)執(zhí)行一個(gè)方法的代碼娃胆,這個(gè)正在被線程執(zhí)行的方法稱為該線程的當(dāng)前方法。如果這個(gè)方法不是native的等曼,那pc寄存器就保存Java虛擬機(jī)正在執(zhí)行的字節(jié)碼指令的地址,如果該方法是native的里烦,那pc寄存器的值是undefined。pc寄存器的容量至少應(yīng)當(dāng)能保存一個(gè)returnAddress類型的數(shù)據(jù)或者一個(gè)與平臺(tái)相關(guān)的本地指針的值涉兽。
Heap(堆區(qū))
??Heap是OOM故障最主要的發(fā)源地招驴,它存儲(chǔ)著幾乎所有的實(shí)例對(duì)象篙程,堆由垃圾收集器自動(dòng)回收枷畏,堆區(qū)由各子線程共享使用。通常情況下虱饿,它占用的空間是所有內(nèi)存區(qū)域中最大的拥诡,但如果無節(jié)制地創(chuàng)建大量對(duì)象触趴,也容易消耗完所有的空間。堆的內(nèi)存空間既可以固定大小渴肉,也可以在運(yùn)行時(shí)動(dòng)態(tài)地調(diào)整冗懦,通過如下參數(shù)設(shè)定初始值和最大值,比如-Xms256M -Xmx1024M仇祭, 其中-X表示它是JVM運(yùn)行參數(shù)披蕉,ms是memory start的簡(jiǎn)稱,mx是memory max的簡(jiǎn)稱乌奇,分別代表最小堆容量和最大堆容量没讲。但是在通常情況下,服務(wù)器在運(yùn)行過程中礁苗,堆空間不斷地?cái)U(kuò)容與回縮爬凑,勢(shì)必形成不必要的系統(tǒng)壓力,所以在線上生產(chǎn)環(huán)境中试伙,JVM的Xms和Xmx設(shè)置成一樣大小嘁信,避免在GC后調(diào)整堆大小時(shí)帶來的額外壓力。
??Java內(nèi)存運(yùn)行時(shí)區(qū)域的各個(gè)部分疏叨,其中程序計(jì)數(shù)器潘靖、虛擬機(jī)棧、本地方法棧三個(gè)區(qū)域隨線程而生蚤蔓,隨線程而滅;棧中的棧幀隨著方法的進(jìn)入和退出而有條不紊地執(zhí)行著出棧和入棧操作秘豹。每一個(gè)棧幀中分配多少內(nèi)存基本上是在類結(jié)構(gòu)確定下來時(shí)就已知的(盡管在運(yùn)行期會(huì)由JIT編譯器進(jìn)行一些優(yōu)化,但在基于概念模型的討論中昌粤,大體上可以認(rèn)為是編譯期可知的)既绕,因此這幾個(gè)區(qū)域的內(nèi)存分配和回收都具備確定性,在這幾個(gè)區(qū)域內(nèi)不需要過多考慮回收的問題涮坐,因?yàn)榉椒ńY(jié)束或線程結(jié)束時(shí)凄贩,內(nèi)存自然就跟隨著回收了。因此堆是垃圾收集的最主要內(nèi)存區(qū)域袱讹。
??如最開始那個(gè)布局圖疲扎,為何要將堆空間分為新生代、老年代捷雕,以及新生代又為何要?jiǎng)澐譃橐粋€(gè)Eden區(qū)和兩個(gè)Survivor(幸存者)區(qū)椒丧,結(jié)合GC講解可更好地理解為何要醬紫劃分。
?? GC(Garbage Collection 垃圾收集)
判斷對(duì)象已死
??堆中幾乎存放著Java世界中所有的對(duì)象實(shí)例救巷,垃圾收集器在對(duì)堆進(jìn)行回收前壶熏,第一件事情就是要確定這些對(duì)象有哪些還“存活”著,哪些已經(jīng)“死去”(即不可能再被任何途徑使用的對(duì)象)浦译。
- 引用計(jì)數(shù)算法
??給對(duì)象中添加一個(gè)引用計(jì)數(shù)器棒假,每當(dāng)有一個(gè)地方引用它時(shí)溯职,計(jì)數(shù)器值就加1 ;當(dāng)引用失效時(shí),計(jì)數(shù)器值就減1 ;任何時(shí)刻計(jì)數(shù)器都為0的對(duì)象就是不可能再被使用的帽哑。引用計(jì)數(shù)算法(Reference Counting)的實(shí)現(xiàn)簡(jiǎn)單谜酒,判定效率也很高,但是妻枕,Java語言中沒有選用引用計(jì)數(shù)算法來管理內(nèi)存僻族,其中最主要的原因是它很難解決對(duì)象之間的相互循環(huán)引用的問題。舉個(gè)簡(jiǎn)單的例代碼如下:
public class ReferenceCountingGC {
public Object instance;
public ReferenceCountingGC(String name){}
}
public static void testGC(){
ReferenceCountingGC a = new ReferenceCountingGC("objA");
ReferenceCountingGC b = new ReferenceCountingGC("objB");
a.instance = b;
b.instance = a;
a = null;
b = null;
}
??我們可以看到屡谐,最后這2個(gè)對(duì)象已經(jīng)不可能再被訪問了鹰贵,但由于他們相互引用著對(duì)方,導(dǎo)致它們的引用計(jì)數(shù)永遠(yuǎn)都不會(huì)為0康嘉,通過引用計(jì)數(shù)算法碉输,也就永遠(yuǎn)無法通知GC收集器回收它們。
- 根搜索算法
??在主流的商用程序語言中(Java和C#亭珍,甚至包括前面提到的古老的Lisp)敷钾,都是使用根搜索算法(GC Roots Tracing)判定對(duì)象是否存活的。這個(gè)算法的基本思路就是通過一系列的名為“GC Roots”的對(duì)象作為起始點(diǎn)肄梨,從這些節(jié)點(diǎn)開始向下搜索阻荒,搜索所走過的路徑稱為引用鏈(Reference Chain),當(dāng)一一個(gè)對(duì)象到GC Roots沒有任何引用鏈相連(用圖論的話來說就是從GCRoots到這個(gè)對(duì)象不可達(dá))時(shí)众羡,則證明此對(duì)象是不可用的侨赡。如下圖所示,對(duì)象object 5粱侣、object 6羊壹、object 7雖然互相有關(guān)聯(lián),但是它們到GCRoots是不可達(dá)的齐婴,所以它們將會(huì)被判定為是可回收的對(duì)象油猫。
??通過根搜索算法,成功解決了引用計(jì)數(shù)所無法解決的問題“循環(huán)依賴”柠偶,只要你無法與 GC Root 建立直接或間接的連接情妖,系統(tǒng)就會(huì)判定你為可回收對(duì)象。那這樣就引申出了另一個(gè)問題诱担,哪些屬于 GC Root毡证。在Java語言里,可作為GCRoots的對(duì)象包括下面幾種:
- 虛擬機(jī)棧(棧幀中的本地變量表)中的引用的對(duì)象蔫仙。
- 方法區(qū)中的類靜態(tài)屬性引用的對(duì)象料睛。
- 方法區(qū)中的常量引用的對(duì)象。
- 本地方法棧中JNI (即一般說的Native方法)的引用的對(duì)象。
垃圾收集算法
- 標(biāo)記-清除算法
??最基礎(chǔ)的收集算法是“標(biāo)記-清除”(Mark-Sweep) 算法秦效,如它的名字一樣雏蛮,算法分為"標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象涎嚼,在標(biāo)記完成后統(tǒng)一回收掉所有被標(biāo)記的對(duì)象阱州,之所以說它是最基礎(chǔ)的收集算法,是因?yàn)楹罄m(xù)的收集算法都是基于這種思路并對(duì)其缺點(diǎn)進(jìn)行改進(jìn)而得到的法梯。它的主要缺點(diǎn)有兩個(gè):一個(gè)是效率問題苔货,標(biāo)記和清除過程的效率都不高,另外一個(gè)是空間問題立哑,標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片夜惭,空間碎片太多可能會(huì)導(dǎo)致,當(dāng)程在以后的運(yùn)行過程中需要分配較大對(duì)象時(shí)無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集 動(dòng)作铛绰。標(biāo)記-清除算法的執(zhí)行過程如下圖所示诈茧。
- 復(fù)制算法
??為了解決效率問題,一種稱為“復(fù)制”(Copying)的收集算法出現(xiàn)了捂掰,它將可用內(nèi)存按容量劃分為大小相等的兩塊敢会,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了这嚣,就將還存活著的對(duì)象復(fù)制到另外一塊上面鸥昏,然后再把已使用過的內(nèi)存空間一次清理掉。這樣使得每次都是對(duì)其中的一塊進(jìn)行內(nèi)存回收姐帚,內(nèi)存分配時(shí)也就不用考慮內(nèi)存碎片等復(fù)雜情況吏垮,只要移動(dòng)堆頂指針,按順序分配內(nèi)存即可罐旗,實(shí)現(xiàn)簡(jiǎn)單膳汪,運(yùn)行高效。只是這種算法的代價(jià)是將內(nèi)存縮小為原來的一半九秀,未免太高了一點(diǎn)旅敷。復(fù)制算法的執(zhí)行過程如下圖所示。
??現(xiàn)在的商業(yè)虛擬機(jī)都采用這種收集算法來回收新生代颤霎,IBM的專門研究表明媳谁,新生代中的對(duì)象98%是朝生夕死的,所以并不需要按照1: 1的比例來劃分內(nèi)存空間友酱,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間晴音,每次使用Eden和其中的一塊Survivor。當(dāng)回收時(shí)缔杉,將Eden和Survivor中還存活著的對(duì)象一次性地拷貝到另外一塊Survivor空間上锤躁,最后清理掉Eden和剛才用過的Survivor的空間。HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8:1或详,也就是每次新生代中可用內(nèi)存空間為整個(gè)新生代容量的90% ( 80%+10%)系羞,只有10%的內(nèi)存是會(huì)被“浪費(fèi)”的郭计。當(dāng)然,98%的對(duì)象可回收只是一般場(chǎng)景下的數(shù)據(jù)椒振,我們沒有辦法保證每次回收都只有不多于10%的對(duì)象存活昭伸,當(dāng)Survivor空間不夠用時(shí),就需要依賴其他內(nèi)存(這里指老年代)澎迎。
- 標(biāo)記-整理算法
??復(fù)制收集算法在對(duì)象存活率較高時(shí)就要執(zhí)行較多的復(fù)制操作庐杨,效率將會(huì)變低。更關(guān)鍵的是夹供,如果不想浪費(fèi)50%的空間灵份,就需要有額外的空間進(jìn)行分配擔(dān)保,以應(yīng)對(duì)被使用的內(nèi)存中所有對(duì)象都100%存活的極端情況哮洽,所以在老年代一般不能直接選用這種算法填渠。根據(jù)老年代的特點(diǎn),有人提出了另外一種“標(biāo)記-整理”(Mark-Compact)算法鸟辅,標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣氛什,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向一端移動(dòng)剔桨,然后直接清理掉端邊界以外的內(nèi)存屉更,“標(biāo)記-整理”算法的示意圖如下圖所示。
- 分代收集算法
??當(dāng)前商業(yè)虛擬機(jī)的垃圾收集都采用“分代收集”(Generational Collection)算法洒缀,這種算法并沒有什么新的思想瑰谜,只是根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊。一般般是把Java堆分為新生代和老年代树绩,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴ㄈ浴T谛律校看卫占瘯r(shí)都發(fā)現(xiàn)有大批對(duì)象死去饺饭,只有少量存活渤早,那就選用復(fù)制算法,只需要付出少量存活對(duì)象的復(fù)制成本就可以完成收集瘫俊。而老年代中因?yàn)閷?duì)象存活率高鹊杖、沒有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用“標(biāo)記一清理”或者“標(biāo)記一整理” 算法來進(jìn)行回收扛芽。
堆內(nèi)存及GC總結(jié)
??堆分成兩大塊:新生代和老年代骂蓖。對(duì)象產(chǎn)生之初在新生代,步入暮年時(shí)進(jìn)入老年代川尖,但是老年代也接納在新生代無法容納的超大對(duì)象登下。新生代= 1個(gè)Eden區(qū)+ 2個(gè)Survivor區(qū)。絕大部分對(duì)象在Eden區(qū)生成,當(dāng)Eden區(qū)裝填滿的時(shí)候被芳,會(huì)觸發(fā)YoungGarbage Collection缰贝, 即YGC(也叫MinorGc)。垃圾回收的時(shí)候畔濒,在Eden區(qū)實(shí)現(xiàn)清除策略剩晴,沒有被引用的對(duì)象則直接回收。依然存活的對(duì)象會(huì)被移送到Survivor區(qū)篓冲,這個(gè)區(qū)真是名副其實(shí)的存在李破。Survivor 區(qū)分為S0和S1兩塊內(nèi)存空間宠哄,送到哪塊空間呢?每次YGC的時(shí)候壹将,它們將存活的對(duì)象復(fù)制到未使用的那塊空間,然后將當(dāng)前正在使用的空間完全清除毛嫉,交換兩塊空間的使用狀態(tài)诽俯。如果YGC要移送的對(duì)象大于Survivor區(qū)容量的上限,則直接移交給老年代承粤。假如一些沒有進(jìn)取心的對(duì)象以為可以一直在新生代的Survivor區(qū)交換來交換去暴区,那就錯(cuò)了。每個(gè)對(duì)象都有一個(gè)計(jì)數(shù)器辛臊,每次YGC都會(huì)加1仙粱。
-XX:MaxTenuringThreshold參數(shù)能配置計(jì)數(shù)器的值到達(dá)某個(gè)閾值的時(shí)候,對(duì)象從新生代晉升至老年代彻舰。如果該參數(shù)配置為1,那么從新生代的Eden區(qū)直接移至老年代伐割。默認(rèn)值是15,可以在Survivor區(qū)交換14次之后刃唤,晉升至老年代隔心。對(duì)象分配及晉升流程圖如下圖所示。
Metaspace (元空間)
??早在JDK8版本中尚胞,元空間的前身Perm區(qū)(永久代)已經(jīng)被淘汰硬霍。在JDK7及之前的版本中,只有Hotspot才有Perm區(qū)笼裳,譯為永久代唯卖,它在啟動(dòng)時(shí)固定大小,很難進(jìn)行調(diào)優(yōu)躬柬,并且FGC時(shí)會(huì)移動(dòng)類元信息拜轨。在某些場(chǎng)景下,如果動(dòng)態(tài)加載類過多楔脯,容易產(chǎn)生Perm區(qū)的0OM撩轰。比如某個(gè)實(shí)際Web工程中,因?yàn)楣δ茳c(diǎn)比較多,在運(yùn)行過程中堪嫂,要不斷動(dòng)態(tài)加載很多的類偎箫,經(jīng)常出現(xiàn)致命錯(cuò)誤:" java.lang.OutOfMemoryError: PermGenspace"為了解決該問題,需要設(shè)定運(yùn)行參數(shù)-XX:MaxPermSize= 1280m皆串,如果部署到新機(jī)器上淹办,往往會(huì)因?yàn)镴VM參數(shù)沒有修改導(dǎo)致故障再現(xiàn)。不熟悉此應(yīng)用的人排查問題時(shí)往往苦不堪言恶复,除此之外怜森,永久代在垃圾回收過程中還存在諸多問題。所以谤牡,JDK8使用元空間替換永久代副硅。在JDK8及以上版本中,設(shè)定MaxPermSize參數(shù)翅萤,JVM在啟動(dòng)時(shí)并不會(huì)報(bào)錯(cuò)恐疲,但是會(huì)提示: Java HotSpot 64Bit Server VM warning:ignoring option MaxPermSize- =2560m; support was removed in 8.0。區(qū)別于永久代套么,元空間在本地內(nèi)存中分配培己。在JDK8里,Perm 區(qū)中的所有內(nèi)容中字符串常量移至堆內(nèi)存胚泌,其他內(nèi)容包括類元信息省咨、字段、靜態(tài)屬性玷室、方法零蓉、常量等都移動(dòng)至元空間內(nèi),比如下圖中的Object類元信息阵苇、靜態(tài)屬性System.out壁公、整型常量1000000等。圖中顯示在常量池中的String绅项,其實(shí)際對(duì)象是被保存在堆內(nèi)存中的紊册。
注:以上大部分內(nèi)容(除代碼示例)摘抄整理自《深入理解java虛擬機(jī)》、《Java虛擬機(jī)規(guī)范 JavaSE 8版本》快耿、《碼出高效:Java開發(fā)手冊(cè)》