Java內存管理——垃圾收集器

 

  概述html


 

提及垃圾收集(Garbage Collection,GC),大部分人都把這項技術當作Java語言的伴生產物。事實上,GC的歷史遠遠比Java久遠,1960年誕生於MIT的Lisp是第一門真正使用內存動態分配和垃圾收集技術的語言。當Lisp還在胚胎時期時,人們就在思考:程序員

  GC須要完成的三件事情:算法

     哪些內存須要回收?緩存

    何時回收?併發

    如何回收?函數

通過半個世紀的發展,內存的動態分配與內存回收技術已經至關成熟,一切看起來都進入了「自動化」時代,那爲何咱們還要去了解GC和內存分配呢?答案很簡單:當須要排查各類內存溢出、內存泄漏問題時,當垃圾收集成爲系統達到更高併發量的瓶頸時,咱們就須要對這些「自動化」的技術實施必要的監控和調節。高併發

  把時間從半個世紀之前撥回到如今,回到咱們熟悉的Java語言。第2章介紹了Java內存運行時區域的各個部分,其中程序計數器、虛擬機棧、本地方法棧三個區域隨線程而生,隨線程而滅;棧中的棧幀隨着方法的進入和退出而有條不紊地執行着出棧和入棧操做。每個棧幀中分配多少內存基本上是在類結構肯定下來時就已知的(儘管在運行期會由JIT編譯器進行一些優化,但在本章基於概念模型的討論中,大致上能夠認爲是編譯期可知的),所以這幾個區域的內存分配和回收都具有肯定性,在這幾個區域內不須要過多考慮回收的問題,由於方法結束或線程結束時,內存天然就跟隨着回收了而Java堆和方法區則不同,一個接口中的多個實現類須要的內存可能不同,一個方法中的多個分支須要的內存也可能不同,咱們只有在程序處於運行期間時才能知道會建立哪些對象,這部份內存的分配和回收都是動態的,垃圾收集器所關注的是這部份內存,本書後續討論中的「內存」分配與回收也僅指這一部份內存。性能

 

  回收策略優化


 

  回收做用:   ui

   經過清除不用的對象來釋放內存,同時垃圾收集的另一個重要做用就是消除堆內存空間的碎片

  1. 引用計數

  不少教科書判斷對象是否存活的算法是這樣的:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任什麼時候刻計數器都爲0的對象就是不可能再被使用的。所以A的回收可能會致使連鎖反應。

  客觀地說,引用計數算法(Reference Counting)的實現簡單,斷定效率也很高,在大部分狀況下它都是一個不錯的算法,也有一些比較著名的應用案例,例如微軟的COM(Component Object Model)技術、使用ActionScript 3的FlashPlayer、Python語言以及在遊戲腳本領域中被普遍應用的Squirrel中都使用了引用計數算法進行內存管理。可是,Java語言中沒有選用引用計數算法來管理內存,其中最主要的緣由是它很難解決對象之間的相互循環引用的問題。

  優勢:簡單,快

  缺點:沒法檢測循環引用,好比A的子類a引用了A,A又引用了a,所以A和a永遠不會被回收。這個缺點是致命的,所以如今這種策略已經不用。

  2. 根搜索算法 

  在主流的商用程序語言中(Java和C#,甚至包括前面提到的古老的Lisp),都是使用根搜索算法(GC Roots Tracing)斷定對象是否存活的。這個算法的基本思路就是經過一系列的名爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來講就是從GC Roots到這個對象不可達)時,則證實此對象是不可用的。如圖3-1所示,對象object 五、object 六、object7雖然互相有關聯,可是它們到GC Roots是不可達的,因此它們將會被斷定爲是可回收的對象。

  在Java語言裏,可做爲GC Roots的對象包括下面幾種:

    虛擬機棧(棧幀中的本地變量表)中的引用的對象。

    方法區中的類靜態屬性引用的對象。

    方法區中的常量引用的對象。

    本地方法棧中JNI(即通常說的Native方法)的引用的對象

  跟蹤收集器一般使用兩種策略來實現:

  1.壓縮收集器:遍歷的過程當中若是發現對象有效,則馬上把對象越過空閒區滑動到堆的一端,這樣堆的另外一端就出現了一個大的連續空閒區,從而消除了堆碎片。

  2.拷貝收集器:堆被分爲大小相等的兩個區域,任什麼時候候都只使用其中一個區域。對象在同一個區域中分配,直到這個區域被耗盡。此時,程序執行被停止,堆被遍歷,遍歷時被標記爲活動的對象被拷貝到另一個區域。這種作法用稱之爲「中止並拷貝」。

  這種作法的主要缺點是:太粗暴,要拷貝就都拷貝,粒度太大,總體性能不高。所以就有了更先進的「按代收集的收集器」

  3. 按代收集的收集器

  基於兩個事實:

  1)大多數程序建立的大部分對象都有很短的生命週期。

  2)大多數程序都建立一些具備很是長生命週期的對象。

  所以按代收集策略就是在「中止並拷貝」策略基礎之上,把對象按照壽命分爲三六九等,不是一視同仁。它把堆劃分爲多個子堆,每個子堆爲一「代」對象服務。最年幼的那一代進行最頻繁的垃圾收集。沒通過一次垃圾收集,存活下來的對象就會「成長」到更老的「代」,越是老的「代」對象數量應該越少,他們也越穩定,所以就能夠採起很經濟的策略來處理他們,簡單「照顧」一下他們就行啦。這樣總體的垃圾收集效率要比簡單粗暴的「中止並拷貝」高。

  4.火車算法

  火車算法是用來替代按代收集策略的嗎?不是的,能夠說,火車算法是對按代收集策略的一個有力補充。咱們知道按代收集策略把堆劃分爲多個」代「,每一個代均可以指定最大size,可是」成熟對象空間「除外,」成熟對象空間「不能指定最大size,由於它是」最老「對象的最終也是惟一的歸宿,除此以外,這些」老傢伙「無處可去。而你沒法肯定一個系統最終會有多少老對象擠進」成熟對象空間「。

  火車算法詳細說明了按代收集的垃圾收集器的成熟對象空間的組織。火車算法的目的是爲了在成熟對象空間提供限定時間的漸進收集。

  火車算法把成熟對象空間劃分爲固定長度的內存塊,算法每次在一個塊中單獨執行。爲何叫」火車算法「?這與算法組織這些塊的方式有關。

  每塊數據至關於一節車箱

  每個數據塊屬於一個集合,集合內的全部數據塊已經進行排序,所以集合就比如是一列火車

  成熟對象空間又包含多個集合,所以就比如有多列火車,而成熟對象空間就比如是火車站。

  

  14中包含了若干個被按序標記的火車火車由隨意多個一樣被按序標記的車箱組成在這個例子中有兩列火車每一個車箱最多能夠存儲三個對象每列火車能夠包含任意多個車箱.

  火車的記憶集合是它全部車箱記憶集合的總和, 不包括那些來自其它火車的引用. 在圖14中, 對象E是1.1車箱在引用集合中, 可是他不在1號火車的引用集合中. 由於垃圾回收算法老是從標記最小的車箱開始, 在更新引用集合的時候, 只有那些來自標記高的車箱的引用才被看做是. 所以, 對象E屬於車箱1.1的記憶集合, 而對象C不在車箱1.2的記憶集合中.

  當垃圾回收器收集第一個車箱, 對象A須要保留下來, 因爲是根引用指向它, 因此它會被拷貝到一個徹底新的火車中去. 因爲對象B只有被A引用, 因此它會被拷貝到和A同一列火車中去. 這一點很重要, 由於經過這種方式, 自循環的垃圾對象結構最終被轉移到同一列單獨的火車中去了. 因爲對象C被來自同一列火車的對象引用, 因此它被拷貝到了火車的最後去了. 如今第一個車箱空了, 能夠被釋放了. 經過第一遍回收, 火車站中的情況能夠如圖15所示

  

  記憶集合將會相應地進行更新第一列火車已經沒有從外面(這裏的外面指的是第一列火車之外)指向的引用的,因此在下一次回收中整個火車空間將會被案例的釋放如圖16所示.  

  

  任什麼時候候在第一列火車中自循環的垃圾對象結構不會被拷貝到另外火車中去當全部不在這個自循環結構中的對象被拷貝到其它火車中後這列火車將會被釋放這很容易理解但是是否能保證每個自循環的結構最終都會留在第一列火車中呢若是一個自循環結構分佈在一些不一樣的火車中那麼在一系列迭代以後原來第二列火車會成爲這個自循環結構的第一列火車而且結構體中的全部對象都會被分配到其它火車中去.(這裏的其它火車指的是剛纔自循環結構所佔據的一些火車可是除去第一列火車.). 所以包含這個自循環結構對象火車數量會減小一個當火車數目達到1剩下的這個火車中包含了自循環結構的全部對象因而這個垃圾對象結構能夠被正確的回收了.
17反映一個由4個對象組成的自循環結構.

  

  總體算法流程

  1.選擇標號最小的火車.

  2.若是火車的記憶集合是空的, 釋放整列火車並終止, 不然進行第三步操做.

  3.選擇火車中標號最小的車箱.

  4.對於車箱記憶集合的每一個元素:

    若是它是一個被根引用引用的對象, 那麼, 將拷貝到一列新的火車中去; 若是是一個被其它火車的對象指向的對象, 那麼, 將它拷貝到這個指向它的火車中去.  

    假設有一些對象已經被保留下來了, 那麼經過這些對象能夠觸及到的對象將會被拷貝到同一列火車中去.

  在這個步驟中, 有必要對受影響的引用集合進行相應地更新. 若是一個對象被來自多個火車的對象引用, 那麼它能夠被拷貝到任意一個火車去.

  釋放車箱而且終止.

 

  再談引用


 

  不管是經過引用計數算法判斷對象的引用數量,仍是經過根搜索算法判斷對象的引用鏈是否可達,斷定對象是否存活都與「引用」有關。在JDK 1.2以前,Java中的引用的定義很傳統:若是reference類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用。這種定義很純粹,可是太過狹隘,一個對象在這種定義下只有被引用或者沒有被引用兩種狀態,對於如何描述一些「食之無味,棄之惋惜」的對象就顯得無能爲力。咱們但願能描述這樣一類對象:當內存空間還足夠時,則能保留在內存之中;若是內存在進行垃圾收集後仍是很是緊張,則能夠拋棄這些對象。不少系統的緩存功能都符合這樣的應用場景。

  在JDK 1.2以後,Java對引用的概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(WeakReference)、虛引用(Phantom Reference)四種,這四種引用強度依次逐漸減弱。

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

  軟引用用來描述一些還有用,但並不是必需的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中並進行第二次回收。若是此次回收仍是沒有足夠的內存,纔會拋出內存溢出異常。在JDK 1.2以後,提供了SoftReference類來實現軟引用。

  弱引用也是用來描述非必需對象的,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在JDK 1.2以後,提供了WeakReference類來實現弱引用。

  虛引用也稱爲幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的惟一目的就是但願能在這個對象被收集器回收時收到一個系統通知。在JDK 1.2以後,提供了PhantomReference類來實現虛引用。

 

  生存仍是死亡?


 

  在根搜索算法中不可達的對象,也並不是是「非死不可」的,這時候它們暫時處於「緩刑」階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程:若是對象在進行根搜索後發現沒有與GC Roots相鏈接的引用鏈,那它將會被第一次標記而且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種狀況都視爲「沒有必要執行」。

  若是這個對象被斷定爲有必要執行finalize()方法,那麼這個對象將會被放置在一個名爲F-Queue的隊列之中,並在稍後由一條由虛擬機自動創建的、低優先級的Finalizer線程去執行。這裏所謂的「執行」是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束。這樣作的緣由是,若是一個對象在finalize()方法中執行緩慢,或者發生了死循環(更極端的狀況),將極可能會致使F-Queue隊列中的其餘對象永久處於等待狀態,甚至致使整個內存回收系統崩潰。finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模的標記,若是對象要在finalize()中成功拯救本身—只要從新與引用鏈上的任何一個對象創建關聯便可,譬如把本身(this關鍵字)賦值給某個類變量或對象的成員變量,那在第二次標記時它將被移除出「即將回收」的集合;若是對象這時候尚未逃脫,那它就真的離死不遠了。從代碼清單3-2中咱們能夠看到一個對象的finalize()被執行,可是它仍然能夠存活。

  從代碼清單3-2的運行結果能夠看到,SAVE_HOOK對象的finalize()方法確實被GC收集器觸發過,而且在被收集前成功逃脫了。

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;
    public void isAlive(){
        System.out.println("yes,i am still alive!");
    }
    protected void finalize() throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();
        //對象第一次成功拯救本身
        SAVE_HOOK = null;
        System.gc();
        // 由於Finalizer方法優先級很低,暫停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方法優先級很低,暫停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no,i am dead!");
        }
    }
}

運行結果:

1 finalize method executed!
2 yes, i am still alive!
3 no, i am dead!

  另一個值得注意的地方就是,代碼中有兩段徹底同樣的代碼片斷,執行結果倒是一次逃脫成功,一次失敗,這是由於任何一個對象的finalize()方法都只會被系統自動調用一次,若是對象面臨下一次回收,它的finalize()方法不會被再次執行,所以第二段代碼的自救行動失敗了。 須要特別說明的是,上面關於對象死亡時finalize()方法的描述可能帶有悲情的藝術色彩,筆者並不鼓勵你們使用這種方法來拯救對象。相反,筆者建議你們儘可能避免使用它,由於它不是C/C++中的析構函數,而是Java剛誕生時爲了使C/C++程序員更容易接受它所作出的一個妥協。它的運行代價高昂,不肯定性大,沒法保證各個對象的調用順序。有些教材中提到它適合作「關閉外部資源」之類的工做,這徹底是對這種方法的用途的一種自我安慰。finalize()能作的全部工做,使用try-finally或其餘方式均可以作得更好、更及時,你們徹底能夠忘掉Java語言中還有這個方法的存在。

 

 

PS.

參考連接

http://www.cnblogs.com/gw811/archive/2012/10/19/2730258.html

http://www.cnblogs.com/wenfeng762/archive/2011/11/18/2137882.html

http://nileader.blog.51cto.com/1381108/402609

相關文章
相關標籤/搜索