Java基礎:JVM垃圾回收算法

衆所周知,Java的垃圾回收是不須要程序員去手動操控的,而是由JVM去完成。本文介紹JVM進行垃圾回收的各類算法。
html

1. 如何肯定某個對象是垃圾

1.1. 引用計數法

在Java中,引用和對象是有關聯的。若是要操做對象則必須用引用進行。所以,很顯然一個簡單的辦法是經過引用計數來判斷一個對象是否能夠回收。簡單說,即一個對象若是沒有任何與之關聯的引用,則說明對象不太可能再被用到,那麼這個對象就是可回收對象。這種方式便是引用計數法。這種方式的問題是沒法解決循環引用的問題,舉個例子:java

public static void main(String[] args){
    Object object1=new Object();
    Object object2=new Object();
    object1.object=object2;
    object2.object=object1;
    object1=null;
    object2=null;
}

顯然,在最後,object1和object2的內存塊都不能再被訪問到了,但他們的引用計數都不爲0,這就會使他們永遠不會被清除。程序員

1.2. 可達性分析

爲了解決引用計數法的循環引用問題,Java使用了可達性分析的方法。經過一系列的「GC roots」對象做爲起點搜索。若是在「GC roots」和一個對象之間沒有可達路徑,則稱該對象是不可達的。要注意的是,不可達對象不等價於可回收對象,不可達對象變爲可回收對象至少要通過兩次標記過程。兩次標記後仍然是可回收對象,則將面臨回收。算法

所謂「GC roots」,或者說tracing GC的「根集合」,就是一組必須活躍的引用。例如說,這些引用可能包括:數據結構

  • 全部Java線程當前活躍的棧幀裏指向GC堆裏的對象的引用;換句話說,當前全部正在被調用的方法的引用類型的參數/局部變量/臨時值。
  • VM的一些靜態數據結構裏指向GC堆裏的對象的引用,例如說HotSpot VM裏的Universe裏有不少這樣的引用。
  • JNI handles,包括global handles和local handles(看狀況)
  • 全部當前被加載的Java類(看狀況)
  • Java類的引用類型靜態變量(看狀況)
  • Java類的運行時常量池裏的引用類型常量(String或Class類型)(看狀況)
  • String常量池(StringTable)裏的引用

比較常見的將對象視爲可回收對象的緣由:多線程

  • 顯式地將對象的惟一強引用指向新的對象。
  • 顯式地將對象的惟一強引用賦值爲Null。
  • 局部引用所指向的對象(如,方法內對象)。

下述代碼中,每次循環結束,object都會被視爲可回收對象。併發

void fun() {
 
.....
    for(int i=0;i<10;i++) {
        Object obj = new Object();
        System.out.println(obj.getClass());
    }   
}
  • 只有弱引用與其關聯的對象

2. 典型的垃圾回收算法

在JVM規範中並無明確GC的運做方式,各個廠商能夠採用不一樣的方式去實現垃圾回收器。這裏討論幾種常見的GC算法。線程

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

最基礎的垃圾回收算法,分爲兩個階段,標註和清除。標記階段標記出全部須要回收的對象,清除階段回收被標記的對象所佔用的空間。如圖:code

從圖中咱們就能夠發現,該算法最大的問題是內存碎片化嚴重,後續可能發生大對象不能找到可利用空間的問題。htm

2.2. 複製算法(Copying)

爲了解決Mark-Sweep算法內存碎片化的缺陷而被提出的算法。按內存容量將內存劃分爲等大小的兩塊。每次只使用其中一塊,當這一塊內存滿後將尚存活的對象複製到另外一塊上去,把已使用的內存清掉,如圖:

這種算法雖然實現簡單,內存效率高,不易產生碎片,可是最大的問題是可用內存被壓縮到了本來的一半。且存活對象增多的話,Copying算法的效率會大大下降。

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

結合了以上兩個算法,爲了不缺陷而提出。標記階段和Mark-Sweep算法相同,標記後不是清理對象,而是將存活對象移向內存的一端。而後清除端邊界外的對象。如圖:

2.4. 分代收集算法(Generational Collection)

分代收集法是目前大部分JVM所採用的方法,其核心思想是根據對象存活的不一樣生命週期將內存劃分爲不一樣的域,通常狀況下將GC堆劃分爲老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特色是每次垃圾回收時只有少許對象須要被回收,新生代的特色是每次垃圾回收時都有大量垃圾須要被回收,所以能夠根據不一樣區域選擇不一樣的算法。

目前大部分JVM的GC對於新生代都採起Copying算法,由於新生代中每次垃圾回收都要回收大部分對象,即要複製的操做比較少,但一般並非按照1:1來劃分新生代。通常將新生代劃分爲一塊較大的Eden空間和兩個較小的Survivor空間(From Space, To Space),每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,將該兩塊空間中還存活的對象複製到另外一塊Survivor空間中。

而老生代由於每次只回收少許對象,於是採用Mark-Compact算法。

另外,不要忘記在Java基礎:Java虛擬機(JVM)中提到過的處於方法區的永生代(Permanet Generation)。它用來存儲class類,常量,方法描述等。對永生代的回收主要包括廢棄常量和無用的類。

對象的內存分配主要在新生代的Eden Space和Survivor Space的From Space(Survivor目前存放對象的那一塊),少數狀況會直接分配到老生代。當新生代的Eden Space和From Space空間不足時就會發生一次GC,進行GC後,Eden Space和From Space區的存活對象會被挪到To Space,而後將Eden Space和From Space進行清理。若是To Space沒法足夠存儲某個對象,則將這個對象存儲到老生代。在進行GC後,使用的即是Eden Space和To Space了,如此反覆循環。當對象在Survivor區躲過一次GC後,其年齡就會+1。默認狀況下年齡到達15的對象會被移到老生代中。

3. 典型的垃圾收集器

垃圾收集算法是垃圾收集器的理論基礎,而垃圾收集器就是其具體實現。下面介紹HotSpot虛擬機提供的幾種垃圾收集器。

3.1. Serial/Serial Old

最古老的收集器,是一個單線程收集器,用它進行垃圾回收時,必須暫停全部用戶線程。Serial是針對新生代的收集器,採用Copying算法;而Serial Old是針對老生代的收集器,採用Mark-Compact算法。優勢是簡單高效,缺點是須要暫停用戶線程。

3.2. ParNew

Seral/Serial Old的多線程版本,使用多個線程進行垃圾收集。

3.3. Parallel Scavenge

新生代的並行收集器,回收期間不須要暫停其餘線程,採用Copying算法。該收集器與前兩個收集器不一樣,主要爲了達到一個可控的吞吐量。

3.4. Parallel Old

Parallel Scavenge的老生代版本,採用Mark-Compact算法和多線程。

3.5. CMS

Current Mark Sweep收集器是一種以最小回收時間停頓爲目標的併發回收器,於是採用Mark-Sweep算法。

3.6. G1

G1(Garbage First)收集器技術的前沿成果,是面向服務端的收集器,能充分利用CPU和多核環境。是一款並行與併發收集器,它可以創建可預測的停頓時間模型。

4. 參考文章

Java垃圾回收機制
java的gc爲何要分代? - RednaxelaFX的回答 - 知乎

相關文章
相關標籤/搜索