《深入理解Java虛擬機(jī)》筆記之JAVA內(nèi)存模式與垃圾回收

文章作為《深入理解Java虛擬機(jī)》讀書筆記黍析,講的可能就沒書本詳細(xì)宪卿。

Java內(nèi)存模型

Java虛擬機(jī)在執(zhí)行程序時(shí)把它管理的內(nèi)存分為若干數(shù)據(jù)區(qū)域畜埋,這些數(shù)據(jù)區(qū)域分布情況如下圖所示:

運(yùn)行時(shí)數(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ì)象。

可達(dá)性分析算法

在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)記-清除

從圖中可以很容易看出標(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)存碎片的問題。具體過程如下圖所示:

復(fù)制

這種算法雖然實(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)存。具體過程如下圖所示:

標(biāo)記-整理

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)容

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末逻淌,一起剝皮案震驚了整個(gè)濱河市么伯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌卡儒,老刑警劉巖田柔,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異骨望,居然都是意外死亡硬爆,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門擎鸠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缀磕,“玉大人,你說我怎么就攤上這事⊥嗖希” “怎么了糟把?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長牲剃。 經(jīng)常有香客問我遣疯,道長,這世上最難降的妖魔是什么颠黎? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任另锋,我火速辦了婚禮,結(jié)果婚禮上狭归,老公的妹妹穿的比我還像新娘夭坪。我一直安慰自己,他們只是感情好过椎,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布室梅。 她就那樣靜靜地躺著,像睡著了一般疚宇。 火紅的嫁衣襯著肌膚如雪亡鼠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天敷待,我揣著相機(jī)與錄音间涵,去河邊找鬼。 笑死榜揖,一個(gè)胖子當(dāng)著我的面吹牛勾哩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播举哟,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼思劳,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了妨猩?” 一聲冷哼從身側(cè)響起潜叛,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎壶硅,沒想到半個(gè)月后威兜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡庐椒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年牡属,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片扼睬。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡逮栅,死狀恐怖悴势,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情措伐,我是刑警寧澤特纤,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站侥加,受9級(jí)特大地震影響捧存,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜担败,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一昔穴、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧提前,春花似錦吗货、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至拓哺,卻和暖如春勇垛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背士鸥。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國打工闲孤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人烤礁。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓讼积,卻偏偏與公主長得像,于是被迫代替她去往敵國和親鸽凶。 傳聞我的和親對(duì)象是個(gè)殘疾皇子币砂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容