淺談java垃圾回收機制

1、問題

  筆者最近遇到超級多的關於java中垃圾回收機制的問題,因此特意寫一遍博客來和你們交流一下java中的垃圾回收究竟是什麼鬼。所謂垃圾回收即便jvm以爲你這個對象沒有存在的必要,將你清理出去,那麼問題來了。java

  1. 如何肯定某個對象是須要被回收?
  2. 典型的垃圾收集算法,是怎麼回收對象的?
  3. 典型的垃圾收集器有哪些?

  下面我來一個一個看問題算法

2、如何肯定某個對象是須要被回收的

  這裏咱們先了解一個的問題:若是肯定某個對象是「垃圾」?既然垃圾收集器的任務是回收垃圾對象所佔的空間供新的對象使用,那麼垃圾收集器如何肯定某個對象是「垃圾」?—即經過什麼方法判斷一個對象能夠被回收了。有些對象是jvm內存不足須要清理內存空間,會將下一輪須要回收的對象進行清理。數組

  在java中是經過引用來和對象進行關聯的,也就是說若是要操做對象,必須經過引用來進行。那麼很顯然一個簡單的辦法就是經過引用計數來判斷一個對象是否能夠被回收。不失通常性,若是一個對象沒有任何引用與之關聯,則說明該對象基本不太可能在其餘地方被使用到,那麼這個對象就成爲可被回收的對象了。這種方式成爲引用計數法。網絡

  這樣的方法簡單粗暴,並且效率很高。效率高必然會暴露一些問題,若是某些對象唄循環引用,即便你把對象賦值爲null,這種算法照樣不能回收。看下下面的代碼多線程

public class GcTest {

    public Object object = null;
    
    public static void main(String[] args) {
        
        GcTest gcTest1 = new GcTest();
        GcTest gcTest2 = new GcTest();
        
        gcTest1.object = gcTest1;
        gcTest2.object = gcTest2;
        
        gcTest1 = null;
        gcTest2 = null;
    }
}

 

  雖然gcTest1,gcTest2是null,他們指向的對象已經不會被訪問到了,可是因爲它們互相引用對方,致使它們的引用計數都不爲0,那麼垃圾收集器就永遠不會回收它們。併發

  上面的問題已經暴露出來了,下面看看jvm是怎麼解決這個問題的。爲了解決這個問題,在Java中採起了可達性分析法。該方法的基本思想是經過一系列的「GC Roots」對象做爲起點進行搜索,若是在「GC Roots」和一個對象之間沒有可達路徑,則稱該對象是不可達的,不過要注意的是被斷定爲不可達的對象不必定就會成爲可回收對象。被斷定爲不可達的對象要成爲可回收對象必須至少經歷兩次標記過程,若是在這兩次標記過程當中仍然沒有逃脫成爲可回收對象的可能性,則基本上就真的成爲可回收對象了。在《深刻理解jvm》講解的很仔細,筆者就簡單介紹下GC Roots的概念,想深刻了解的能夠去讀下筆者介紹的這本書。jvm

  如下三類對象在jvm中做爲GC roots,來判斷一個對象是否能夠被回收 (一般來講咱們只要知道虛擬機棧和靜態引用就夠了)spa

   一、虛擬機棧(JVM stack)中引用的對象(準確的說是虛擬機棧中的棧幀(frames)) 。咱們知道,每一個方法執行的時候,jvm都會建立一個相應的棧幀(棧幀中包括操做數棧、局部變量表、運行時常量池的引用),棧幀中包含這在方法內部使用的全部對象的引用(固然還有其餘的基本類型數據),當方法執行完後,該棧幀會從虛擬機棧中彈出,這樣一來,臨時建立的對象的引用也就不存在了,或者說沒有任何gc roots指向這些臨時對象,這些對象在下一次GC時便會被回收掉線程

  二、方法區中類靜態屬性引用的對象 。靜態屬性是該類型(class)的屬性,不單獨屬於任何實例,所以該屬性天然會做爲gc roots。只要這個class存在,該引用指向的對象也會一直存在。class 也是會被回收的,在面後說明3d

   三、本地方法棧(Native Stack)引用的對象

   下面介紹下關於軟引用(softReference)和弱引用(weakReference)的對象垃圾回收對他們作的處理

String str = new String("hello");//A
SoftReference<String> sr = new SoftReference<String>(new String("java"));//B
WeakReference<String> wr = new WeakReference<String>(new String("world"));//C

   上面的幾個對象中回收狀況以下,B在內存不足的狀況下會將String對象斷定爲可回收對象,C不管什麼狀況下String對象都會被斷定爲可回收對象。也就是說軟引用會在內存溢出(OOM)的時候回收,而弱引用不管什麼狀況都會在下一輪迴收的時候回收掉。

  通常jvm會對這些對象回收

  一、顯示地將某個引用賦值爲null或者將已經指向某個對象的引用指向新的對象。

  二、局部引用所指向的對象。

  三、上面說的弱引用(weakReference)。

3、垃圾收集算法

  在肯定了哪些垃圾能夠被回收後,垃圾收集器要作的事情就是開始進行垃圾回收,可是這裏面涉及到一個問題是:如何高效地進行垃圾回收。因爲Java虛擬機規範並無對如何實現垃圾收集器作出明確的規定,所以各個廠商的虛擬機能夠採用不一樣的方式來實現垃圾收集器,就以最經常使用的HotShot爲例,因此在此只討論幾種常見的垃圾收集算法的核心思想。

一、Mark-Sweep(標記-清除)算法

  這是最基礎的垃圾回收算法,之因此說它是最基礎的是由於它最容易實現,思想也是最簡單的。標記-清除算法分爲兩個階段:標記階段和清除階段。標記階段的任務是標記出全部須要被回收的對象,清除階段就是回收被標記的對象所佔用的空間。圖解來自網絡,很好的說明了標記-清楚算法的處理前和處理後的內存分佈。

下面全部的圖是模擬內存塊,紅色爲未使用內存塊,灰色爲待回收對象內存塊,黃色爲存活對象

回收以前

回收以後

  很容易看出這樣的操做是有弊端的,這樣講標記的對象的清楚後,內存塊就變的零零散散,若是如今有一個對象佔用的內存很大,這個時候必需要在執行一遍垃圾回收,爲這個大的對象騰出空間。

二、Copying(複製)算法

  爲了解決Mark-Sweep算法的缺陷,Copying算法就被提了出來。它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。

回收以前

回收以後

  複製算法會提早空出通常的內存,在垃圾回收的時候將存活的對象移動的另一半內存,這樣內存的移動消耗太大,雖然內存不是零散的,可是代價過高。

三、Mark-Compact(標記-整理)算法

  爲了解決Copying算法的缺陷,充分利用內存空間,提出了Mark-Compact算法。該算法標記階段和Mark-Sweep同樣,可是在完成標記以後,它不是直接清理可回收對象,而是將存活對象都向一端移動,而後清理掉端邊界之外的內存。具體過程以下圖所示:

回收以前

回收以後

四、Generational Collection(分代收集)算法

  分代收集算法是目前大部分JVM的垃圾收集器採用的算法。它的核心思想是根據對象存活的生命週期將內存劃分爲若干個不一樣的區域。通常狀況下將堆區劃分爲老年代(Tenured Generation)和新生代(Young Generation),老年代的特色是每次垃圾收集時只有少許對象須要被回收,並非回收全部,而新生代的特色是每次垃圾回收時都有大量的對象須要被回收,那麼就能夠根據不一樣代的特色採起最適合的收集算法。能夠調用System.gc()方法查看回收狀況。

  目前大部分垃圾收集器對於新生代都採起Copying算法,由於新生代中每次垃圾回收都要回收大部分對象,也就是說須要複製的操做次數較少,可是實際中並非按照1:1的比例來劃分新生代的空間的,通常來講是將新生代劃分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,將Eden和Survivor中還存活的對象複製到另外一塊Survivor空間中,而後清理掉Eden和剛纔使用過的Survivor空間。

  而因爲老年代的特色是每次回收都只回收少許對象,通常使用的是Mark-Compact算法。

  注意,在堆區以外還有一個代就是永久代(Permanet Generation),它用來存儲class類、常量、方法描述等。對永久代的回收主要回收兩部份內容:廢棄常量和無用的類。

3、典型的垃圾收集器

  下面都是些機率性的東西,筆者看得也似懂非懂,直接搬過來分享給你們

1.Serial/Serial Old

  Serial/Serial Old收集器是最基本最古老的收集器,它是一個單線程收集器,而且在它進行垃圾收集時,必須暫停全部用戶線程。Serial收集器是針對新生代的收集器,採用的是Copying算法,Serial Old收集器是針對老年代的收集器,採用的是Mark-Compact算法。它的優勢是實現簡單高效,可是缺點是會給用戶帶來停頓。

2.ParNew

  ParNew收集器是Serial收集器的多線程版本,使用多個線程進行垃圾收集。

3.Parallel Scavenge

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

4.Parallel Old

  Parallel Old是Parallel Scavenge收集器的老年代版本(並行收集器),使用多線程和Mark-Compact算法。

5.CMS

  CMS(Current Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器,它是一種併發收集器,採用的是Mark-Sweep算法。

6.G1

  G1收集器是當今收集器技術發展最前沿的成果,它是一款面向服務端應用的收集器,它能充分利用多CPU、多核環境。所以它是一款並行與併發收集器,而且它能創建可預測的停頓時間模型。

4、總結和補充

  對象的內存分配,往大方向上講就是在堆上分配,對象主要分配在新生代的Eden Space和From Space,少數狀況下會直接分配在老年代。若是新生代的Eden Space和From Space的空間不足,則會發起一次GC,若是進行了GC以後,Eden Space和From Space可以容納該對象就放在Eden Space和From Space。在GC的過程當中,會將Eden Space和From  Space中的存活對象移動到To Space,而後將Eden Space和From Space進行清理。若是在清理的過程當中,To Space沒法足夠來存儲某個對象,就會將該對象移動到老年代中。在進行了GC以後,使用的即是Eden space和To Space了,下次GC時會將存活對象複製到From Space,如此反覆循環。當對象在Survivor區躲過一次GC的話,其對象年齡便會加1,默認狀況下,若是對象年齡達到15歲,就會移動到老年代中。

  通常來講,大對象會被直接分配到老年代,所謂的大對象是指須要大量連續存儲空間的對象,最多見的一種大對象就是大數組,好比:

  byte[] data = new byte[4*1024*1024]

  這種通常會直接在老年代分配存儲空間。

  固然分配的規則並非百分之百固定的,這要取決於當前使用的是哪一種垃圾收集器組合和JVM的相關參數。

相關文章
相關標籤/搜索