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

文章作為《深入理解Java虛擬機(jī)》讀書(shū)筆記,講的可能就沒(méi)書(shū)本詳細(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ù)類(lèi)型,對(duì)象引用。其中64位長(zhǎng)度的long和double類(lèi)型的數(shù)據(jù)會(huì)占用2個(gè)局部變量空間,其余的數(shù)據(jù)類(lèi)型只占用一個(gè)。如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,則拋出StackOverflowError異常。如果動(dòng)態(tài)擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存,則拋出OutOfMemoryError異常。

  • 本地方法棧:與虛擬機(jī)棧功能類(lèi)似,只不過(guò)虛擬機(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ī)加載類(lèi)信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。(線程共享)

  • 運(yùn)行時(shí)常量池:運(yùn)行時(shí)常量池是方法區(qū)的一部分,Class文件中除了有類(lèi)的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池,用于存放編譯期生成的各種字面量和符號(hào)引用。運(yùn)行期間可以將新的常量放入常量池中,用得比較多的就是String類(lèi)的intern()方法,當(dāng)一個(gè)String實(shí)例調(diào)用intern時(shí),Java查找常量池中是否有相同的Unicode的字符串常量,若有,則返回其引用;若沒(méi)有,則在常量池中增加一個(gè)Unicode等于該實(shí)例字符串并返回它的引用。

虛擬機(jī)對(duì)象

  • 對(duì)象的創(chuàng)建:虛擬機(jī)遇到一條new指令時(shí),首先將去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類(lèi)的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類(lèi)是否已被加載,解析和初始化。如果沒(méi)有則先執(zhí)行相應(yīng)的類(lèi)加載過(guò)程。
    在堆中為對(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分代年齡等
      ②另一部分是類(lèi)型指針,即對(duì)象指向它的類(lèi)元數(shù)據(jù)的指針。虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例。

    • 實(shí)例數(shù)據(jù):是對(duì)象真正存儲(chǔ)的有效信息,也是在程序代碼中所定義的各種類(lèi)型的字段內(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ù)部分沒(méi)有對(duì)齊時(shí),就需要通過(guò)對(duì)齊填充來(lái)補(bǔ)全。

內(nèi)存回收GC

前面內(nèi)存模型講到5個(gè)數(shù)據(jù)區(qū)域,其中程序計(jì)數(shù)器,虛擬機(jī)棧,本地方法棧3個(gè)區(qū)域隨線程而生,隨線程而滅,棧中的棧幀隨著方法的進(jìn)入和退出而有條不紊地執(zhí)行著出棧和入棧操作。在這幾個(gè)區(qū)域就不需要過(guò)多考慮內(nèi)存回收問(wèn)題,因?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)引用的問(wèn)題,主流Java虛擬機(jī)沒(méi)有選用引用計(jì)數(shù)法來(lái)管理內(nèi)存。

  • 可達(dá)性分析算法

這個(gè)算法的基本思路就是通過(guò)一系列的稱為“GC Roots”的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開(kāi)始向下搜索,搜索所走過(guò)的路徑稱為引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連(用圖論的話來(lái)說(shuō),就是從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語(yǔ)言中,可作為GC Roots的對(duì)象包括下面幾種:
①虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象。
②方法區(qū)中類(lèi)靜態(tài)屬性引用的對(duì)象。
③方法區(qū)中常量引用的對(duì)象。
④本地方法棧中JNI(即一般說(shuō)的Native方法)引用的對(duì)象。

即使在可達(dá)性分析算法中不可達(dá)的對(duì)象,也并非是“非死不可”的,這時(shí)候它們暫時(shí)處于“緩刑”階段,要真正宣告一個(gè)對(duì)象死亡,至少要經(jīng)歷兩次標(biāo)記過(guò)程:如果對(duì)象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)沒(méi)有與GC Roots相連接的引用鏈,那它將會(huì)被第一次標(biāo)記并且進(jìn)行一次篩選,篩選的條件是此對(duì)象是否有必要執(zhí)行finalize()方法。當(dāng)對(duì)象沒(méi)有覆蓋finalize()方法,或者finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過(guò),虛擬機(jī)將這兩種情況都視為“沒(méi)有必要執(zhí)行”。

程序中可以通過(guò)覆蓋finalize()來(lái)一場(chǎng)"驚心動(dòng)魄"的自我拯救過(guò)程,但是,這只有一次機(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ǔ)的垃圾回收算法,之所以說(shuō)它是最基礎(chǔ)的是因?yàn)樗钊菀讓?shí)現(xiàn),思想也是最簡(jiǎn)單的。標(biāo)記-清除算法分為兩個(gè)階段:標(biāo)記階段和清除階段。標(biāo)記階段的任務(wù)是標(biāo)記出所有需要被回收的對(duì)象,清除階段就是回收被標(biāo)記的對(duì)象所占用的空間。具體過(guò)程如下圖所示:

標(biāo)記-清除

從圖中可以很容易看出標(biāo)記-清除算法實(shí)現(xiàn)起來(lái)比較容易,但是有一個(gè)比較嚴(yán)重的問(wèn)題就是容易產(chǎn)生內(nèi)存碎片,碎片太多可能會(huì)導(dǎo)致后續(xù)過(guò)程中需要為大對(duì)象分配空間時(shí)無(wú)法找到足夠的空間而提前觸發(fā)新的一次垃圾收集動(dòng)作。

2.Copying(復(fù)制)算法

為了解決Mark-Sweep算法的缺陷,Copying算法就被提了出來(lái)。它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用的內(nèi)存空間一次清理掉,這樣一來(lái)就不容易出現(xiàn)內(nèi)存碎片的問(wèn)題。具體過(guò)程如下圖所示:

復(fù)制

這種算法雖然實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效且不容易產(chǎn)生內(nèi)存碎片,但是卻對(duì)內(nèi)存空間的使用做出了高昂的代價(jià),因?yàn)槟軌蚴褂玫膬?nèi)存縮減到原來(lá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)存。具體過(guò)程如下圖所示:

標(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ì)象,也就是說(shuō)需要復(fù)制的操作次數(shù)較少,但是實(shí)際中并不是按照1:1的比例來(lái)劃分新生代的空間的,一般來(lái)說(shuō)是將新生代劃分為一塊較大的Eden空間和兩塊較小的Survivor空間(一般為8:1:1),每次使用Eden空間和其中的一塊Survivor空間,當(dāng)進(jìn)行回收時(shí),將Eden和Survivor中還存活的對(duì)象復(fù)制到另一塊Survivor空間中,然后清理掉Eden和剛才使用過(guò)的Survivor空間。

而由于老年代的特點(diǎn)是每次回收都只回收少量對(duì)象,一般使用的是Mark-Compact標(biāo)記-整理算法。


參考資料:《深入理解Java虛擬機(jī)》2章和3章內(nèi)容

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容