Java虛擬機8:垃圾收集(GC)-3(垃圾收集算法)

1.垃圾對象的判斷

 Java堆中存放着幾乎所有的對象實例,垃圾收集器對堆中的對象進行回收前,要先確定這些對象是否還有用,判定對象是否爲垃圾對象有如下算法:

 (1):引用計數算法

    給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1,當引用失效時,計數器值就減1,任何時刻計數器都爲0的對象就是不可能再被使用的。

    引用計數算法的實現簡單,判定效率也很高,在大部分情況下它都是一個不錯的選擇,當Java語言並沒有選擇這種算法來進行垃圾回收,主要原因是它很難解決對象之間的相互循環引用問題。

 看下面代碼:

複製代碼

/**
 * 虛擬機參數:-verbose:gc
 */
public class ReferenceCountingGC
{
    private Object instance = null;
    private static final int _1MB = 1024 * 1024;
    
    /** 這個成員屬性唯一的作用就是佔用一點內存 */
    private byte[] bigSize = new byte[2 * _1MB];
    
    public static void main(String[] args)
    {
        ReferenceCountingGC objectA = new ReferenceCountingGC();
        ReferenceCountingGC objectB = new ReferenceCountingGC();
        objectA.instance = objectB;
        objectB.instance = objectA;
        objectA = null;
        objectB = null;
        
        System.gc();
    }
}

複製代碼

 

看下運行結果:

[GC 4417K->288K(61440K), 0.0013498 secs]
[Full GC 288K->194K(61440K), 0.0094790 secs]

 

看到,兩個對象相互引用着,但是虛擬機還是把這兩個對象回收掉了,這也說明虛擬機並不是通過引用計數法來判定對象是否存活的。

(2):可達性分析法(又稱根搜索算法)

    爲了克服引用計數法的弊端,現在比較主流的實現算法是可達性分析算法。Java和C#中都是採用根搜索算法來判定對象是否存活的。這種算法的基本思路是通過一系列名爲“GC Roots”的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,就證明此對象是不可用的。

具體如圖:

在Java語言裏,可作爲GC Roots的對象包括下面幾種:

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象。
  • 方法區中的類靜態屬性引用的對象。
  • 方法區中的常量引用的對象。
  • 本地方法棧中JNI(Native方法)的引用對象。

2.引用判斷過程

判斷引用是否無效的過程分爲三個階段 
(1):當JVM進行垃圾收集時,JVM使用可達性分析算法進行分析,如果對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈,此時該對象將被第一次標記,並進行一次篩選,篩選的條件是此對象有沒有必要執行finalize()方法,如果對象沒有覆蓋該方法,或者該方法已經被虛擬機調用過了,虛擬機將這兩種情況都視爲“沒有必要執行”。 
(2):如果該對象被判定爲有必要執行finalize()方法,那麼對象將會被放置到一個叫做F-Queue的隊列中,並在稍後由一個由虛擬機自動建立的、低優先級的Finalizer線程去執行它。這裏所謂的執行是指虛擬機會觸發這個方法,但並不承若會等待它運行結束。因爲一個對象可能在finalize()方法中執行緩慢,或者發生了死循環,這將導致該隊列中的其他對象長期處於等待階段,甚至導致整個內存系統的奔潰。 
(3):F-Queue中的標記篩選。 
finalize()方法是對象逃脫死亡命運的最後一次機會,然後GC將對F-Queue中的對象進行第二次小規模的標記。如果對象在finalize()方法中成功拯救了自己,即與引用鏈上的任何一個對象建立關聯,那麼在第二次標記的時候,該算法將被移出F-Queue的集合,如果對象這個時候還沒有逃脫,那基本上它就真的被回收了。

3.對象的4種引用狀態

 在JDK1.2之前,Java中引用的定義很傳統:如果引用(reference)類型的數據中存儲的數值代表的是另外一塊內存的起始地址,就稱這塊內存代表着一個引用。這種定義很純粹,一個對象只有被引用或者沒被引用兩種狀態。我們希望描述這樣一類對象:當內存空間還足夠時,則能保留在內存中;如果內存空間在進行垃圾收集後還是非常緊張,則可以拋棄這些對象。很多系統的緩存功能都符合這樣的引用場景。在JDK1.2之後,Java對引用的概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種,引用強度依次減弱。

  • 強引用:如“Object obj = new Object()”,這類引用是Java程序中最普遍的。只要強引用還存在,垃圾收集器就永遠不會回收掉被引用的對象。
  • 軟引用:它用來描述一些可能還有用,但並非必須的對象。在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍進行二次回收。如果這次回收還沒有足夠的內存,纔會拋出內存溢出異常。Java中的類SoftReference類來表示軟引用。
  • 弱引用:它也是用來描述非需對象的,但它的強度比軟引用更弱些,被弱引用關聯的對象只能生存到下一次垃圾回收發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在JDK1.2之後,提供了WeakReference類來表示弱引用。
  • 虛引用:最弱的一種引用關係,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的唯一目的是希望能在這個對象被收集器回收時收到一個系統通知。JDK1.2之後提供了PhantomReference類來實現虛引用。

4.方法區回收

虛擬機規範中不要求方法區一定要實現垃圾回收,而且方法區中進行垃圾回收的效率也確實比較低,但是HotSpot對方法區也是進行回收的,主要回收的是廢棄常量和無用的類兩部分。判斷一個常量是否“廢棄常量”比較簡單,只要當前系統中沒有任何一處引用該常量就好了,但是要判定一個類是否“無用的類”條件就要苛刻很多,類需要同時滿足以下三個條件:

1、該類所有實例都已經被回收,也就是說Java堆中不存在該類的任何實例

2、加載該類的ClassLoader已經被回收

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

在大量使用反射、動態代理、CGLib等ByteCode框架、動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載功能,以保證方法區不會溢出。

5.垃圾收集算法

目前比較主流的垃圾收集算法有四種:標記-清除算法、複製算法、標記-整理算法、分代收集算法。

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

標記-清除算法最基礎的算法,分爲“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,標記完成後統一回收所有被標記的對象。

優點:簡單,易於實現。

缺點:主要體現在效率和空間,從效率的角度講,標記和清除兩個過程的效率都不高;從空間的角度講,標記清除後會產生大量不連續的內存碎片, 內存碎片太多可能會導致以後程序運行過程中在需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發一次垃圾收集動作。

標記-清除算法執行過程如圖:

 

(2):複製(Copying)算法

複製算法是爲了解決效率問題而出現的,它將可用的內存分爲大小相等的兩塊,每次只使用其中一塊,當這一塊內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已經使用過的內存空間一次性清理掉。這樣每次只需要對整個半區進行內存回收,內存分配時也不需要考慮內存碎片等複雜情況,只需要移動指針,按照順序分配即可。

優點:內存分配時算法不產生內存碎片。

缺點:空間消耗太大,內存被壓縮爲原來的一半。

複製算法的執行過程如圖:

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

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

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

優點:內存分配時算法不產生內存碎片,也比較易於實現。

缺點:算法複雜度大,執行步驟較多。

標記-整理算法的工作過程如圖:

(4):分代收集算法

根據上面的內容,用一張圖概括一下堆內存的佈局

現代商用虛擬機基本都採用分代收集算法來進行垃圾回收。這種算法其實是前三種算法的有機結合罷了,根據對象的生命週期的不同將內存劃分爲幾塊,然後根據各塊的特點採用最適當的收集算法。一般在Java堆中分爲兩大塊新生代和老年代。

    在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,選用:複製算法

    在老年代中,因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清除”或者“標記-整理”算法來進行回收

 

轉載地址:http://www.cnblogs.com/xrq730/p/4836700.html

http://blog.csdn.net/ns_code/article/details/18076173

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章