1. 程式人生 > >垃圾收集算法

垃圾收集算法

量算 失效 root 無法找到 spa 正在 節點 gc roots code

什麽是垃圾回收?

  垃圾回收,Garbage Collection,簡稱GC。

  在我們日常生活中的垃圾,我們會丟入垃圾桶,等待清潔工處理掉。

  Java中的垃圾,指存在於內存中,不會再被使用的對象,需要把這些無用的對象進行清理掉,那麽,這些垃圾對象所占用的空間就可以被騰出來被其他對象使用,對內存空間管理來說,識別和清理垃圾是非常重要的。

  而怎樣找出並清理這些垃圾呢?這裏介紹幾種垃圾收集算法。

引用計數算法

  引用計數法是最簡單的一種方法。

  它的實現很簡單:給對象配備一個整型的引用計數器,每當有一個地方引用這個對象時,計數器值就加1,當引用失效時,計數器值就減1,只要該對象的計數器的值為0,那麽這個對象就是不可能再被使用的,就可以視作垃圾處理。

  引用計數法的實現簡單,判斷效率也高,但是,在java虛擬機內卻沒有使用引用計數法來管理內存,最主要的原因是它無法解決對象之間互相引用的問題。

  當然,引用計數法還有一個缺點,就是每次因引用的產生和消除的時候,需要伴隨一個計數器的加法操作和減法操作,對系統的性能會有一定影響。但是這一點並不是致命的。

致命的是引用循環問題,示例如下:

public class Test {
    public Object obj = null;
    public static void main(String[] args) throws Exception{
        Test a 
= new Test(); Test b = new Test(); a.obj = b; b.obj = a; a = null; b = null; System.gc(); } }

  如果使用引用計數法,a和b兩個對象是無法被回收的。

可達性分析算法

  在java中,判斷對象是否存活,是通過可達性分析來實現的,在後續的各種垃圾收集的算法中,幾乎都是通過可達性分析來標記垃圾的。

  它的思路是通過一些稱作“GC Roots”的對象作為起始點,從這些節點開始往下搜索,搜索走過的路徑叫做引用鏈,當一個對象到GC Roots沒有引用鏈相連接的時候,就可以叫做這個對象到GC Roots不可達,則此對象就可以定為不可用。

如圖所示:

技術分享圖片

  盡管obj5、obj6、obj7之間互相引用,但是他們到GC Roots都是不可達的,所以他們將會被判定為可回收垃圾。

  在Java中,GC Roots包括下面幾種:

1.Java棧(棧幀中的本地變量表)裏面引用的對象。
2.方法區中靜態屬性引用的對象。
3.方法區中常量引用的對象。
4.本地方法棧中JNI引用的對象。

標記清除算法

  標記清除算法將垃圾回收分為標記、清除兩個階段,這種分步執行的思路奠定了現代垃圾收集算法的思想基礎。

  和引用計數法不同的是,標記清除算法不需要運行環境檢測每一次內存分配和指針操作,只需要在標記階段中根據可達性分析算法就可以找出所有存活對象,進行標記,未標記的對象即視為垃圾。在清除階段,清除所有未被標記的對象。

如圖:

技術分享圖片

  標記清除算法最大的問題就是會產生空間碎片,由於被執行內存回收的對象占用的內存有可能是一些不連續的內存塊,清除之後就會產生大量的不連續的內存碎片,空間碎片太多可能會導致以後在程序運行過程中需要給較大對象分配內存時,無法找到足夠的連續內存。

復制算法

  為了解決標記清除算法的缺陷,JVM後來引入了復制算法。

  復制算法的核心思想是:將原有的內存空間分為兩塊,每次只使用其中一塊,在垃圾回收時,將正在使用的內存中的存活對象復制到另一塊內存空間去,然後,清除這一塊內存空間中的所有對象,交換兩個內存塊的角色,完成垃圾回收。

  如果系統中垃圾對象很多,那麽復制算法需要復制的存活對象數量就不會太大,在真正需要回收垃圾的時候,復制算法的效率是很高的。

  並且對象在垃圾回收過程中統一被復制到新的內存空間中,因此可以確保回收後的內存空間是沒有碎片的。

如圖:

技術分享圖片

  復制算法的缺陷是將系統內存折半。

  在Java的新生代串行垃圾回收器中,就使用了復制算法的思想。

  (存放年輕對象的堆空間就叫新生代,年輕對象是指剛剛創建的或者沒怎麽經歷過垃圾回收的對象,同樣的,存放老年對象的堆空間叫做老年代,老年對象指經歷過多次垃圾回收依然還沒死的對象)

  新生代分為eden空間、from空間和to空間3個部分,其中from和to空間可以視為用於復制的兩塊大小相同、地位相等、並且可以進行角色互換的空間塊。from空間和to空間也稱為survivor空間,即幸存者空間,用來存放沒有被回收的對象。

看圖理解:

技術分享圖片

  在垃圾回收時,Eden空間中的存活對象會被復制到未使用的survivor空間中(假設是To),正在使用的survivor空間(假設是from)中的年輕對象也會被復制到to空間中(大對象、或者老年對象會直接進入老年代,如果to空間已滿,則對象也會直接進入老年代),這時候eden空間和from空間中剩余對象就是垃圾對象,可以直接清空,接下來,from空間和to空間將會互換位置,to空間則存放此次回收後的存活對象,這種趕緊的復制算法既保證了空間的連續性,又避免了大量的內存空間浪費。

  其實復制算法無非就是使用to空間作為一個臨時的空間交換角色。

  復制算法比較適用於新生代,因為在新生代,垃圾對象通常會多於存活對象,復制算法的效果會比較好。

標記壓縮算法

  復制算法的高效性是建立在存活對象少,垃圾對象多的前提下的,這種情況在年輕代經常發生,但是在老年代更常見的情況是大部分對象都是存活對象,如果依然使用復制算法,那麽復制的成本會很高,所以基於老年代垃圾的特性,需要使用其他的算法。

  標記壓縮算法就是老年代使用的算法,也可以叫做標記整理算法,他其實是在標記清除算法上做了相應的優化,標記階段依然是使用可達性分析對所有存活對象進行標記,但之後並不是簡單的清理未標記的對象,而是將所有標記的對象壓縮到內存的另一端,之後,再清理邊界外的所有空間。

如圖所示:

技術分享圖片

  標記壓縮算法的最終效果相當於標記清除算法執行完成後,再進行一次內存碎片整理,這種方法既避免了碎片的產生,又不需要兩塊相同的內存空間,性價比相對於來說較高。

分區算法

  在垃圾回收過程中,應用程序所有的線程都會處於一種掛起狀態,暫停一切正常工作,等待垃圾回收結束。

  如果垃圾回收時間過長,將嚴重影響用戶的體驗或者系統穩定性,為了解決這個問題,所以誕生了一個叫分區算法的概念,也可以叫做增量算法。

  它的基本思想是,如果一次性將所有的垃圾進行處理,需要造成系統長時間的停頓,那麽就可以讓垃圾收集線程和應用程序線程交替執行。將java堆的內存空間分割成多個小片區域,每次垃圾收集線程只收集一小片區域的內存空間,接著切換到應用程序線程,依次反復,直到垃圾收集完成。

分代收集算法

  分代收集算法並沒有什麽新的思想,它只是根據對象存活周期的不同特點將內存劃分為幾塊,一般是把java堆分為新生代和老年代,這樣可以根據各個年代的特點采用最適合的垃圾收集算法。

  一般來說,java虛擬機會把所有新建的對象都放入稱為新生代的內存區域,新生代的特點是對象會很快被回收,所以,在新生代就選擇效率較高的復制算法。

  當一個對象經過幾次垃圾回收後依然存活,對象就會被放入老年代區域,在老年代中幾乎所有的對象都是經過幾次垃圾回收後依然存活的,可以認為這些對象在一段時期內,甚至在應用程序的整個生命周期中,都是常駐內存的,如果依然要用復制算法回收老年代,將要復制大量對象,所以這種做法是不可取的。

  根據分代的思想,需要對老年代的回收使用與新生代不同的標記清除或標記壓縮算法,可以提高垃圾回收的效率。

  分代收集的思想被現有的虛擬機廣泛使用,幾乎目前所有的商業虛擬機的垃圾回收器都采用了這種思想。

見下圖:

技術分享圖片

垃圾收集算法