筆者最近遇到超級多的關於java中垃圾回收機制的問題,所以特地寫一遍博客來和大家交流一下java中的垃圾回收到底是什麼鬼。所謂垃圾回收即使jvm覺得你這個物件沒有存在的必要,將你清理出去,那麼問題來了。
如何決定某個物件是需要被回收?
典型的垃圾收集演算法,是怎麼回收物件的?
典型的垃圾收集器有哪些?
下面我來一個一個看問題
這裡我們先了解一個的問題:如果確定某個物件是「垃圾」?既然垃圾收集器的任務是回收垃圾物件所佔的空間供新的物件使用,那麼垃圾收集器如何確定某個物件是「垃圾」? —即透過什麼方法判斷一個物件可以被回收了。有些物件是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中作為GC roots,來判斷一個物件是否可以被回收(通常來說我們只要知道虛擬機器棧和靜態引用就夠了)
1、虛擬機器堆疊(JVM stack)中所引用的物件(準確的說是虛擬機器堆疊中的堆疊幀(frames)) 。我們知道,每個方法執行的時候,jvm都會創建一個對應的棧幀(棧幀中包含操作數棧、局部變數表、運行時常數池的參考),棧幀中包含這在方法內部使用的所有物件的參考(當然還有其他的基本型別資料),當方法執行完後,該棧幀會從虛擬機棧中彈出,這樣一來,臨時創建的對象的引用也就不存在了,或者說沒有任何gc roots指向這些臨時對象,這些對像在下一次GC時便會被回收掉
# 2、方法區中類別靜態屬性所引用的對象。靜態屬性是該類型(class)的屬性,不單獨屬於任何實例,因此該屬性自然會作為gc roots。只要這個class存在,該引用所指向的物件也會一直存在。 class 也是會被回收的,在面後說明
3、本地方法堆疊(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會對這些物件回收
1、顯示地將某個引用賦值為null或是將已經指向某個物件的參考指向新的物件。
2、局部引用所指向的物件。
3、上面說的弱引用(weakReference)。
在確定了哪些垃圾可以被回收後,垃圾收集器要做的事情就是開始進行垃圾回收,但是這裡面涉及到一個問題是:如何有效率地進行垃圾回收。由於Java虛擬機器規範並沒有對如何實作垃圾收集器做出明確的規定,因此各個廠商的虛擬機可以採用不同的方式來實作垃圾收集器,就以最常用的HotShot為例,所以在此只討論幾種常見的垃圾收集演算法的核心思想。
這是最基礎的垃圾回收演算法,之所以說它是最基礎的是因為它最容易實現,想法也是最簡單的。標記-清除演算法分為兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所佔用的空間。圖解來自網絡,很好的說明了標記-清楚演算法的處理前和處理後的記憶體分佈。
下面所有的圖是模擬記憶體區塊,紅色為未使用記憶體區塊,灰色為待回收物件記憶體區塊,黃色為存活物件
回收之前
回收之後
# 很容易看出這樣的操作是有弊端的,這樣講標記的物件的清楚後,記憶體區塊就變的零零散散,如果現在有一個物件佔用的記憶體很大,這個時候必須要在執行一遍垃圾回收,為這個大的物件騰出空間。
為了解決Mark-Sweep演算法的缺陷,Copying演算法就被提了出來。它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用的記憶體空間一次清理掉,這樣一來就不容易出現記憶體碎片的問題。
回收之前
#回收之後
# 複製演算法會提前空出一般的內存,在垃圾回收的時候將存活的物件移動的另外一半內存,這樣內存的移動消耗太大,雖然內存不是零散的,但是代價太高。
為了解決Copying演算法的缺陷,充分利用記憶體空間,提出了Mark-Compact演算法。這個演算法標記階段和Mark-Sweep一樣,但是在完成標記之後,它不是直接清理可回收對象,而是將存活對像都向一端移動,然後清理掉端邊界以外的記憶體。具體流程如下圖所示:
回收前
回收之後
##4、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類別、常數、方法描述等。永久代的回收主要回收兩部分:廢棄常量和無用的類。
下面都是些概率性的東西,筆者看得也似懂非懂,直接搬過來分享給大家
Serial/Serial Old收集器是最基本最古老的收集器,它是一個單線程收集器,並且在它進行垃圾收集時,必須暫停所有用戶線程。 Serial收集器是針對新生代的收集器,採用的是Copying演算法,Serial Old收集器是針對老年代的收集器,採用的是Mark-Compact演算法。它的優點是實現簡單高效,但是缺點是會為使用者帶來停頓。
ParNew收集器是Serial收集器的多執行緒版本,使用多個執行緒進行垃圾收集。
Parallel Scavenge收集器是一個新生代的多線程收集器(並行收集器),它在回收期間不需要暫停其他用戶線程,其採用的是Copying演算法,該收集器與前兩個收集器有所不同,它主要是為了達到一個可控的吞吐量。
Parallel Old是Parallel Scavenge收集器的老年代版本(平行收集器),使用多執行緒和Mark-Compact演算法。
CMS(Current Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器,它是一種並發收集器,採用的是Mark -Sweep演算法。
G1收集器是當今收集器技術發展最前沿的成果,它是一款面向服務端應用的收集器,它能充分利用多CPU、多核心環境。因此它是一款並行與並發收集器,並且它能建立可預測的停頓時間模型。
物件的記憶體分配,往大方向上講就是在堆上分配,物件主要分配在新生代的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的相關參數。
以上是java垃圾回收機制是怎麼一回事?的詳細內容。更多資訊請關注PHP中文網其他相關文章!