記憶體回收介紹

weixin_33806914發表於2018-05-29

notes

本文基於《深入理解Java虛擬機器第二版》

在Java執行資料區時我們瞭解了Java執行時資料的各個區域,其中程式計數器、Java虛擬機器棧和本地方法棧的生命週期與執行緒相關,線上程建立時分配,線上程結束時釋放,故該部分記憶體不需要進行專門的記憶體回收。而方法區和Java堆記憶體的分配和回收是動態的,記憶體回收主要關注這部分記憶體。在Java虛擬機器規範中,並沒有對虛擬機器應當如何進行垃圾回收進行規定,不同的虛擬機器的實現方式也不同,下面主要是對相關的演算法進行介紹。

物件存活判定

在GC收集器回收記憶體之前,首先要判定哪些物件不再存活(不存在該物件的引用),存在以下兩種演算法可以進行物件存活的判斷。

引用計數演算法

最基本的演算法是給每個物件新增一個引用的計數器,當增加一個該物件的引用時,對計數器加一,反之則減一。在進行回收時,計數器的值為零,則表明該物件不再被使用,可以進行回收。該方法既簡單,又有效率,是一個非常實用的方法。然而,它存在一個嚴重的問題,即它無法解決物件之間的相互引用。

public class ReferenceCounter {
    private ReferenceCounter reference;
    
    public static void main(String[] args) {
        ReferenceCounter r1 = new ReferenceCounter();
        ReferenceCounter r2 = new ReferenceCounter();
        r1.reference = r2;
        r2.reference = r1;
    }
}
複製程式碼

針對以上程式碼,使用引用計數的方法時,r1和r2都存在對方的引用,造成計數器不為零而無法回收。然而實際上,上述物件是應當被回收的。

可達性分析演算法

該演算法的主要思想是以一系列的‘GC Roots’物件為起點,不斷向下搜尋,搜尋的路徑形成引用鏈。當一個物件沒有任何引用鏈引用時,表明該物件不再被使用,可以進行垃圾回收。

可以作為"GC Roots"的物件如下:

  • 虛擬機器棧中引用的物件
  • 方法區類靜態屬性引用的物件
  • 方法區中常量引用的物件
  • 本地方法棧中本地方法引用的物件

在Java虛擬機器的主流實現中,都採用了該演算法進行物件是否存活的判定。

垃圾收集演算法

垃圾收集的方式有很多,各個平臺虛擬機器的實現方式也不僅相同,下面介紹幾種演算法以便了解。

標記-清除演算法

該演算法分為"標記"和"清除"兩個階段,第一階段採用上面介紹的相關演算法標記出需要進行回收的物件,然後第二階段將標記的物件進行回收。

該演算法簡單易懂,然而卻存在以下問題:

  • 標記和清除的過程效率不高
  • 清除物件後,會造成記憶體產生大量的記憶體碎片,導致分配大物件時沒有足夠的連續記憶體而提前觸發垃圾收集

複製演算法

為了解決效率的問題,出現一種"複製"的演算法。該演算法將記憶體分為大小相同的兩塊,每次使用其中的一塊。當其中一塊使用完時,將存活的物件複製到另外一塊的一端,本塊記憶體可以全部清理。這種演算法避免了記憶體碎片的產生,記憶體分配只需要順序分配即可。缺點就是隻能使用總記憶體的一半,代價太高。

標記-整理演算法

為了避免標記-清除演算法產生大量的記憶體碎片,該方法應運而生。該方法第一階段也是對不再使用的物件進行標記,第二階段卻不再直接進行垃圾收集,而是將所有存活的物件移向記憶體的一端,然後將剩餘的記憶體全部清除。這樣就避免的不連續記憶體的產生,缺點就是需要進行大量的物件移動。

分代收集演算法

該演算法主要針對Java堆記憶體,將堆分為新生代和老年代。新生代中每次垃圾回收都會有大量的物件被回收,而老年代中物件的存活率比較高。

老年代中的物件一般不會發生頻繁的回收,所有可以使用標記-清除演算法或者標記-整理演算法進行垃圾回收,而新生代物件變動比較頻繁,可以使用複製演算法進行垃圾回收。

在HotSpot虛擬機器的實現中,對新生代的複製演算法進行了優化。由於新生代中大量的物件存活時間短,所以不需要將記憶體進行1:1劃分來保證有足夠的記憶體可以進行復制,而是將記憶體分為一塊較大的區域Eden和兩塊較小的區域Survivor,預設比例是8:1:1。每次只使用Eden和一塊Survivor空間,所以可用空間是新生代總空間的90%。在進行垃圾回收時,將存活的物件複製到另外一塊Survivor空間,然後將使用過的Eden和Survivor空間進行清除。如果存活的物件較多導致Survivor空間無法容納,那麼這些物件直接進入老年代。

記憶體分配

在多數情況下,物件在新生代的Eden空間進行分配,當Eden空間無法提供足夠的記憶體時,虛擬機器將發起一次Minor GC(新生代垃圾回收,與之對應的Full/Major GC,表示發生在老年代的垃圾回收,通常這種回收速度慢,伴隨至少一次的Minor GC)。

虛擬機器給每個物件定義了一個物件年齡計數器,如果物件在Eden空間建立,在每經過一次Minor GC而沒有被回收時,該計數器加一。當該計數器達到一定的值(預設15),就會晉升到老年代。為了更好的使用記憶體,如果Survivor空間中相同年齡的物件總和大於Survivor空間的一半,那麼年齡大於等於該年齡的物件直接進入老年代。

當要建立需要連續記憶體空間的大物件時,如長字串和大陣列,為了避免Eden和Survivor空間之間發生大量的記憶體複製,大物件可以之間分配到老年代。大物件所需記憶體可以通過引數**-XX:PretenureSizeThreshold**設定。

相關文章