1. 程式人生 > >垃圾收集與幾種常用的垃圾收集算法

垃圾收集與幾種常用的垃圾收集算法

嘗試 產生 統一 規則 存在 允許 成本 garbage 找到

前言:

  首先思考垃圾收集(Garbage Collection,GC)需要完成的三件事情

  1)哪些內存需要回收?

  2)什麽時候回收?

  3)如何回收?

  再上一個博客中提到了Java內存運行時區域的各個部分,其中程序計數器、虛擬機棧、本地方法棧3個區域會隨著線程而生,隨線程而滅;棧中的棧幀隨著方法的進行有條不紊地執行著出棧和入棧操作。每一個棧幀中分配多少內存基本上是在類結構確定下來時就已知得,因此這幾個區域的內存分配回收都具備確定性,在這幾個區域就不需要過多的考慮回收的問題,因為在方法結束或線程結束時內存就被回收了。而Java堆和方法區則不一樣,一個接口中的實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣,我們只有在程序處於運行期間才能知道會創建那些對象,這部分內存的分配和回收都是動態的,垃圾收集器所關註的是這部分內存。

一、對象已死

  1.所謂對象已死,其實就是垃圾收集器在對堆進行回收之前需要判斷這些對象中哪些還“存活著”,哪些已經“死去”

  2.判斷算法

    1)引用計數法(Reference Counting):許多教科書上判斷對象是否存活都是這個算法,但是在主流的Java虛擬機裏沒有選用這個算法來管理內存,下面來簡單介紹一下此算法,其實就是為對象中添加一個引用計數器,每當一個地方引用它時,計數器就加1;當引用失效時,計數器就減1;任何時刻計數器為0的對象就是不可能再被使用的

    2)可達性分析算法(Reachability Analysis):在主流的商用程序語言的實現中,都是通過可達性分析法來判定對象是否存活的。這個算法的基本思路就是通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的

    技術分享

    在Java語言中,可作為GC Roots的對象包括:

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

      方法區中常量引用的對象

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

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

  3.引用

    其實無論通過那種算法來判斷對象是否已死,判斷都與“引用”有關

    Java中引用可分為強引用、軟引用、弱引用、虛引用四種,這4中引用強度一次降低

      1)強引用:程序間普遍存在的,類似“Object obj = new Object()”這類引用,只要強引用還存在,垃圾收集器就永遠不會回收掉被引用的對象

      2)軟引用:用來描述一些還有用但並非必須的對象。對於軟引用關聯的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常

      3)弱引用:也是用來描述非必須對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉被弱引用關聯的對象

      4)虛引用:也成為幽靈引用或者歡迎引用,它是最弱的一種引用關系。一個對象是否具有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例

二、垃圾收集算法

  1.標記-清除算法

    最基本的收集算法“標記-清除”(Mark-Sweep)算法,算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象,之所以說它是最基本的收集算法,是因為後續的收集算法都是基於這種思路並對其不足進行改進而得到的。它的主要不足有兩個:

    一是效率問題,標記和清除效率都不高,二是空間問題,標記清除後會產生大量不連續的內存碎片,空間碎片太多可能會導致以後程序在運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作

    執行過程如下圖:

    技術分享

  2.復制算法

    為了解決效率問題,一種稱為“復制”(Copying)的收集算法出現了,他將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這塊的內存用完了,就將還存活這的對象復制到另外一塊上面,然後再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。只是這種算法的代價是將內存縮小為了原來的一半,未免太高了一點。

    技術分享

  3.標記-整理算法

    復制收集算法在對象存活率較高時就要進行較多的復制操作,效率將會變低。更關鍵的是如果不想浪費50%的空間就要使用額外的空間進行分配擔保(Handle Promotion當空間不夠時,需要依賴其他內存),以應對被使用的內存中所有對象都100%存活的極端情況

    對於“標記-整理”算法,標記過程仍與“標記-清除”算法一樣,但是後續步驟不是直接對可回收對象進行清理,而是讓所有的存活對象都向一端移動,然後直接清理掉端邊界以外的內存,”標記-整理“算法示意圖如下:

    技術分享

  4.分代收集算法

    當前的商業虛擬機的垃圾收集都是采用“分代收集”(Generational Collection)算法,這種算法並沒有什麽新的思想,只是根據對象存活周期的不同將內存劃分為幾塊。一般是把堆劃分為新生代和老年代,這樣就可以根據各個年代的特點采用最適合的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就采用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或者“標記-整理”算法來進行回收

三、內存分配與回收策略

  對象的內存分配往大方向上講,就是在堆上分配,對象主要分配在新生代的Eden區上(一種分代布局方式,將新生代內存分為一塊較大的Eden空間和兩塊較小的Survivor空間,這裏就不細說了,了解就好),如果啟動了本地線程分配緩沖TLAB(Thread Local Allocation Buffer),將按線程優先在TLAB上分配。少數情況下也可能會直接分配在老年代中,分配的規則並不是百分之百固定的,其細節取決於當前使用的是哪一種垃圾收集器組合,還有虛擬機中與內存相關的參數設置

  1.對象優先在Eden分配

    大多數情況下,對象在新生代Eden區中分配。當Eden區沒有足夠的空間進行分配時,虛擬機將發起一次新生代垃圾回收(Minor GC:發生在新生代的垃圾收集動作,因為Java對象大多數都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。這裏也順帶說一下老年代垃圾回收Major GC:經常會伴隨至少一次Minor GC,Major GC的速度一般會比Minor GC慢10倍以上)

  2.大對象直接進入老年代

    所謂大對象,是指需要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串和數組。大對象對虛擬機的內存分配來說就是一個壞消息,經常出現大對象容易導致內存還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來安置他們

  3.長期存活的對象將進入老年代

    既然Java虛擬機采用了分代收集的思想來管理內存,那麽內存回收時就必須能識別哪些對象應該放在新生代,哪些對象應放在老年代。為了做到這一點,虛擬機為每個對象定義了一個對象年齡計數器。如果對象在Eden出生並經過第一次Minor GC後仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並且對象年齡增加1歲,當它的年齡增加到一定程度(默認15歲),將會晉升到老年代中

  4.動態對象年齡判定

    為了能更好地適應不同程序的內存狀況,虛擬機並不是永遠地要求對象的年齡必須達到了最大年齡才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到最大年齡

  5.空間分配擔保

    在發生Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那麽Minor GC可以確保是安全的。如果不成立虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。如果允許,那麽會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試著進行一次Minor GC,盡管這次Minor GC有風險;如果小於,或者不允許擔保失敗,那這是也要改為一次老年代垃圾回收

    解釋一下“風險”是什麽風險:新生代使用的是復制收集算法,但為了內存利用率,只使用其中一個Survivor空間作為輪換備份,因此當出現大量對象在Minor GC後仍然存活的情況(最極端的情況就是內存回收後新生代中對象都存活),就需要老年代進行分配擔保,把Survivor無法容納的對象直接進入老年代   

補充: 

  參考:深入Java虛擬機

  對垃圾收集部分與內存分配部分做了簡單整理,供大家快速了解此部分知識點,想要深入了解可以去看此書

垃圾收集與幾種常用的垃圾收集算法