1. 程式人生 > >第三章 垃圾收集器與記憶體分配策略

第三章 垃圾收集器與記憶體分配策略

3.1 概述

      1960年誕生的Lisp是第一門真正使用記憶體動態分配和垃圾收集技術的語言。程式計數器、虛擬機器棧、本地方法棧三個區域隨執行緒而生,隨執行緒而滅;棧中的棧幀隨著方法的進入和退出而有條不紊的執行著出棧和入棧操作。每一個棧幀中分配多少記憶體基本上是在類結構確定下來時就已知的,因此這幾個區域的記憶體分配和回收都具備確定性,在這幾個區域內不需要過多考慮回收的問題,因為方法結束或執行緒結束時,記憶體自然就跟著回收了。而Java堆和方法區則不一樣,一個介面中的多個實現類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也可能不一樣,我們只有在程式處於執行期間時才能知道會建立哪些物件,這部分記憶體的分配和回收都是動態的,垃圾回收期關注的是這部分記憶體。

3.2 物件已死

3.2.1 引用計數演算法

      給物件新增一個引用計數器,每當有一個地方引用它時,計數器值就加1,;當引用失效時,計數器值就減1;任何時刻計數器值為0的物件就是不可能再被使用的。

      Java語言中沒有選用計數演算法來管理記憶體,其中最主要的原因是它很難解決物件之間的相互迴圈引用的問題。比如:物件objA和objB都有欄位instance,賦值令objA.instance = objB及objB.instance = objA,除此之外,這兩個物件再無任何引用,實際上這兩個物件已經不可能再被訪問,但是它們因為互相引用著對方,導致它們的引用計數都不為0,於是引用計數演算法無法通知GC收集器回收它們。

3.2.2 根搜尋演算法

      在主流的商用程式語言中,都是使用根搜尋演算法判定物件是否存活的。這個演算法的思路就是通過一系列的名為“GC Roots ”的物件作為起始點,從這些節點開始向下搜尋,搜尋走過的路徑成為“引用鏈”,當一個物件到GC Roots沒有任何的鏈相連,則證明此物件是不可用的。

      在Java語言裡,可作為GC Roots的物件包括下面幾種:(虛擬機器棧中的引用的物件、方法區中的類靜態屬性引用的物件、方法區中的常量引用的物件、本地方法棧中JNI的引用的物件)。

3.2.3 再談引用

      無論是通過引用計數演算法判斷物件的引用數量還是通過根搜尋演算法判斷物件的引用鏈是否可達,判定物件是否存活都與“引用”有關。如果reference型別的資料中儲存的數值代表的是另一塊記憶體的起始地址,就成這塊記憶體代表著一個引用。

      在JDK1.2之後,Java對引用的概念進行了擴充,將引用分為強引用、軟引用、弱引用、虛引用四種,這四種引用強度依次逐漸減弱。

      強引用就是指程式程式碼之中普遍存在的,類似“Object obj = new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的物件。 軟引用用來描述一些還有用,但並非必須的物件。對於軟引用關聯著的物件,在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍之中並進行第二次回收。如果這次回收還是沒有足夠的記憶體,才會丟擲記憶體溢位異常。在JDK1.2之後,提供了SoftReference類來實現軟引用。 弱引用也是用來描述非必須物件的,但是它的強度比軟引用更弱一些,被弱引用關聯的物件只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。WeakReference  虛引用也被稱為幽靈引用或者幻影引用,它是最弱的一種引用關係。一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個物件例項。為一個物件設定虛引用關聯的唯一目的就是希望能在這個物件被收集器回收時收到一個系統通知。PhantomReference。

3.2.4 回收方法區

      在方法區中進行垃圾收集的價效比一般比較低,在堆中,尤其在新生代中,常規應用進行一次垃圾收集一般可以回收70-90%的空間,而永久代的垃圾收集效率遠低於此。永久代的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。

      判斷一個常量是否是廢棄常量比較簡單,而要判定一個類是否是無用的類的條件則相對苛刻許多。類需要同時滿足下面3個條件才能算是無用的類。(該類所有的例項都已經被回收,也就是Java堆中不存在該類的任何例項;載入該類的ClassLoader已經被回收;該類對應的java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。) 

      虛擬機器可以對滿足上述3個條件的無用類進行回收,這裡說的僅僅是“可以”,而不是和物件一樣,不使用了就必然會回收。在大量使用反射、動態代理、CGLib等bytecode框架的場景,以及動態生成JSP和OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機器具備類解除安裝的功能,以保證永久代不會溢位。

3.3 垃圾收集演算法

3.3.1 標記——清除演算法

       最基礎的收集演算法是標記-清除演算法,如它的名字一樣,演算法分為標記和清除兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收掉所有被標記的物件。它的主要缺點有兩個:一個是效率問題,標記和清除的過程效率都不高;另外一個是空間問題,標記清楚之後會產生大量不連續的記憶體碎片,空間碎片過多可能會導致當程式在以後的執行過程中需要分配較大物件時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。

3.3.2 複製演算法

       它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊,當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這種演算法的代價是將記憶體縮小為原來的一半,未免太高了一點。

3.3.3 標記——整理演算法

      標記過程仍然與“標記——清除”演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有可存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。

3.3.4 分代收集演算法

      當前商業虛擬機器的垃圾收集都採用“分代收集”演算法,這種演算法並沒有什麼新的思想,只是根據物件的存活週期的不同將記憶體劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集演算法。在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量存活,那就選用複製演算法,只需要付出少量存活物件的複製成本就可以完成收集。而老年代中因為物件存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記——清理”或“標記——整理”演算法來進行回收。

3.4 垃圾收集器

      如果說垃圾收集演算法是記憶體回收的方法論,垃圾回收器就是記憶體回收的具體實現。java虛擬機器規範中對垃圾收集器應該如何實現並沒有任何規定,因此不同廠商不同版本的虛擬機器所提供的垃圾收集器都可能有很大的差別。

3.4.1 Serial收集器

      最基本,歷史最悠久的收集器,在JDK1.3.1之前是虛擬機器新生代收集的唯一選擇。這個收集器是一個單執行緒的收集器,“單執行緒”的意義不僅僅是說明它只會使用一個CPU或一條收集執行緒去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作執行緒,直到它收集結束。優於其他收集器的地方:簡單而高效,對於限定單個CPU環境來說,Serial收集器由於沒有執行緒互動的開銷,專心做垃圾收集自然可以獲得最高的單執行緒收集效率。

3.4.2 ParNew收集器

      是Serial收集器的多執行緒版本,除了使用多執行緒進行垃圾收集之外,其餘行為包括Serial收集器可用的所有控制引數、收集演算法、Stop The World、物件分配規則、回收策略等都與Serial收集器完全一樣,實現這兩種收集器也共用了相當多的程式碼。

3.4.3 Parallel Scavenge收集器

      Parallel Scavenge收集器也是一個新生代收集器,它也是使用複製演算法的收集器 ,又是並行的多執行緒收集器。它的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是儘可能的縮短垃圾收集時使用者執行緒的停頓時間,而Parallel Scavenge收集器的目標是達到一個可控制的吞吐量。

3.4.4 Serial Old收集器

      是Serial收集器的老年代版本,它同樣是一個單執行緒收集器,使用“標記——整理”演算法。這個收集器的主要意義也是被Client模式下的虛擬機器使用。

3.4.5 Parallel Old收集器

      是Parallel收集器的老年代版本,這個收集器是在JDK1.6中才開始提供的,在此之前,新生代的Parallel Scavenge收集器一直處於比較尷尬的狀態。原因是,如果新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old收集器別無選擇。

3.4.6 CMS收集器

      CMS收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的java應用都集中在網際網路站或B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,已給使用者帶來較好的體驗。基於“標記——清除演算法”,它的運作過程相對於前面幾種收集器來說要更復雜一些,整個過程分為4個步驟:初始標記、併發標記、重新標記、併發清除。

      優點:併發收集,低停頓。 

      缺點:CMS收集器對CPU資源非常敏感;CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。由於CMS併發清理階段使用者執行緒還在執行著,伴隨程式的執行自然還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS無法在本次收集中處理掉它們,只好留待下一次GC時再將其處理掉。這一部分垃圾就稱為浮動垃圾;最後一個缺點是收集結束時會產生大量的空間碎片,空間碎片過多時將會給大物件分配帶來很大的麻煩,往往會出現老年代還有很大的空間剩餘,但是無法找到足夠大的連續空間來分配當前物件,不得不提前觸發一次Full GC。

3.4.7 G1收集器

      G1收集器是垃圾收集器理論進一步發展的產物,它與前面的CMS收集器相比有兩個顯著的改進:一是G1收集器是基於“標記——整理”演算法實現的收集器,也就是說它不會產生空間碎片,這對於長時間執行的應用系統來說十分重要。二是它可以非常精確的控制停頓,既能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎己經是實時Java的垃圾收集器的特徵了。G1收集器可以實現在基本上不犧牲吞吐量的前提下完成低停頓的記憶體回收。