1. 程式人生 > >垃圾回收機制(GC) Java記憶體區域及物件

垃圾回收機制(GC) Java記憶體區域及物件

前言

  上一篇文章Java記憶體區域及物件講述了Java記憶體執行時的各個部分,其中程式計數器、虛擬機器棧、本地方法棧3個區域隨執行緒生而生,隨執行緒滅而滅,在這幾個區域是不需要過多的考慮回收的問題的,因為方法結束或者執行緒結束時,記憶體自然就跟隨著回收了;而Java堆和方法區則不一樣,一個介面中的多個實現類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也可能不一樣,我們只有在程式處於執行期間時才能知道會建立哪些物件,這部分記憶體的分配和回收都是動態的,垃圾收集器所關注的就是這部分的記憶體。

哪些物件需要回收

  在堆中存放著Java世界中幾乎所有的物件例項,垃圾收集器在對堆進行回收前,第一件事就是要確定這些物件之中哪些還“活”著,哪些已經“死去”(即不可能再被任何途徑使用的物件)了。

1.引用計數法

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

  客觀來說,引用計數演算法實現很簡單,判定效率也很高,在大部分的情況下它都是一個不錯的演算法,但是也有一個缺點就是:它很難解決物件之間相互迴圈引用的問題。

 1 /**
 2  * 虛擬機器引數:-verbose:gc
 3  */
 4 public class ReferenceCountingGC
 5 {
 6     private Object instance = null;
 7
private static final int _1MB = 1024 * 1024; 8 9 /** 這個成員屬性唯一的作用就是佔用一點記憶體 */ 10 private byte[] bigSize = new byte[2 * _1MB]; 11 12 public static void main(String[] args) 13 { 14 ReferenceCountingGC objectA = new ReferenceCountingGC(); 15 ReferenceCountingGC objectB = new
ReferenceCountingGC(); 16 objectA.instance = objectB; 17 objectB.instance = objectA; 18 objectA = null; 19 objectB = null; 20 21 System.gc(); 22 } 23 }

輸出結果:

[0.109s][info][gc] Using G1
[0.221s][info][gc] GC(0) Pause Full (System.gc()) 8M->1M(10M) 3.719ms

上述程式碼中objectB和objectA,只是在16、17行中相互引用,除此之外,再也沒有其他地方使用過。

從輸出結果看,雖然兩個物件相互引用著,但是虛擬機器還是把這兩個物件回收了,這說明了虛擬機器並不是使用的引用計數法來判定物件是否還活著的。

2.可達性分析

  可達性分析演算法:通過一系列的稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的。如下圖所示:

圖片中,object5、object6、object7雖然相互有關聯,但是他們到GC Roots是不可達的,所以他們將會判定為可回收物件。

在Java中,可作為GC Roots的物件包括下面幾種:

  ❤ 虛擬機器棧(棧幀中的本地變量表)中引用的物件;

  ❤ 方法區中類靜態屬性引用的物件;

  ❤ 方法區中常量引用的物件;

  ❤ 本地方法棧中JNI(即一般說的Native方法)引用的物件。

在瞭解垃圾回收機制之前,我們需要了解一些關於引用以及垃圾回收的一些常識:

引用:

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

  ❤ 強引用就是指在程式程式碼之中普遍存在的,類似“Object obj = new Object();”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的物件。

  ❤ 軟引用用來描述一些還有用但並非必需的物件。對於軟引用關聯著的物件,在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍中進行第二次回收。如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常,使用SoftReference類來實現軟引用。

  ❤ 弱引用也是用來描述非必需的物件,但是它的強度比軟引用更弱一些,被弱引用關聯的物件只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。使用WeakReference類來實現弱引用。

  ❤ 虛引用也稱為幽靈引用或者幻影引用,它時最弱的一種引用關係。一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個物件例項。為一個物件設定虛引用關聯的唯一目的就是能在這個物件被垃圾回收器回收時收到一個系統通知。使用PhantomReference類來實現虛引用。

生存還是死亡:

  即使在可達性演算法中不可達的物件,也並非是“非死不可”的,這時候它們暫時處於緩刑階段,要宣告一個物件死亡,至少需要經歷兩次的標記過程:如果物件在進行可達性分析之後發現沒有與GC Roots相連線的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此物件是否有必要執行finalize()方法,當物件沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機器呼叫過,虛擬機器將這兩種情況視為“沒有必要執行”。

  如果這個物件被判定為有必要執行finalize()方法,那麼這個物件將會放置在一個叫做F-Queue的佇列之中,並在稍後由一個虛擬機器自動建立的、低優先順序的Finalizer執行緒去執行它,這裡所說的“執行”是指虛擬機器會觸發這個方法,但並不會承諾等待它執行結束,這樣做的原因是,如果一個物件在finalize()方法中執行緩慢,或者發生了死迴圈,將很可能導致F-Queue佇列中的其他物件永久處於等待,甚至導致整個記憶體回收系統崩潰。finalize()方法是物件逃脫死亡的最後一次機會,稍後GC將會對F-Queue中的物件進行第二次小規模的標記,如果物件要在finalize()中成功解救自己-----只要重新與引用鏈上的任何一個物件建立關聯即可,比如把自己(this關鍵字)賦值給某個類變數或者物件的成員變數,那麼在第二次標記時它將會被移除“即將回收”的集合,如果物件這時候還沒有逃脫,那基本上就真的被回收了。

方法區的回收

  很多人認為方法區(HotSpot虛擬機器中的永久代)是沒有垃圾回收的,Java虛擬機器規範中確實說過可以不要求虛擬機器在方法區實現垃圾回收,而且在方法區中進行垃圾回收的“價效比”一般比較低:在堆中,尤其是在新生代中,常規應用進行一次垃圾回收一般可以收集70%~95%的空間,而永久代的垃圾回收效率遠低於此。

  永久代垃圾收集主要回收兩部分內容:廢棄常量和無用的類。

  廢棄常量:沒有任何物件引用常量池中的某個常量,同時也沒有其他地方引用了這個字面量,那麼這個常量就是廢棄常量。

  無用的類需要滿足以下三個條件:

    (1)該類的所有例項都已經被回收,也就是Java堆中不存在該類的任何例項;

    (2)載入該類的ClassLoader已經被回收;

    (3)該類對應的java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法;

垃圾回收演算法

1.標記 - 清除(Mark-Sweep)演算法

  這是最基礎的演算法,標記 - 清除演算法就如同它的名字樣,演算法分為標記和清除兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件。

標記 - 清除演算法的缺點:

  ❤ 標記和清除兩個過程的效率都不高;

  ❤ 標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次的垃圾收集動作;

標記 - 清除演算法的執行過程如下圖所示:

2.複製(Copying)演算法

  複製演算法是為了解決效率問題而出現的,它將可用的記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊,當這一塊的記憶體用完了,就將還存活的物件複製到另一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣使得每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動指標,按順序分配記憶體即可。

  複製演算法的執行過程如下所示:

複製演算法的優點是:實現簡單,執行高效;當然也有個缺點:記憶體縮小為原來的一半,代價比較高。

  現在的商業虛擬機器都採用這種演算法來回收新生代,不過研究表明1:1的比例非常不科學,因此新生代的記憶體被劃分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Suivivor。每次回收時,將Eden和Survivor中還存活著的物件一次性複製到另外一塊Survivor空間上,最後清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機器預設Eden區和Survivor區的比例是8:1,意思是每次新生代中可用記憶體空間為整個新生代容量的90%。當然,我們沒有辦法保證每次回收都只有不多於10%的物件存活,當Survivor空間不夠用時,需要依賴老年代進行分配擔保。

3.標記 - 整理(Mark - Compact)演算法

  複製演算法在物件存活率較高的場景下要進行大量的複製操作,效率很低。萬一物件100%存活,那麼需要有額外的空間進行分配擔保。老年代都是不易回收的物件,物件的存活率高,因此一般不能直接選用複製演算法。根據老年代的特點,提出了標記 - 整理演算法,過程和標記 - 清除演算法一樣,不過不是直接對可回收物件進行清理,而是讓所有存活的物件向一端移動,然後直接清理掉邊界以外的記憶體。標記 - 整理演算法的執行過程如下圖所示:

這種演算法的優點是:既沒有記憶體空間碎片,也沒有浪費50%的記憶體空間。

4.分代收集(Generational Collection)演算法

  用一張圖概括堆記憶體的佈局:

  現代商用虛擬機器基本都採用分代收集演算法來進行垃圾回收。這種演算法沒有什麼特別的,無非就是上面幾種演算法的結合罷了,根據物件的生命週期的不同將記憶體劃分為幾塊,然後根據各塊的特點採用最適合的收集演算法。大批的物件死去、少量存活的,使用複製演算法;物件存活率高,沒有額外空間進行擔保的,採用標記-清理或者標記-整理演算法。

GC的觸發時機

最後總結一下什麼時候會觸發一次GC,個人經驗看,有三種場景會觸發GC:

  1、第一種場景應該很明顯,當年輕代或者老年代滿了,Java虛擬機器無法再為新的物件分配記憶體空間了,那麼Java虛擬機器就會觸發一次GC去回收掉那些已經不會再被使用到的物件

  2、手動呼叫System.gc()方法,通常這樣會觸發一次的Full GC以及至少一次的Minor GC

  3、程式執行的時候有一條低優先順序的GC執行緒,它是一條守護執行緒,當這條執行緒處於執行狀態的時候,自然就觸發了一次GC了。

 參考:《深入理解Java虛擬機器》 周志明 編著: