深入理解JVM(4) : Java垃圾收集 (GC)

Java與C++之間有一堵由內(nèi)存動態(tài)分配和垃圾收集技術(shù)所圍成的“高墻”,墻外面的人想進去,墻里面的人卻想出來。

對于垃圾收集(Gabage Collection,GC), 我們需要考慮三件事情:

  • 哪些內(nèi)存需要回收?
  • 什么時候回收?
  • 如何回收?

一、GC的工作區(qū)域(哪些內(nèi)存需要回收?)

Java虛擬機的內(nèi)存區(qū)域中,程序計數(shù)器、虛擬機棧和本地方法棧三個區(qū)域是線程私有的,隨線程生而生,隨線程滅而滅;棧中的棧幀隨著方法的進入和退出而進行入棧和出棧操作,每個棧幀中分配多少內(nèi)存基本上是在類結(jié)構(gòu)確定下來時就已知的,因此這幾個區(qū)域的內(nèi)存分配和回收都具有確定性。在這幾個區(qū)域不需要過多考慮回收的問題,因為方法結(jié)束或線程結(jié)束時,內(nèi)存自然就跟隨著回收了。

垃圾回收重點關(guān)注的是堆和方法區(qū)部分的內(nèi)存。因為一個接口中的多個實現(xiàn)類需要的內(nèi)存可能不一樣,一個方法的多個分支需要的內(nèi)存也可能不一樣,我們只有在程序處于運行期間才能知道會創(chuàng)建哪些對象,這部分內(nèi)存的分配和回收都是動態(tài)的,所以垃圾回收器所關(guān)注的主要是這部分的內(nèi)存。


二、垃圾對象的判定(什么時候回收?)

Java堆中存放著幾乎所有的對象實例,垃圾收集器對堆中的對象進行回收前,要先確定這些對象是否還有用,哪些還活著。對象死去的時候才需要回收。

1. 判斷對象是否存活的算法:

  • 引用計數(shù)算法
    給對象添加一個引用計數(shù)器,每當(dāng)有一個地方引用它時,計數(shù)器值就加1,當(dāng)引用失效時,計數(shù)器值就減1,任何時刻計數(shù)器為0的對象就是不可能再被使用的。
    1)優(yōu)點:引用計數(shù)算法的實現(xiàn)簡單,判定效率也很高,在大部分情況下它都是一個不錯的選擇.
    2)缺點:Java虛擬機并沒有選擇這種算法來進行垃圾回收,主要原因是它很難解決對象之間的相互循環(huán)引用問題。

    public class ReferenceCountingGC {
    public Object instance = null;
    private static final int _1MB = 1024 * 1024;
    // 這個成員屬性的唯一意義就是占點內(nèi)存,以便在能在GC日志中看清楚是否有回收過
    private byte[] bigSize = new byte[2 * _1MB];
    public static void testGC() {
      ReferenceCountingGC objA = new ReferenceCountingGC();
      ReferenceCountingGC objB = new ReferenceCountingGC();
      objA.instance = objB;
      objB.instance = objA;
      objA = null;
      objB = null;
      // 假設(shè)在這行發(fā)生GC,objA和objB是否能被回收?
      System.gc();
    }
    }
    

    對象objA和objB都有字段instance,賦值令objA.instance = objB;以及objB.instance = objA;,除此之外,這兩個對象再無任何其他引用,實際上這兩個對象已經(jīng)不可能再被訪問,但是因為它們互相引用著對方,導(dǎo)致它們的引用計數(shù)值都不為0,引用計數(shù)算法無法通知GC收集器回收它們。

  • 可達性分析算法
    這種算法的基本思路是通過一系列名為“GC Roots”的對象作為起始點,從這些節(jié)點開始向下搜索,搜索所走過的路徑稱為引用鏈,當(dāng)一個對象到GC Roots沒有任何引用鏈相連時,就證明此對象是不可用的。
    Java語言是通過可達性分析算法來判斷對象是否存活的。


    在Java語言里,可作為GC Roots的對象包括下面幾種:
    虛擬機棧(棧幀中的本地變量表)中引用的對象。
    方法區(qū)中的類靜態(tài)屬性引用的對象。
    方法區(qū)中的常量引用的對象。
    本地方法棧中JNI(Native方法)的引用對象。

2. 正確理解引用:

無論是通過引用計數(shù)算法判斷對象的引用數(shù)量,還是通過可達性分析算法判斷對象的引用鏈?zhǔn)欠窨蛇_,判定對象是否存活都與“引用”有關(guān)。

在JDK 1.2以前,Java中的引用的定義很傳統(tǒng):如果reference類型的數(shù)據(jù)中存儲的數(shù)值代表的是另外一塊內(nèi)存的起始地址,就稱這塊內(nèi)存代表著一個引用。這種定義很純粹,但是太過狹隘,一個對象在這種定義下只有被引用或者沒有被引用兩種狀態(tài),對于如何描述一些“食之無味,棄之可惜”的對象就顯得無能為力。
我們希望能描述這樣一類對象:當(dāng)內(nèi)存空間還足夠時,則能保留在內(nèi)存之中;如果內(nèi)存空間在進行垃圾收集后還是非常緊張,則可以拋棄這些對象。很多系統(tǒng)的緩存功能都符合這樣的應(yīng)用場景。

在JDK 1.2之后,Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。

  • 強引用就是指在程序代碼之中普遍存在的,類似Object obj = new Object()這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。

  • 軟引用是用來描述一些還有用但并非必需的對象。對于軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會把這些對象列進回收范圍之中進行第二次回收。如果這次回收還沒有足夠的內(nèi)存,才會拋出內(nèi)存溢出異常。在JDK 1.2之后,提供了SoftReference類來實現(xiàn)軟引用。

  • 弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前。當(dāng)垃圾收集器工作時,無論當(dāng)前內(nèi)存是否足夠,都會回收掉只被弱引用關(guān)聯(lián)的對象。在JDK 1.2之后,提供了WeakReference類來實現(xiàn)弱引用。

  • 虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種引用關(guān)系。一個對象是否有虛引用的存在,完全不會對其生存時間構(gòu)成影響,也無法通過虛引用來取得一個對象實例。為一個對象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個對象被收集器回收時收到一個系統(tǒng)通知。在JDK 1.2之后,提供了PhantomReference類來實現(xiàn)虛引用。

3. 對象死亡的標(biāo)記過程:

即使在可達性分析算法中不可達的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段,要真正宣告一個對象死亡,至少要經(jīng)歷兩次標(biāo)記過程:

  • 如果對象在進行可達性分析后發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈,那它將會被第一次標(biāo)記并且進行一次篩選,篩選的條件是此對象是否有必要執(zhí)行finalize()方法。當(dāng)對象沒有覆蓋finalize()方法,或者finalize()方法已經(jīng)被虛擬機調(diào)用過,虛擬機將這兩種情況都視為“沒有必要執(zhí)行”。

    如果這個對象被判定為有必要執(zhí)行finalize()方法,那么這個對象將會放置在一個叫做F-Queue的隊列之中,并在稍后由一個由虛擬機自動建立的、低優(yōu)先級的Finalizer線程去執(zhí)行它。這里所謂的“執(zhí)行”是指虛擬機會觸發(fā)這個方法,但并不承諾會等待它運行結(jié)束,這樣做的原因是,如果一個對象在finalize()方法中執(zhí)行緩慢,或者發(fā)生了死循環(huán)(更極端的情況),將很可能會導(dǎo)致F-Queue隊列中其他對象永久處于等待,甚至導(dǎo)致整個內(nèi)存回收系統(tǒng)崩潰。

  • finalize()方法是對象逃脫死亡命運的最后一次機會,稍后GC將對F-Queue中的對象進行第二次小規(guī)模的標(biāo)記,如果對象要在 finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關(guān)聯(lián)即可,譬如把自己(this關(guān)鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標(biāo)記時它將被移除出“即將回收”的集合;如果對象這時候還沒有逃脫,那基本上它就真的被回收了。

下面的代碼演示了兩點:

  1. 對象可以在被GC時自我拯救。
  2. 這種自救的機會只有一次,因為一個對象的finalize()方法最多只會被系統(tǒng)自動調(diào)用一次
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();
        //對象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        // 因為Finalizer方法優(yōu)先級很低,暫停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();
        // 因為Finalizer方法優(yōu)先級很低,暫停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }
    }
}
finalize mehtod executed!  
yes, i am still alive :)  
no, i am dead :( 

PS : finalize()的運行代價高昂,不確定性大,無法保證各個對象的調(diào)用順序,應(yīng)該盡量避免使用。

4. 回收方法區(qū):

很多人認為方法區(qū)(或者HotSpot虛擬機中的永久代)是沒有垃圾收集的,Java虛擬機規(guī)范中確實說過可以不要求虛擬機在方法區(qū)實現(xiàn)垃圾收集,而且在方法區(qū)中進行垃圾收集的“性價比”一般比較低:在堆中,尤其是在新生代中,常規(guī)應(yīng)用進行一次垃圾收集一般可以回收70%~95%的空間,而永久代的垃圾收集效率遠低于此。

永久代的垃圾收集主要回收兩部分內(nèi)容:廢棄常量和無用的類。

  • 回收廢棄常量與回收Java堆中的對象非常類似。
    以常量池中字面量的回收為例,假如一個字符串“abc”已經(jīng)進入了常量池中,但是當(dāng)前系統(tǒng)沒有任何一個String對象是叫做“abc”的,換句話說,就是沒有任何String對象引用常量池中的“abc”常量,也沒有其他地方引用了這個字面量,如果這時發(fā)生內(nèi)存回收,而且必要的話,這個“abc”常量就會被系統(tǒng)清理出常量池。常量池中的其他類(接口)、方法、字段的符號引用也與此類似。

  • 判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面3個條件才能算是“無用的類”:

    • 該類所有的實例都已經(jīng)被回收,也就是Java堆中不存在該類的任何實例。
    • 加載該類的ClassLoader已經(jīng)被回收。
    • 該類對應(yīng)的java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

    虛擬機可以對滿足上述3個條件的無用類進行回收,這里說的僅僅是“可以”,而并不是和對象一樣,不使用了就必然會回收。是否對類進行回收,需要虛擬機的參數(shù)進行控制。
    在大量使用反射、動態(tài)代理、CGLib等ByteCode框架、動態(tài)生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。

三、垃圾收集算法 ( 如何回收?)


由于垃圾收集算法的實現(xiàn)涉及大量的程序細節(jié),而且各個平臺的虛擬機操作內(nèi)存的方法又各不相同,以下只是介紹幾種算法的思想及其發(fā)展過程。

  1. 標(biāo)記-清除算法:
    最基礎(chǔ)的收集算法是“標(biāo)記-清除”(Mark-Sweep)算法,如同它的名字一樣,算法分為“標(biāo)記”和“清除”兩個階段:首先標(biāo)記出所有需要回收的對象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對象。
    缺點:
    1)效率問題,標(biāo)記和清除兩個過程的效率都不高;
    2)空間問題,標(biāo)記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會導(dǎo)致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作。

  2. 復(fù)制算法:
    為了解決效率問題,一種稱為“復(fù)制”(Copying)的收集算法出現(xiàn)了,它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對象復(fù)制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。
    1)優(yōu)點:每次都是對整個半?yún)^(qū)進行內(nèi)存回收,內(nèi)存分配時也就不用考慮內(nèi)存碎片等復(fù)雜情況,只要移動堆頂指針,按順序分配內(nèi)存即可,實現(xiàn)簡單,運行高效。
    2)缺點:算法的代價是將內(nèi)存縮小為了原來的一半,未免太高了一點。


    現(xiàn)在的商業(yè)虛擬機都采用這種收集算法來回收新生代,研究表明,新生代中的對象98%是“朝生夕死”的,所以并不需要按照1∶1的比例來劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。
    當(dāng)回收時,將Eden和Survivor中還存活著的對象一次性地復(fù)制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用內(nèi)存空間為整個新生代容量的90%,只有10%的內(nèi)存會被“浪費”。
    當(dāng)然,90%的對象可回收只是一般場景下的數(shù)據(jù),我們沒有辦法保證每次回收都只有不多于10%的對象存活,當(dāng)Survivor空間不夠用時,需要依賴其他內(nèi)存(這里指老年代)進行分配擔(dān)保(Handle Promotion)。

  3. 標(biāo)記-整理算法:
    復(fù)制收集算法在對象存活率較高時就要進行較多的復(fù)制操作,效率將會變低。更關(guān)鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔(dān)保,以應(yīng)對被使用的內(nèi)存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。
    根據(jù)老年代的特點,有人提出了另外一種“標(biāo)記-整理”(Mark-Compact)算法,標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存。

  4. 分代收集算法:
    當(dāng)前商業(yè)虛擬機的垃圾收集都采用“分代收集”(Generational Collection)算法,這種算法并沒有什么新的思想,只是根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣就可以根據(jù)各個年代的特點采用最適當(dāng)?shù)氖占惴ā?br> 1)在新生代中,每次垃圾收集時都發(fā)現(xiàn)有大批對象死去,只有少量存活,那就選用復(fù)制算法,只需要付出少量存活對象的復(fù)制成本就可以完成收集。
    2)在老年代中因為對象存活率高、沒有額外空間對它進行分配擔(dān)保,就必須使用"標(biāo)記—清理"或者"標(biāo)記—整理"算法來進行回收。


推薦閱讀:《深入理解Java虛擬機:JVM高級特性與最佳實踐》周志明著


[2015-09-03]

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,441評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,475評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,834評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,009評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,559評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,306評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,516評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,728評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,249評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,484評論 2 379

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