1. 程式人生 > >深入理解Java虛擬機之垃圾收集一

深入理解Java虛擬機之垃圾收集一

native 直觀 軟引用 老年 系統清理 邊界 lan 除了 每次

“生存還是死亡”

如何來判定對象是否存活?針對這個問題書中給出了兩種算法,分別是引用計數算法和可達性分析算法

引用計數算法

該算法的思路簡單並且易於實現。我們給對象中添加一個引用計數器,當有一個地方引用它時,引用計數器就加一,當引用失效時,計數器減一,當計數器為0時就說明該對象不可能再被引用。
客觀的評價,該算法判定效率很高,在很多情況下都是一種不錯的算法,但是,至少主流的Java虛擬機並沒有采用采用這種算法。原因是該算法無法解決對象之間的循環引用問題。
什麽是循環引用呢?筆者認為就是對象之間的嵌套的引用,將對象B賦值給對象A中的字段instance,再將對象A賦值給對象B中的字段instance,這樣就形成了嵌套的互相的引用關系,則引用計數器永遠也不會為0,根據算法規定,兩個對象自然也就不會被回收,但事實並不是這樣,虛擬機成功的回收了兩個對象(具體可以參考《深入理解Java虛擬機》63頁的代碼)。

可達性分析算法

技術分享圖片
從圖中可以直觀的看出,判斷對象是否存活可以檢查該對象是否與GC Roots關聯,或者說是從GC Roots出發,到對象是否存在一條路徑,該路徑也被稱作引用鏈(Reference Chain)。如果可達,那麽該對象可引用,如果對象不可達,那麽該對象不可引用。
在Java語言中,可作為GC Roots的對象包括以下幾種:

  1. 虛擬機棧(棧幀中的本地變量表)中引用的對象
  2. 方法區中類靜態屬性引用的對象
  3. 方法區中常量引用的對象
  4. 本地方法棧中JNI(即一般說的Native方法)引用的對象

再探引用:

Java中的引用關系原本只有兩種,引用和未被引用,這種定義是狹隘的,當我們需要描述像”雞肋“這樣的對象時,這種描述顯得無能為力,因此我們引入了其他幾種引用:

  1. 強引用(只要強引用依然存在,垃圾收集器就不會回收掉被引用的對象)
  2. 軟引用(在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行第二次回收)
  3. 弱引用(弱引用關聯的對象只能生存到下一次垃圾收集發生之前)
  4. 虛引用(一個對象是否由虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例)

對象的死亡

在可達性分析算法中不可達,或者沒有引用鏈的對象並不會立即死亡,要正式宣告對象的死亡,需要兩個階段:
第一個階段,在可達性分析算法中被發現沒有引用鏈,那麽該對象將會被第一次標記並且進行一次篩選。下面我們來看一下篩選的過程,篩選的條件是該對象是否有必要執行finalize()方法。
當對象沒有覆蓋finalize()方法或者finalize()方法已被虛擬機調用過,那麽,虛擬機將這兩種情況視為”沒有必要執行“。
當對象被判定為有必要執行finalize()方法,那麽該對象將會被放置在一個叫做Queue的隊列之中,並在稍後由一個虛擬機自動建立的、低優先級的Finalizer線程去執行(執行並不以為著虛擬機會等待它的結束),接著(第二個階段),GC將會對F-Queue隊列中的對象進行第二次小規模的標記,如果對象在finalize()方法中成功的進行引用(和引用鏈上的任何一個對象建立關聯),那麽該對象將會被移出”即將回收“集合,反之,該對象將會被GC回收。

建議讀者參考周誌明老師的《深入理解java虛擬機》67頁代碼

回收方法區

方法區(永久代)的垃圾收集主要回收兩部分內容:廢棄常量和無用的類
我們先以一個實例來考察一下廢棄常量的回收過程:

假如一個字符串"Hello World!"已經進入了常量池中,但是當前系統沒有任何一個String對象是叫做"Hello World!"的,換句話說,也就是沒有一個String對象引用常量池中的”Hello World!"常量,也沒有其他地方引用這個字面量,那麽這時如果發生內存回收,這個常量就會被系統清理出常量池。

下面我們來看一下如何判定一個類是否是“無用的類”

  1. 該類所有的實例已經被回收,也就是Java堆中不存在該類的任何實例
  2. 加載該類的ClassLoader已經被回收
  3. 該類對應的java.lang.class對象沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法
常量池中的其他類(接口)、方法、字段的符號引用也與此類似
虛擬機可以對滿足上述三個條件的無用類進行回收,但並不是和對象一樣,不使用了就必然會回收,是否對類進行回收,虛擬機給出了相關的參數進行調節

垃圾收集算法

技術分享圖片

字不重要,主要看圖↑

上圖演示了標記-清除算法
顧名思義,標記-清除算法分為兩個階段:標記階段和清除階段
標記階段前文已經介紹過,讀者可以查看上文;清除階段即清除已被標記了的對象
標記-清除算法的缺點主要由兩個,第一:該算法標記和清除的兩個過程的效率都不高;第二:該算法清除了可回收對象後會產生不連續的內存空間,在分配大的連續的對象時,就需要提前觸發一次垃圾收集動作。

技術分享圖片

字不重要,主要看圖↑

上圖演示了復制算法
復制算法的基本思想是將內存空間分為大小相同的兩部分,當前這半個內存用完時,將其中存活的對象復制到另一半內存中,然後一次性清理掉當前這一半內存空間。
該算法完美的規避了標記-清除算法的缺點,同時也浪費了大量的內存空間
補充
HotSpot虛擬機是將內存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和其中一塊Survivor,當回收時,將Eden和Survivor中還存活著的對象一次性復制到另外一塊Survivor空間,最後清理掉Eden和Survivor空間。HotSpot虛擬機采用分配的比例為8:1,也就是每次有效使用的空間為90%
當Survivor空間不足呢?虛擬機是如何處理的?這裏會涉及到一個分配擔保的問題(類似於銀行貸款的數學模型),我們在下一篇博文中會詳細介紹。

技術分享圖片

字不重要,主要看圖↑

上圖演示了標記-整理算法
標記-整理算法的提出和老年代有關。整個算法的執行過程和標記-清除算法一樣,但是後續的步驟不是直接對已標記的對象進行清理,而是讓所有存活的對象都向一邊移動,接著直接清理掉端邊界以外的內存。

分代收集算法

該算法汲取了復制算法和標記-整理算法的長處。我們將Java堆分為新生代和老年代,這樣就可以根據各個年代的特點來選擇算法:

新生代選擇復制算法,老年代選擇標記-整理算法。

HotSpot虛擬機如何發起內存回收?這個問題以後我會專門發一篇博文來進行簡單的介紹,這裏不詳細介紹!

本文版權歸作者0xTsmon和博客園所有,歡迎轉載和商用,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利.

深入理解Java虛擬機之垃圾收集一