文章作為《深入理解Java虛擬機(jī)》讀書筆記黍析,講的可能就沒書本詳細(xì)宪卿。
Java內(nèi)存模型
Java虛擬機(jī)在執(zhí)行程序時(shí)把它管理的內(nèi)存分為若干數(shù)據(jù)區(qū)域畜埋,這些數(shù)據(jù)區(qū)域分布情況如下圖所示:
程序計(jì)數(shù)器:一塊較小內(nèi)存區(qū)域缆瓣,指向當(dāng)前所執(zhí)行的字節(jié)碼。如果線程正在執(zhí)行一個(gè)Java方法材泄,這個(gè)計(jì)數(shù)器記錄正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址沮焕,如果執(zhí)行的是Native方法,這個(gè)計(jì)算器值為空拉宗。(線程私有)
Java虛擬機(jī)棧:線程私有的峦树,其生命周期和線程一致,描述的是Java方法執(zhí)行的內(nèi)存模型旦事。每個(gè)方法執(zhí)行時(shí)都會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量表魁巩、操作數(shù)棧、動(dòng)態(tài)鏈接姐浮、方法出口等信息谷遂。(線程私有)
局部變量表:存放了編譯期可知的各種基本數(shù)據(jù)類型,對(duì)象引用卖鲤。其中64位長度的long和double類型的數(shù)據(jù)會(huì)占用2個(gè)局部變量空間肾扰,其余的數(shù)據(jù)類型只占用一個(gè)畴嘶。如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,則拋出StackOverflowError異常集晚。如果動(dòng)態(tài)擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存掠廓,則拋出OutOfMemoryError異常。
本地方法棧:與虛擬機(jī)棧功能類似甩恼,只不過虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則為使用到的Native方法服務(wù)沉颂。
Java堆:是虛擬機(jī)管理內(nèi)存中最大的一塊条摸,被所有線程共享,該區(qū)域用于存放對(duì)象實(shí)例铸屉,幾乎所有的對(duì)象都在該區(qū)域分配钉蒲。Java堆是內(nèi)存回收的主要區(qū)域,從內(nèi)存回收角度看彻坛,由于現(xiàn)在的收集器大都采用分代收集算法顷啼,所以Java堆還
可以細(xì)分為:新生代和老年代,再細(xì)分一點(diǎn)的話可以分為Eden空間昌屉、From Survivor空間钙蒙、To Survivor空間等。根據(jù)Java虛擬機(jī)規(guī)范規(guī)定间驮,Java堆可以處于物理上不連續(xù)的空間躬厌,只要邏輯上是連續(xù)的就行。(線程共享)方法區(qū):與Java一樣竞帽,是各個(gè)線程所共享的扛施,用于存儲(chǔ)已被虛擬機(jī)加載類信息、常量屹篓、靜態(tài)變量疙渣、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。(線程共享)
運(yùn)行時(shí)常量池:運(yùn)行時(shí)常量池是方法區(qū)的一部分堆巧,Class文件中除了有類的版本妄荔、字段、方法恳邀、接口等描述信息外懦冰,還有一項(xiàng)信息是常量池,用于存放編譯期生成的各種字面量和符號(hào)引用谣沸。運(yùn)行期間可以將新的常量放入常量池中刷钢,用得比較多的就是String類的intern()方法,當(dāng)一個(gè)String實(shí)例調(diào)用intern時(shí)乳附,Java查找常量池中是否有相同的Unicode的字符串常量内地,若有伴澄,則返回其引用;若沒有阱缓,則在常量池中增加一個(gè)Unicode等于該實(shí)例字符串并返回它的引用非凌。
虛擬機(jī)對(duì)象
對(duì)象的創(chuàng)建:虛擬機(jī)遇到一條new指令時(shí),首先將去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類的符號(hào)引用荆针,并且檢查這個(gè)符號(hào)引用代表的類是否已被加載敞嗡,解析和初始化。如果沒有則先執(zhí)行相應(yīng)的類加載過程航背。
在堆中為對(duì)象分配內(nèi)存有:“指針碰撞”(堆連續(xù)) 和 “空閑列表”(堆不連續(xù))喉悴。由Java堆是否規(guī)整決定。-
對(duì)象的內(nèi)存布局:對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為3塊區(qū)域玖媚。對(duì)象頭(Header)箕肃,實(shí)例數(shù)據(jù)(Instance Data)和對(duì)齊填充(Padding)
對(duì)象頭:分為兩部分
①第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼今魔,GC分代年齡等
②另一部分是類型指針勺像,即對(duì)象指向它的類元數(shù)據(jù)的指針。虛擬機(jī)通過這個(gè)指針來確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例错森。實(shí)例數(shù)據(jù):是對(duì)象真正存儲(chǔ)的有效信息吟宦,也是在程序代碼中所定義的各種類型的字段內(nèi)容
對(duì)齊填充:起著占位符的作用。由于HotSpot VM的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍涩维,也就是對(duì)象的大小必須是8字節(jié)的整數(shù)倍督函。而對(duì)象對(duì)不正好是8字節(jié)的倍數(shù)。因此激挪,當(dāng)對(duì)象實(shí)例數(shù)據(jù)部分沒有對(duì)齊時(shí)辰狡,就需要通過對(duì)齊填充來補(bǔ)全。
內(nèi)存回收GC
前面內(nèi)存模型講到5個(gè)數(shù)據(jù)區(qū)域垄分,其中程序計(jì)數(shù)器宛篇,虛擬機(jī)棧馁害,本地方法棧3個(gè)區(qū)域隨線程而生专肪,隨線程而滅峻汉,棧中的棧幀隨著方法的進(jìn)入和退出而有條不紊地執(zhí)行著出棧和入棧操作半哟。在這幾個(gè)區(qū)域就不需要過多考慮內(nèi)存回收問題,因?yàn)榉椒ńY(jié)束或者線程結(jié)束時(shí)家淤,內(nèi)存自然就跟著回收了佳晶。而Java堆和方法區(qū)則不一樣抄瓦,這部分內(nèi)存的分配和回收都是動(dòng)態(tài)的坐求。
垃圾對(duì)象如何確定蚕泽?
Java堆中存放著幾所所有的對(duì)象實(shí)例,垃圾收集器在對(duì)堆進(jìn)行回收前,首先需要確定哪些對(duì)象還"活著"须妻,哪些已經(jīng)"死亡"仔蝌,也就是不會(huì)被任何途徑使用的對(duì)象。 對(duì)象存活判定方法:
- 引用計(jì)數(shù)算法
引用計(jì)數(shù)法實(shí)現(xiàn)簡(jiǎn)單荒吏,效率較高敛惊,在大部分情況下是一個(gè)不錯(cuò)的算法。其原理是:給對(duì)象添加一個(gè)引用計(jì)數(shù)器绰更,每當(dāng)有一個(gè)地方引用該對(duì)象時(shí)瞧挤,計(jì)數(shù)器加1,當(dāng)引用失效時(shí)儡湾,計(jì)數(shù)器減1皿伺,當(dāng)計(jì)數(shù)器值為0時(shí)表示該對(duì)象不再被使用。需要注意的是:引用計(jì)數(shù)法很難解決對(duì)象之間相互循環(huán)引用的問題盒粮,主流Java虛擬機(jī)沒有選用引用計(jì)數(shù)法來管理內(nèi)存。
- 可達(dá)性分析算法
這個(gè)算法的基本思路就是通過一系列的稱為“GC Roots”的對(duì)象作為起始點(diǎn)奠滑,從這些節(jié)點(diǎn)開始向下搜索丹皱,搜索所走過的路徑稱為引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象到GC Roots沒有任何引用鏈相連(用圖論的話來說宋税,就是從GC Roots到這個(gè)對(duì)象不可達(dá))時(shí)摊崭,則證明此對(duì)象是不可用的。如圖所示杰赛,對(duì)象object 5呢簸、object 6、object 7雖然互相有關(guān)聯(lián)乏屯,但是它們到GC Roots是不可達(dá)的根时,所以它們將會(huì)被判定為是可回收的對(duì)象。
在Java語言中辰晕,可作為GC Roots的對(duì)象包括下面幾種:
①虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象蛤迎。
②方法區(qū)中類靜態(tài)屬性引用的對(duì)象。
③方法區(qū)中常量引用的對(duì)象含友。
④本地方法棧中JNI(即一般說的Native方法)引用的對(duì)象替裆。
即使在可達(dá)性分析算法中不可達(dá)的對(duì)象,也并非是“非死不可”的窘问,這時(shí)候它們暫時(shí)處于“緩刑”階段辆童,要真正宣告一個(gè)對(duì)象死亡,至少要經(jīng)歷兩次標(biāo)記過程:如果對(duì)象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈惠赫,那它將會(huì)被第一次標(biāo)記并且進(jìn)行一次篩選把鉴,篩選的條件是此對(duì)象是否有必要執(zhí)行finalize()方法。當(dāng)對(duì)象沒有覆蓋finalize()方法儿咱,或者finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過纸镊,虛擬機(jī)將這兩種情況都視為“沒有必要執(zhí)行”倍阐。
程序中可以通過覆蓋finalize()來一場(chǎng)"驚心動(dòng)魄"的自我拯救過程,但是逗威,這只有一次機(jī)會(huì)峰搪。
/**
* 此代碼演示了兩點(diǎn):
* 1.對(duì)象可以在被GC時(shí)自我拯救。
* 2.這種自救的機(jī)會(huì)只有一次凯旭,因?yàn)橐粋€(gè)對(duì)象的finalize()方法最多只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次
* @author zzm
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("yes, i am still alive :)");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize mehtod executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
//對(duì)象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
//因?yàn)閒inalize方法優(yōu)先級(jí)很低概耻,所以暫停0.5秒以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
//下面這段代碼與上面的完全相同,但是這次自救卻失敗了
SAVE_HOOK = null;
System.gc();
//因?yàn)閒inalize方法優(yōu)先級(jí)很低罐呼,所以暫停0.5秒以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
}
}
運(yùn)行結(jié)果為:
finalize mehtod executed!
yes, i am still alive :)
no, i am dead :(
任何一個(gè)對(duì)象的finalize()方法都只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次鞠柄,如果對(duì)象面臨下一次回收,它的finalize()方法不會(huì)被再次執(zhí)行嫉柴。因此第二段代碼的自救行動(dòng)失敗了厌杜。
前面的算法講的是如何判定垃圾對(duì)象,判定完后則該如何處理進(jìn)行垃圾回收计螺?
典型的垃圾回收算法
1.Mark-Sweep(標(biāo)記-清除)算法
這是最基礎(chǔ)的垃圾回收算法夯尽,之所以說它是最基礎(chǔ)的是因?yàn)樗钊菀讓?shí)現(xiàn),思想也是最簡(jiǎn)單的登馒。標(biāo)記-清除算法分為兩個(gè)階段:標(biāo)記階段和清除階段匙握。標(biāo)記階段的任務(wù)是標(biāo)記出所有需要被回收的對(duì)象,清除階段就是回收被標(biāo)記的對(duì)象所占用的空間陈轿。具體過程如下圖所示:
從圖中可以很容易看出標(biāo)記-清除算法實(shí)現(xiàn)起來比較容易圈纺,但是有一個(gè)比較嚴(yán)重的問題就是容易產(chǎn)生內(nèi)存碎片,碎片太多可能會(huì)導(dǎo)致后續(xù)過程中需要為大對(duì)象分配空間時(shí)無法找到足夠的空間而提前觸發(fā)新的一次垃圾收集動(dòng)作麦射。
2.Copying(復(fù)制)算法
為了解決Mark-Sweep算法的缺陷蛾娶,Copying算法就被提了出來。它將可用內(nèi)存按容量劃分為大小相等的兩塊潜秋,每次只使用其中的一塊茫叭。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面半等,然后再把已使用的內(nèi)存空間一次清理掉揍愁,這樣一來就不容易出現(xiàn)內(nèi)存碎片的問題。具體過程如下圖所示:
這種算法雖然實(shí)現(xiàn)簡(jiǎn)單杀饵,運(yùn)行高效且不容易產(chǎn)生內(nèi)存碎片莽囤,但是卻對(duì)內(nèi)存空間的使用做出了高昂的代價(jià),因?yàn)槟軌蚴褂玫膬?nèi)存縮減到原來的一半切距。
很顯然朽缎,Copying算法的效率跟存活對(duì)象的數(shù)目多少有很大的關(guān)系,如果存活對(duì)象很多,那么Copying算法的效率將會(huì)大大降低话肖。
3.Mark-Compact(標(biāo)記-整理)算法
為了解決Copying算法的缺陷北秽,充分利用內(nèi)存空間,提出了Mark-Compact算法最筒。該算法標(biāo)記階段和Mark-Sweep一樣贺氓,但是在完成標(biāo)記之后,它不是直接清理可回收對(duì)象床蜘,而是將存活對(duì)象都向一端移動(dòng)辙培,然后清理掉端邊界以外的內(nèi)存。具體過程如下圖所示:
4.Generational Collection(分代收集)算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法邢锯。它的核心思想是根據(jù)對(duì)象存活的生命周期將內(nèi)存劃分為若干個(gè)不同的區(qū)域扬蕊。一般情況下將堆區(qū)劃分為老年代(Tenured Generation)和新生代(Young Generation),老年代的特點(diǎn)是每次垃圾收集時(shí)只有少量對(duì)象需要被回收丹擎,而新生代的特點(diǎn)是每次垃圾回收時(shí)都有大量的對(duì)象需要被回收尾抑,那么就可以根據(jù)不同代的特點(diǎn)采取最適合的收集算法。
目前大部分垃圾收集器對(duì)于新生代都采取Copying算法蒂培,因?yàn)樾律忻看卫厥斩家厥沾蟛糠謱?duì)象再愈,也就是說需要復(fù)制的操作次數(shù)較少,但是實(shí)際中并不是按照1:1的比例來劃分新生代的空間的毁渗,一般來說是將新生代劃分為一塊較大的Eden空間和兩塊較小的Survivor空間(一般為8:1:1),每次使用Eden空間和其中的一塊Survivor空間单刁,當(dāng)進(jìn)行回收時(shí)灸异,將Eden和Survivor中還存活的對(duì)象復(fù)制到另一塊Survivor空間中,然后清理掉Eden和剛才使用過的Survivor空間羔飞。
而由于老年代的特點(diǎn)是每次回收都只回收少量對(duì)象肺樟,一般使用的是Mark-Compact標(biāo)記-整理算法。
參考資料:《深入理解Java虛擬機(jī)》2章和3章內(nèi)容