1. 程式人生 > >垃圾收集器——判定回收

垃圾收集器——判定回收

     執行時資料區域除了按照執行緒私有公有劃分以外,還可以按照記憶體分配是否具有確定性來進行一個分類。其中程式計數器、虛擬機器棧和本地方法棧三個區域與執行緒生命週期相同。棧中的棧幀隨著方法的進入和退出而有條不紊的執行者出棧和入棧操作。每個棧幀中分配多大的記憶體基本上在類結構確定下來時就是已知的了(儘管編譯器會由JIT編譯器進行一些優化),因此這幾個區域的記憶體分配和回收都具備確定性,方法結束或者執行緒結束時,記憶體自然就被回收了。而Java堆和方法區則不一樣,一個介面中的多個實現類需要的記憶體可能是不一樣的一個方法中的多個分支需要的記憶體也是不一樣的,我們只有在程式處於執行期間時才能知道會建立哪些物件,這部分記憶體的分配和回收都是動態的,所以在講五大執行時資料區域時,Java堆為垃圾回收器管理的主要區域。

一、堆區的記憶體回收

     1. 判斷物件是否需要回收的兩種方法

     在堆裡面存放著Java中幾乎所有物件的例項,垃圾收集器在對堆進行回收前,首先會判斷哪些物件需要被回收,即不可能再被任何途徑使用的物件。目前有兩種判斷物件是否需要回收的方法:

     ① 引用計數演算法

     引用計數演算法是給物件添加了一個引用計數器,每當有一個地方引用它時,計數器的值就加1,當引用失效時,計數器的值就減1。任何時刻計數器為0的物件就表示暫時不可能再被使用的物件。這種判斷物件是否在被使用的方式雖然實現比較簡單,判斷效率也比較高,但在大多數主流的虛擬機器是沒有被採用的。其中最主要的原因是它很難解決物件之間的迴圈引用問題。例如objA和objB兩個物件都有欄位instance,賦值令objA.instance=objB及objB.instance=objB,就會造成兩個物件的引用計數器的值均不為0,所以造成物件無法被回收。

     ②可達性分析演算法

     在主流的商用程式語言,其實主要採用的是這種可達性分析演算法來判斷物件是否需要被回收。這個演算法的基本思路就是通過一系列的“GC Roots”的物件作為起始點,物件的每一次正常引用實際上就對應著一個以“GC Roots”為頭節點的掛鏈過程,相反引用失效時也就對應著一個斷鏈的過程。當需要進行垃圾回收時,GC就會以“GC Roots”物件作為起始點開始向下搜尋,搜尋走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈時,也就是該演算法所說的沒有可達路徑,則證明該物件是不可用的,也就是需要被回收的物件。

在Java語言中,GC Roots是一組必須活躍的引用。一般為全域性性引用(常量、類靜態屬性)和執行上下文(棧幀中的本地變量表)。可作為GC Roots的物件包括以下幾種:

  • 虛擬機器棧(棧幀中的本地變量表)中引用的物件,也就是當前正在被呼叫的方法的引用型別的引數、區域性變數以及臨時值。
  • 方法區中類靜態屬性引用的物件,也就是指向當前被載入的類的引用。
  • 方法區常量引用的物件,也就是常量池的引用型別常量。
  • 本地方法棧中JNI(即一般說的Native方法)引用的物件。

     2. dk1.2和jdk1.2之後版本的引用

     無論是採用引用計數演算法還是可達性分析演算法,判定物件存活都與“引用”有關。在jdk1.2之前,Java定義引用為reference型別的資料中儲存另一個物件的起始地址的物件。這種定義相對狹隘,一個物件在這種定義下只有被引用或者沒有被引用兩種狀態。但我們有時候希望一些物件在記憶體空間足夠時保留,在進行垃圾回收之後還是非常緊張的時候摒棄。實際上在很多作業系統中的快取功能都符合這種應用場景。所以在jdk1.2之後,Java對引用的概念進行了擴充,將其分為強引用、軟引用、弱引用以及虛引用4種,這4中引用強度一次逐漸減弱。

  • 強引用指程式中普遍存在的,類似“Object obj=new Object() "這類的引用,只要強引用還在,垃圾收集器就永遠不會回收掉被引用的物件;
  • 軟引用是用來描述一些還有用但並非必須的物件。對於軟引用關聯的物件,在系統將要發生溢位異常之前,將會把這些物件列入回收範圍之中準備進行第二次回收。如果兩次回收完成後還沒有足夠的記憶體,才會丟擲記憶體溢位異常。Java提供SoftReference型別來實現軟引用;
  • 弱引用也是用來描述非必須物件的,但它的強度比軟引用更軟弱一些,被弱引用關聯的物件只能生存到下一次垃圾回收發生之前。與軟引用不同的是,弱引用不會考慮第一次垃圾回收後記憶體是否夠用,也就是不管怎樣,進行第二次回收時,弱引用都將被回收。Java中提供了WeakReference類來實現弱引用;
  • 虛引用為最弱的一種引用關係,一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過一個虛引用來取得一個物件例項。而設定它的主要目的是能在這個物件被收集器回收時受到一個系統通知。Java提供了PhantomReference類來實現虛引用。

     3. 物件生存還是死亡的兩次決斷

     不管採用引用計數演算法還是可達性分析演算法,此時的物件都不能定性為必須回收不可。一個物件是否必須被回收需要進行兩次標記過程,第一次標記就是物件在進行可達性或者引用計數分析之後確定物件為不可用物件,第二次標記是判斷此物件是否有必要執行finalize()方法。當該物件沒有覆蓋finalize()或者finalize()方法已被虛擬機器呼叫過,虛擬機器將這兩種情況均視為”沒有必要執行“,這種情況下物件就直接宣佈回收死亡。

     如果一個物件被判定為有必要執行finalize()方法時,此時finalize()方法就成為了該物件的”救命稻草“。那麼這個物件將會被放置在一個F-Queue的佇列中,並在稍後由一個虛擬機器自動建立的、低優先順序的Finalizer執行緒去執行它。這裡的”執行“可能與其它的不同,它指的是虛擬機器會觸發這個方法,但不會等待它執行結束,這樣做的原因就是如果一個物件在finalize方法中執行緩慢,或者發生死迴圈,將可能導致F-Qunue佇列中其它物件永久處於等待狀態,甚至導致整個記憶體回收系統崩潰。前面我們提到,執行finalize()方法是一個物件的”救命稻草“,在執行finalize()方法中,只要重新與引用鏈上的任何一個引用關聯,那在第二次標記時該物件將會被移除”即將回收“的集合。相反,若finalize()方法未能拯救它,它也將直接宣佈回收死亡。但必須注意的是,finalize()方法只能被該物件呼叫一次,也就是該物件被判定可回收之後,只有一次救命的機會。如以下程式碼示例:

測試結果如下:

     finalize()在很容易讓人理解為C/C++中的解構函式,但其實不然。它只是Java剛剛誕生為了使C/C++程式設計師更容易理接受它所做出的一個妥協。在Java中一般建議儘量避免使用finalize方法,它的執行代價高昂,不確定性大,無法保證各個物件的呼叫順序。

二、方法區的記憶體回收

     很多人認為方法區是沒有垃圾收集的,特別是HotSpot虛擬機器的永久代,對永久代進行垃圾收集的效率非常低。永久代的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。回收廢棄常量池與回收Java堆中的物件非常類似。以常量池中字面量的回收為例,例如一個字串”abc“已經進入常量池中,但沒有任何String物件引用常量池中的”abc“常量,也沒有其它地方引用這個字面量,如果這時發生記憶體回收,而且必要的話,這個”abc“常量就會被系統清理出常量池。常量池中的其他類(介面)、方法、欄位的符號引用也與此類似。判斷一個常量是否為”廢棄常量“比較簡單,而判定一個類是否是”無用的類“的條件則相對苛刻。

類需要同時滿足下面三個條件才能算是”無用的類“:

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

     滿足上述3個條件的類只是被判定為可以被虛擬機器回收,而不是和物件一樣,兩次判定完成之後就必然會回收。是否對類進行回收,還需要對虛擬機器進行相應的引數設定。在HotSpot中,虛擬機器提供-Xnoclassgc引數進行控制,還可以使用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading檢視類載入和解除安裝資訊,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虛擬機器中使用,-XX:+TraceClassUnLoading引數需要FastDebug版的虛擬機器支援。在大量使用反射、動態代理、CGLib等ByteCode框架、動態生成JSP以及OSGI這類頻繁自定義ClassLoader的場景都需要虛擬機器具備類解除安裝功能,以保證永久代不會溢位。