淺談java記憶體回收機制,淺談java記憶體回收
一、問題
筆者最近遇到超級多的關於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");//ASoftReference<String> sr = new SoftReference<String>(new String("java"));//BWeakReference<String> wr = new WeakReference<String>(new String("world"));//C
上面的幾個對象中回收情況如下,B在記憶體不足的情況下會將String對象判定為可回收對象,C無論什麼情況下String對象都會被判定為可回收對象。也就是說軟引用會在記憶體溢出(OOM)的時候回收,而弱引用無論什麼情況都會在下一輪迴收的時候回收掉。
一般jvm會對這些對象回收
1、顯示地將某個引用賦值為null或者將已經指向某個對象的引用指向新的對象。
2、局部引用所指向的對象。
3、上面說的弱引用(weakReference)。
三、垃圾收集演算法
在確定了哪些垃圾可以被回收後,垃圾收集器要做的事情就是開始進行記憶體回收,但是這裡面涉及到一個問題是:如何高效地進行記憶體回收。由於Java虛擬機器規範並沒有對如何?垃圾收集器做出明確的規定,因此各個廠商的虛擬機器可以採用不同的方式來實現垃圾收集器,就以最常用的HotShot為例,所以在此只討論幾種常見的垃圾收集演算法的核心思想。
1、Mark-Sweep(標記-清除)演算法
這是最基礎的記憶體回收演算法,之所以說它是最基礎的是因為它最容易實現,思想也是最簡單的。標記-清除演算法分為兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所佔用的空間。圖解來自網路,很好的說明了標記-清楚演算法的處理前和處理後的記憶體分布。
很容易看出這樣的操作是有弊端的,這樣講標記的對象的清楚後,記憶體塊就變的零零散散,如果現在有一個對象佔用的記憶體很大,這個時候必須要在執行一遍記憶體回收,為這個大的對象騰出空間。
2、Copying(複製)演算法
為瞭解決Mark-Sweep演算法的缺陷,Copying演算法就被提了出來。它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的對象複製到另外一塊上面,然後再把已使用的記憶體空間一次清理掉,這樣一來就不容易出現記憶體片段的問題。
複製演算法會提前空出一般的記憶體,在記憶體回收的時候將存活的對象移動的另外一半記憶體,這樣記憶體的移動消耗太大,雖然記憶體不是零散的,但是代價太高。
3、Mark-Compact(標記-整理)演算法
為瞭解決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類、常量、方法描述等。對永久代的回收主要回收兩部分內容:廢棄常量和無用的類。
三、典型的垃圾收集器
下面都是些機率性的東西,筆者看得也似懂非懂,直接搬過來分享給大家
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、多核環境。因此它是一款並行與並發收集器,並且它能建立可預測的停頓時間模型。
四、總結和補充
對象的記憶體配置,往大方向上講就是在堆上分配,對象主要分配在新生代的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的相關參數。