Java垃圾收集的藝術

垃圾收集(Garbage Collection),簡稱GC,是Java語言一個成名特性,使它擺脫了C、C++那樣手動管理內存的痛苦,提到垃圾收集,必然想到它是幹什麼的?簡單來講,它是咱們管理堆內存和方法區上的空間的好助手,要想對垃圾收集創建最基本的認識,最起碼可以回答:算法

1 .垃圾收集何時發生?

2 .垃圾收集回收什麼對象?

3 .垃圾回收時作了什麼事情?

回答這些問題必須知道Java的垃圾回收是按代的垃圾回收機制。Java裏面沒有顯示的註銷內存的方式,有人可能說Java裏面有finalize()方法,可是這個方法絕對不是C++中的析構函數,並且執行的時機也是不肯定甚至是否執行也是未知的,也有可能使用System.gc(),可是這個方法會顯著的影響系統性能,不建議過多使用。bash

首先簡略的回答下上面的三個問題。1.通常發現空間不夠或者其它時機會觸發GC,GC又分爲minor GC/full GC,下面會詳細展開說。 2. Java回收那些從GC roots開始不可達的對象3. 主要作的就是中止線程,標記內存,有的會複製清理,有的會標記清理,取決於具體的垃圾回收算法。多線程

上面只是一個粗淺的印象,下面來講說按代的垃圾回收機制。併發

按代的垃圾回收機制

我在淺析JVM內存分區中提到過Java堆分爲新生代和老年代。函數

新生代(Young Gen)

新生代的目標就是儘量快速的收集掉那些生命週期短的對象,大多數對象可謂是朝生夕死,GC的頻率也比較高,總的來講它有3個空間。高併發

  • 1個Eden空間(伊甸園)
  • 2個Survivor空間(倖存者)

對象保存在Eden和from survivor區,minor GC運行時,Eden中的倖存對象會被複制到to Survivor(同時對象年齡會增長1)。而from survivor區中的倖存對象會考慮對象年齡,若是年齡沒達到閾值,對象依然複製到to survivor中。若是對象達到閾值那麼將被移到老年代。複製階段完成後,Eden和From倖存區中只保存死對象,能夠視爲清空。若是在複製過程當中to倖存區被填滿了,剩餘的對象將被放到老年代。最後,From survivor和to survivor會調換一下名字,下次Minor GC時,To survivor變爲From Survivor。

Eden空間和Survior空間的空間比例默認是8:1,經過參數-XX: SurvivorRatio=8來控制,也能夠設置爲別的值。post

老年代(Old Gen)

對象沒有變得不可達(後面會說到不可達即表明對象還保持使用),而且可以重新生代的屢次GC中存活下來,就會被拷貝到老年代,其佔用的空間也比新生代要多,因此老年代內發生GC的次數明顯要少得多,老年代的GC事件通常是在空間已滿時發生,執行的過程根據GC類型的不一樣而有所區別。老年代滿時觸發FullGC(Major GC),由於老年代中的對象比較「能活」,因此FullGC觸發的頻率較低。性能

具體來講,虛擬機給每一個對象定義了一個對象年齡(Age)計數器。若是對象在Eden出生而且經歷過一次minor GC仍然存活就會被轉移到Survivor,這時候它的Age也增長1,對象在Survivor區每熬過一次minor GC,年齡就增長1,當增加到必定程度(默認15歲),就會晉升到老年代中,這個程度能夠經過-XX: MaxTenuringThreshold設置。優化

固然按照年齡進入老年代也不是絕對的,虛擬機還支持動態年齡斷定,**當Survivor空間中相同年齡全部對象大小的綜合大於Survivor空間的一半,年齡大於等於該年齡的對象就能夠直接進入老年代,無需等到MaxTenuringThreshold設置的年齡。**還有一個特例是通常大對象直接在老年代分配,避免大對象在各個區域間來回拷貝,形成性能損失,不過最好的方法是不要new過多的大對象。spa

永久代(Permanent Gen)

在不少地方咱們還能看到永久代的說法,其實就是JVM內存分區裏的方法區,HotSpot在1.7之前把方法區和堆放在一塊兒作垃圾收集的,因此方法區又叫永久代。主要存放靜態文件,如Java類、方法等。永久代對垃圾回收沒有顯著影響,可是現現在,例如Spring或者JSP都大量利用反射,動態代理,CGLib生成大量的Class,這時候咱們須要設置一個比較大的永久代空間,防止方法區發生內存溢出。至於1.8已經使用Metaspace了。

上面說的是分代垃圾收集的思想,可是有個常常提到卻尚未解答的問題,咱們在每一次GC都會保留存活的對象,那麼如何判斷出哪些對象時存活的,哪些對象又是要清理的呢?這也是咱們一開始提的問題中的一個,即垃圾收集回收什麼對象?

對象判活算法

引用計數法

顧名思義,引用計數法就是在每個對象上綁定一個計數器,當有一個地方引用該對象時,引用計數值就加1,引用失效時,計數值就會減1,當計數值爲0時,說明對象再也不被使用,這時候就能夠看做無效對象了。就我所知C++的智能指針和Objective C中ARC都利用了引用計數,有關智能指針能夠參考個人C++11 智能指針

可是在Java虛擬機的實現中並無採用引用計數法,其核心緣由就是由於對象間的相互循環引用

class C{
  public Object x;
}
C obj一、obj2 = new C();
obj1.x = obj2;
obj2.x = obj1;
obj一、obj2 = null;
複製代碼

obj1和obj2相互持有對方的引用,因此GC收集器沒法回收它們。

可達性分析算法

在主流的支持GC的語言中,都是經過可達性分析來判斷對象是否存活的。算法思想就是經過一系列的GC Roots做爲起始點,而後往下搜索,可以鏈接到GC Roots的,都證實仍是活的,若是斷鏈子了,則證實不可達,也就是對象不是存活的。

如圖所示,藍色的所有可以直接或者間接的連接到GC Roots,Obj六、Obj七、Obj8雖然彼此相連,可是沒法連接到GC Roots,因此他們都是不可達的,將會被 斷定爲可回收的

那麼哪些對象會成爲GC Roots呢,能夠分爲如下幾種:

  • 虛擬機棧(棧幀的本地變量表)中引用的對象
  • 方法區中類靜態屬性引用的對象
  • 方法區中常量引用的對象
  • 本地方法棧中JNI(即通常說的Native方法)引用的對象

知道什麼對象須要回收,上面也說了分代回收的思想,那麼具體在回收的時候,內存是怎麼作的呢?那就不得不整理一下Java垃圾回收的常見算法。

垃圾回收算法

標記清除算法

標記清除即Mark-Sweep,是一種最簡單的收集算法。在經歷過對象判活之後,咱們把須要回收的對象標記出來,而後在統一時刻回收全部被標記的對象。如圖所示:

黑色標記的可回收對象在回收後所有變成未使用空間,可是這樣回收後有木有發現空間碎片不少,碎片太多就會致使再分配稍微大點的空間時,找不到這樣的連續內存,從而致使GC會被頻繁調用,因此標記清除是一種基礎的垃圾收集算法,其它算法基本都是以它爲基礎優化產生。

複製算法

複製算法的思想就是把內存分爲兩塊,每次只在一邊分配內存,當一邊的內存用完了,就把全部還存活的對象複製到另外一半去,這時候把原來使用過的這一邊的全部空間一次性清理掉,因此也就不存在內存碎片的問題了,基本思路如圖:

其實前面提到的分代GC算法在新生代區域就用了複製算法,而且也沒有分紅1:1,而是8:1,也就是所謂的Eden區和survivor區,大多數對象都是「朝生夕死」的,因此在minorGC時,只把存活下來的對象所有複製到survivor區,具體的賦值過程前文中也提到過,在此再也不復述。

標記整理算法

上面提到的賦值算法也有它的弱點,就是當對象存活率很高的時候,就會存在不少的複製操做,從而影響了效率。因此這種算法運用在老年代的話很明顯不合適,因而又有了標記整理算法,這種算法的主要思路就是把活躍對象標記出來,以後再向內存的一側移動,而後直接清理掉端邊界之外的內存,具體思路以下:

由於老年代須要清理的對象比較少,因此這種移動也會比較少。

分代收集算法

分代收集算法是前面提到的算法的綜合之做,當前的商業虛擬機的垃圾收集都採用分代收集,通常新生代採用複製算法,老年代採用「標記-清理」或者「標記-整理」。

知道這麼所垃圾收集算法,它們的實現也是繁多的,這裏介紹幾種主流實現,不少都是屹立多年的經典垃圾收集器了,固然新的收集器一直不斷的在被開發中。

垃圾收集器

目前看主流的垃圾收集器也就下面這些,其中Serial、ParNew、Parallel Scavenge主要應用在新生代,CMS、SerialOld、Parallel Old主要應用在老年代,而G1的回收範圍是整個Java堆(包括新生代和老年代)。有連線的說明彼此之間能夠結合使用。

  • Serial收集器(複製算法): 新生代單線程收集器,標記和清理都是單線程,優勢是簡單高效;

  • Serial Old收集器 (標記-整理算法): 老年代單線程收集器,Serial收集器的老年代版本;

  • ParNew收集器 (複製算法): 新生代收並行集器,其實是Serial收集器的多線程版本,在多核CPU環境下有着比Serial更好的表現;

  • Parallel Scavenge收集器 (複製算法): 新生代並行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用戶線程時間/(用戶線程時間+GC線程時間),高吞吐量能夠高效率的利用CPU時間,儘快完成程序的運算任務,適合後臺應用等對交互相應要求不高的場景;

  • Parallel Old收集器 (標記-整理算法): 老年代並行收集器,吞吐量優先,Parallel Scavenge收集器的老年代版本;

  • CMS(Concurrent Mark Sweep)收集器(標記-清除算法): 老年代並行收集器,以獲取最短回收停頓時間爲目標的收集器,具備高併發、低停頓的特色,追求最短GC回收停頓時間。

  • G1(Garbage First)收集器 (標記-整理算法): Java堆並行收集器,G1收集器是JDK1.7提供的一個新收集器,G1收集器基於「標記-整理」算法實現,也就是說不會產生內存碎片。此外,G1收集器不一樣於以前的收集器的一個重要特色是:G1回收的範圍是整個Java堆(包括新生代,老年代),而前六種收集器回收的範圍僅限於新生代或老年代。

有空再整理每一個垃圾收集器具體的實現。

相關文章
相關標籤/搜索