Java垃圾回收詳解

一.如何確定某個對象是“垃圾”?

  首先要搞清一個最基本的問題:如果確定某個對象是“垃圾”?既然垃圾收集器的任務是回收垃圾對象所佔的空間供新的對象使用,那麼垃圾收集器如何確定某個對象是“垃圾”?—即通過什麼方法判斷一個對象可以被回收了。

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

  這種方式的特點是實現簡單,而且效率較高,但是它無法解決循環引用的問題,因此在Java中並沒有採用這種方式(Python採用的是引用計數法)。看下面這段代碼:

複製代碼
 1 public class Main {
 2     public static void main(String[] args) {
 3         MyObject object1 = new MyObject();
 4         MyObject object2 = new MyObject();
 5 
 6         object1.object = object2;
 7         object2.object = object1;
 8 
 9         object1 = null;
10         object2 = null;
11     }
12 }
13 
14 class MyObject{
15     public Object object = null;
16 }
複製代碼

  最後面兩句將object1和object2賦值爲null,也就是說object1和object2指向的對象已經不可能再被訪問,但是由於它們互相引用對方,導致它們的引用計數都不爲0,那麼垃圾收集器就永遠不會回收它們。

  爲了解決這個問題,在Java中採取了 可達性分析法

  該方法的基本思想是通過一系列的“GC Roots”對象作爲起點進行搜索,如果在“GC Roots”和一個對象之間沒有可達路徑,則稱該對象是不可達的,不過要注意的是被判定爲不可達的對象不一定就會成爲可回收對象。被判定爲不可達的對象要成爲可回收對象必須至少經歷兩次標記過程,如果在這兩次標記過程中仍然沒有逃脫成爲可回收對象的可能性,則基本上就真的成爲可回收對象了。

  總結一下平常遇到的比較常見的將對象判定爲可回收對象的情況:

  1)顯示地將某個引用賦值爲null或者將已經指向某個對象的引用指向新的對象,比如下面的代碼:

1 Object obj = new Object();
2 obj = null;
3 Object obj1 = new Object();
4 Object obj2 = new Object();
5 obj1 = obj2; 

  2)局部引用所指向的對象,比如下面這段代碼:

複製代碼
1 void fun() {
2 
3 .....
4     for(int i=0;i<10;i++) {
5         Object obj = new Object();
6         System.out.println(obj.getClass());
7     }    
8 }
複製代碼

  循環每執行完一次,生成的Object對象都會成爲可回收的對象。

  3)只有弱引用與其關聯的對象,比如:

1 WeakReference<String> wr = new WeakReference<String>(new String("world"));

二.典型的垃圾收集算法

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

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

  這是最基礎的垃圾回收算法,之所以說它是最基礎的是因爲它最容易實現,思想也是最簡單的。標記-清除算法分爲兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所佔用的空間。具體過程如下圖所示:

  從圖中可以很容易看出標記-清除算法實現起來比較容易,但是有一個比較嚴重的問題就是容易產生內存碎片,碎片太多可能會導致後續過程中需要爲大對象分配空間時無法找到足夠的空間而提前觸發新的一次垃圾收集動作。

2.Copying(複製)算法

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

  這種算法雖然實現簡單,運行高效且不容易產生內存碎片,但是卻對內存空間的使用做出了高昂的代價,因爲能夠使用的內存縮減到原來的一半。

  很顯然,Copying算法的效率跟存活對象的數目多少有很大的關係,如果存活對象很多,那麼Copying算法的效率將會大大降低。

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

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

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

  分代收集算法是目前大部分JVM的垃圾收集器採用的算法。它的核心思想是根據對象存活的生命週期將內存劃分爲若干個不同的區域。

  Java 中的堆是 JVM 所管理的最大的一塊內存空間,主要用於存放各種類的實例對象。在 Java 中,堆被劃分成兩個不同的區域:新生代 ( Young )、老年代 ( Old )。老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那麼就可以根據不同代的特點採取最適合的收集算法。新生代 ( Young ) 又被劃分爲三個區域:Eden、From Survivor、To Survivor。這樣劃分的目的是爲了使 JVM 能夠更好的管理堆內存中的對象,包括內存的分配以及回收。

  JVM 每次只會使用 Eden 和其中的一塊 Survivor 區域來爲對象服務,所以無論什麼時候,總是有一塊 Survivor 區域是空閒着的。因此,新生代實際可用的內存空間爲 9/10 ( 即90% )的新生代空間。新生代垃圾回收採用複製算法,清理的頻率比較高。如果新生代在若干次清理(可以進行設置)中依然存活,則移入老年代,有的內存佔用比較大的直接進入老年代。老年代使用標記整理算法,清理的頻率比較低。

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

  而由於老年代的特點是每次回收都只回收少量對象,一般使用的是Mark-Compact算法。

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

  這種回收方式用了程序的一種特性:大部分對象會從產生開始在很短的時間內變成垃圾,而存在的很長時間的對象往往都有較長的生命週期。高頻對新生成的對象進行回收,稱爲「小回收」,低頻對所有對象回收,稱爲「大回收」。每一次「小回收」過後,就把存活下來的對象歸爲「老生代」,「小回收」的時候,遇到老生代直接跳過。大多數分代回收算法都採用的「複製收集」方法,因爲小回收中垃圾的比例較大。

  這種方式存在一個問題:如果在某個新生代的對象中,存在「老生代」的對象對它的引用,它就不是垃圾了,那麼怎麼制止「小回收」對其回收呢?這裏用到了一中叫做寫屏障的方式。

三.典型的垃圾收集器

垃圾收集算法是 內存回收的理論基礎,而垃圾收集器就是內存回收的具體實現。下面介紹一下HotSpot(JDK 7)虛擬機提供的幾種垃圾收集器,用戶可以根據自己的需求組合出各個年代使用的收集器。

  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、多核環境。因此它是一款並行與併發收集器,並且它能建立可預測的停頓時間模型。

  不同於其他的分代回收算法、G1將堆空間劃分成了互相獨立的區塊。每塊區域既有可能屬於O區、也有可能是Y區,且每類區域空間可以是不連續的(對比CMS的O區和Y區都必須是連續的)。這種將O區劃分成多塊的理念源於:當併發後臺線程尋找可回收的對象時、有些區塊包含可回收的對象要比其他區塊多很多。雖然在清理這些區塊時G1仍然需要暫停應用線程、但可以用相對較少的時間優先回收包含垃圾最多區塊。這也是爲什麼G1命名爲Garbage First的原因:第一時間處理垃圾最多的區塊。

平時工作中大多數系統都使用CMS、即使靜默升級到JDK7默認仍然採用CMS、那麼G1相對於CMS的區別在:

  • G1在壓縮空間方面有優勢
  • G1通過將內存空間分成區域(Region)的方式避免內存碎片問題
  • Eden, Survivor, Old區不再固定、在內存使用效率上來說更靈活
  • G1可以通過設置預期停頓時間(Pause Time)來控制垃圾收集時間避免應用雪崩現象
  • G1在回收內存後會馬上同時做合併空閒內存的工作、而CMS默認是在STW(stop the world)的時候做
  • G1會在Young GC中使用、而CMS只能在O區使用

就目前而言、CMS還是默認首選的GC策略、可能在以下場景下G1更適合:

  • 服務端多核CPU、JVM內存佔用較大的應用(至少大於4G)
  • 應用在運行過程中會產生大量內存碎片、需要經常壓縮空間
  • 想要更可控、可預期的GC停頓週期;防止高併發下應用雪崩現象

下面補充一下關於內存分配方面的東西:

  對象的內存分配,往大方向上講就是在堆上分配,對象主要分配在新生代的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的相關參數。

原文鏈接:http://www.cnblogs.com/Eason-S/p/5782478.html

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