1. 程式人生 > >自動記憶體管理機制(2)- 記憶體回收和垃圾收集演算法

自動記憶體管理機制(2)- 記憶體回收和垃圾收集演算法

自動記憶體管理機制(2)- 記憶體回收和垃圾收集演算法

1. 概述

首先思考三個問題:

  • 哪些記憶體需要回收
  • 什麼時候回收
  • 如何回收

程式計數器、虛擬機器棧、本地方法棧是執行緒私有的,因此這幾個區域的記憶體分配和回收都具有確定性(執行緒結束時執行垃圾回收)。但Java堆和方法區因為是執行緒共有的,這部分的記憶體分配和回收都是動態的,垃圾收集器關注的就是這部分的記憶體。


2. 堆記憶體的回收

2.1. 如何判定是否回收

堆裡面存放著Java中幾乎所有的物件例項。在進行垃圾回收之前,首先要判斷這些物件是否可以被回收,有以下兩種判斷演算法:

  • 引用計數演算法

    每個物件都有一個計數器,當這個物件被一個變數或另一個物件引用一次,該計數器加1,;若該引用失效則計數器減1.當計數器為0時,就認為該物件是無效物件。

    引用計數法雖然簡單,但存在一個嚴重的問題,它無法解決迴圈引用的問題。

  • 可達性分析演算法

    所有和GC Roots直接或間接關聯的物件都是有效物件,和GC Roots沒有關聯的物件就是無效物件。

    GC Roots定義:

    • 虛擬機器棧(棧幀中的本地變量表)中引用的物件
    • 方法區中類靜態屬性引用的物件
    • 方法區中常量引用的物件
    • 本地方法棧中JNI(即一般所說的Native方法)引用的物件

2.2. Java中引用的型別

判斷一個物件是否存活,無論是引用計數法還是可達性分析演算法,都需要和“引用”相關。

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

  • 強引用就是指在程式程式碼中普遍存在的,類似Object obj = new Object()這類的引用(即我們平時使用的引用,也就是通過關鍵字new建立的物件所關聯的引用就是強引用)。

    只要強引用還存在,垃圾收集器永遠不會回收掉被引用的物件。

  • 軟引用用來描述一些還有用但又非必須的物件。在系統要發生OOM異常時,才會對這些物件進行回收。

    軟引用通過SoftReference類實現。軟引用的生命週期比強引用短一些。

  • 弱引用也是用來描述非必須物件的,但它的強度比軟引用更弱一些。只要垃圾收集器執行,軟引用所指向的物件就會被回收。

    弱引用通過WeakReference類實現。弱引用的生命週期比軟引用短。

  • 虛引用也叫幽靈引用,它和沒有引用沒有區別,無法通過虛引用訪問物件的任何屬性或函式。一個物件關聯虛引用唯一的作用就是在該物件被垃圾收集器回收之前會受到一條系統通知。

    虛引用通過PhantomReference類來實現。

2.3. 回收物件的過程

即使在可達性分析演算法中不可達的物件,也並不是一定會被清除的。要真正清除一個物件,至少要經歷兩次標記過程:

  1. 判斷該物件是否覆蓋了finalize()方法

    1. 若已覆蓋該方法,並該物件的finalize()方法還沒有被執行過,那麼就會將finalize()扔到F-Queue佇列中;
    2. 若未覆蓋該方法,則直接釋放物件記憶體;
  2. 執行F-Queue佇列中的finalize()方法

    虛擬機器會以較低的優先順序執行這些finalize()方法們,也不會確保所有的finalize()方法都會執行結束。如果finalize()方法中出現耗時操作,虛擬機器就直接停止執行,將該物件清除

  3. 物件重生或死亡

    如果在執行finalize()方法時,將this賦給了某一個引用,那麼該物件就重生了。如果沒有,那麼就會被垃圾收集器清除。

強烈不建議使用finalize()來進行任何操作,使用try-finally或者其他方式都可以做的更好、更及時。


3. 方法區的回收

3.1. 如何判定是否回收

方法區(或者說是HotSpot虛擬機器中的永久代)中進行垃圾收集的“價效比”一般比較低:在堆中,尤其是新生代中,常規應用進行一次垃圾收集一般可以回收70%左右的空間,而永久代的垃圾收集效率遠小於這個值。

方法區的垃圾收集主要收集兩部分內容:廢棄常量無用的類

判斷一個常量是否是“廢棄常量”比較簡單,沒有引用即可。但判斷一個類是否是“無用的類”的條件就就比較麻煩,有以下幾個條件:

  • 該類所有的例項都已經被回收,也就是java堆中不存在該類的任何例項
  • 載入該類的ClassLoader已經被回收
  • 該類對應的java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法

4. 垃圾收集演算法

4.1 標記 - 清除演算法

當堆中的有效記憶體空間(Available Memory)被耗盡的時候,就會停止整個程式(也被稱為Stop the World)。

它有兩個階段,標記和清除:

  1. 標記出所有需要回收的物件(遍歷所有的GC Roots,將可達的物件標為存活物件)。
  2. 清除的過程將遍歷堆中所有的物件,將沒有標記的物件全部清除掉。

不足之處:

  1. 效率問題,標記和清除兩個過程的效率都不高
  2. 空間問題,標記清除後會產生大量不連續的記憶體碎片,導致以後的使用不便。

4.2. 複製演算法

將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活的物件複製到另一塊上,然後再把已使用過的記憶體空間一次清理掉。

它有三個階段:

  1. 當需要回收垃圾時,標記出廢棄的資料。
  2. 將有用的資料複製到另一塊記憶體上。
  3. 將第一塊記憶體全部清除。

這種演算法的代價是將記憶體縮小為了原來的一般,空間利用率不高。

解決空間利用率問題:

新生代中的物件98%都是“朝生夕死”的,所以並不需要按照1:1的比例來劃分空間。我們可以將記憶體劃分成三塊:Eden、Survior1、Survior2,記憶體大小分別是8:1:1,每次使用Eden和其中一塊Servivor。當回收時,將Eden和Survivor中還存活著的物件一次性的複製到另外一塊Survivor空間上,最後清理掉Eden和剛剛用過的Survivor。

但如果出現物件申請的記憶體空間太大,Eden和Survivor加起來也存不下的情況,就需要依賴其他記憶體(這裡指老年代)進行分配擔保(Handle Promotion)。

4.3. 標記 - 整理演算法

在回收垃圾前,首先將所有廢棄的物件做上標記,然後將所有未標記的物件移到一邊,清空剩下的另一邊區域,有以下幾個過程:

  1. 標記出所有需要回收的物件(遍歷所有的GC Roots,將可達的物件標為存活物件)。
  2. 讓所有存活的第項都向一端移動,然後清理掉端邊界以外的記憶體。

它是一種老年代的垃圾收集演算法。老年代中的物件一般壽命比較長,因此每次垃圾回收會有大量物件存活,因此如果選用“複製”演算法,每次需要複製大量存活的物件,會導致效率很低。而且,在新生代中使用“複製”演算法,當Eden+Survior中都裝不下某個物件時,可以使用老年代的記憶體進行“分配擔保”,而如果在老年代使用該演算法,那麼在老年代中如果出現Eden+Survior裝不下某個物件時,沒有其他區域給他作分配擔保。因此,老年代中一般使用“標記-整理”演算法。

標記/整理演算法不僅可以彌補標記/清除演算法當中,記憶體區域分散的缺點,也消除了複製演算法當中,記憶體減半的高額代價,標記/整理演算法唯一的缺點就是效率也不高,不僅要標記所有存活物件,還要整理所有存活物件的引用地址。從效率上來說,標記/整理演算法要低於複製演算法。

4.4. 分代收集演算法

當前商業虛擬機器的垃圾收集都採用“分代收集”(Generational Collection)演算法,將記憶體劃分為老年代和新生代。老年代中存放壽命較長的物件,新生代中存放“朝生夕死”的物件。然後在不同的區域使用不同的垃圾收集演算法。

在新生代中選用複製演算法;在老年代中選擇“標記 - 清理”或者“標記 - 整理”演算法進行回收。

5. 小結

  1. 如何判定一個物件是否需要回收

    使用引用計數演算法或者可達性分析演算法

  2. 回收物件的過程

    檢視是否覆蓋了finalize()方法,執行F-Queue佇列中的finalize()方法,物件死亡或者重生

  3. 有哪幾種引用

    強引用,軟引用,弱引用,虛引用

  4. 分別描述下垃圾收集演算法

    標記 - 清理演算法

    複製演算法

    標記 - 整理演算法

    分代收集演算法