聊聊GC

GC(Garbage Collection)即Java垃圾回收機制,是Java與C++的主要區別之一,做爲Java開發者,通常不須要專門編寫內存回收和垃圾清理代碼,對內存泄露和溢出的問題,也不須要像C++程序員那樣戰戰兢兢,就是由於Java有這個方便的機制。程序員

爲了對GC有一個直觀的認識,先來一張圖:
GC
對圖中各類名詞不熟悉的話,請參照個人上一篇文章:JVM小結算法

GC算法整體概述

JVM在進行GC時,並不是每次都對上面三個內存區域一塊兒回收的,大部分時候回收的都是指新生代。所以GC按照回收的區域又分了兩種類型,一種是普通GC(minor GC),一種是全局GC(major GC or Full GC)segmentfault

  • 普通GC(minor GC):只針對新生代區域的GC。
  • 全局GC(major GC or Full GC):針對年老代的GC,偶爾伴隨對新生代的GC以及對永久代的GC。

四大算法

1. 複製算法(Copying)

年輕代中使用的是Minor GC,這種GC算法採用的是複製算法(Copying)。
heap數組

此圖表明瞭堆的內存結構併發

HotSpot JVM把年輕代分爲了三部分:1個Eden區和2個Survivor區(分別叫from和to)。默認比例爲8:1:1,通常狀況下,新建立的對象都會被分配到Eden區(一些大對象特殊處理),這些對象通過第一次Minor GC後,若是仍然存活,將會被移到Survivor區。對象在Survivor區中每熬過一次Minor GC,年齡就會增長1歲,當它的年齡增長到必定程度時,就會被移動到年老代中。由於年輕代中的對象基本都是朝生夕死的(80%以上),因此在年輕代的垃圾回收算法使用的是複製算法,複製算法的基本思想就是將內存分爲兩塊,每次只用其中一塊,當這一塊內存用完,就將還活着的對象複製到另一塊上面。複製算法不會產生內存碎片。
copying
在GC開始的時候,對象只會存在於Eden區和名爲From的Survivor區,Survivor區To是空的。緊接着進行GC,Eden區中全部存活的對象都會被複制到To,而在From區中,仍存活的對象會根據他們的年齡值來決定去向。年齡達到必定值(年齡閾值,能夠經過-XX:MaxTenuringThreshold來設置)的對象會被移動到年老代中,沒有達到閾值的對象會被複制到To區域。通過此次GC後,Eden區和From區已經被清空。這個時候,FromTo會交換他們的角色,也就是新的To就是上次GC前的From,新的From就是上次GC前的To。無論怎樣,都會保證名爲To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到To區被填滿,To區被填滿以後,會將全部對象移動到年老代中。佈局

-XX:MaxTenuringThreshold 設置對象在新生代中存活的次數

FromTo
由於Eden區對象通常存活率較低,通常的,使用兩塊10%的內存做爲空閒和活動區間,而另外80%的內存,則是用來給新建對象分配內存的。一旦發生GC,將10%的from活動區間與另外80%中存活的eden對象轉移到10%的to空閒區間,接下來,將以前90%的內存所有釋放,以此類推。 spa

劣勢:
複製算法彌補了標記/清除算法中,內存佈局混亂的缺點。不過與此同時,它的缺點也是至關明顯的:線程

  1. 它浪費了一半的內存。
  2. 若是對象的存活率很高,咱們能夠極端一點,假設是100%存活,那麼咱們須要將全部對象都複製一遍,並將全部引用地址重置一遍。複製這一工做所花費的時間,在對象存活率達到必定程度時,將會變的不可忽視。 因此從以上描述不難看出,複製算法要想使用,最起碼對象的存活率要很是低才行,並且最重要的是,咱們必需要克服50%內存的浪費。

2. 標記清除(Mark-Sweep)

老年代通常是由標記清除或者是標記清除與標記整理的混合實現。設計

Mark-Sweep

當堆中的有效內存空間(available memory)被耗盡的時候,就會中止整個程序(也被稱爲stop the world),而後進行兩項工做,第一項則是標記,第二項則是清除。code

  • 標記:從引用根節點開始標記全部被引用的對象。標記的過程其實就是遍歷全部的GC Roots,而後將全部GC Roots可達的對象 標記爲存活的對象。
  • 清除:遍歷整個堆,把未標記的對象清除。

通俗來說,就是當程序運行期間,若可使用的內存被耗盡的時候,GC線程就會被觸發並將程序暫停,隨後將依舊存活的對象標記一遍,最終再將堆中全部沒被標記的對象所有清除掉,接下來便讓程序恢復運行。

缺點:

  1. 效率比較低(遞歸與全堆對象遍歷),並且在進行GC的時候,須要中止應用程序,這會致使用戶體驗很是差。
  2. 這種方式清理出來的空閒內存是不連續的,咱們的死亡對象都是隨即的出如今內存的各個角落的,把它們清除以後,內存的佈局天然會散亂。而爲了應付這一點,JVM就不得不維持一個內存的空閒列表,這又是一種開銷。並且在分配數組對象的時候,尋找連續的內存空間會有難度。

3. 標記壓縮(Mark-Compact)

老年代通常是由標記清除或者是標記清除與標記壓縮的混合實現。
Mark-Compact

在整理壓縮階段,再也不對標記的對像作回收,而是經過全部存活對像都向一端移動,而後直接清除邊界之外的內存。
能夠看到,標記的存活對象將會被整理,按照內存地址依次排列,而未被標記的內存會被清理掉。如此一來,當咱們須要給新對象分配內存時,JVM只須要持有一個內存的起始地址便可,這比維護一個空閒列表顯然少了許多開銷。
標記/整理算法不只能夠彌補標記/清除算法當中,內存區域分散的缺點,也消除了複製算法當中,內存減半的高額代價。

劣勢:標記/整理算法惟一的缺點就是效率也不高,不只要標記全部存活對象,還要整理全部存活對象的引用地址。從效率上來講,標記/整理算法要低於複製算法。

4. 標記清除壓縮(Mark-Sweep-Compact)

Mark-Sweep-Compact

多提一嘴:引用計數法

這種算法最直接簡單,它維護了一個引用計數器,對象被引用一次計數器+1,少一次-1,若是計數器爲0則對象就被視爲垃圾進行回收。

注:JVM的實現通常不採用這種方式。

劣勢:

  1. 每次對對象賦值時均需維護計數器,且計數器自己有必定消耗。
  2. 較難處理循環引用。

小結

內存效率:複製算法>標記清除算法>標記整理算法(此處的效率只是簡單的對比時間複雜度,實際狀況不必定如此)。
內存整齊度:複製算法==標記整理算法>標記清除算法。
內存利用率:標記整理算法==標記清除算法>複製算法。
能夠看出,效率上來講,複製算法最快,可是卻浪費了太多內存,而爲了儘可能兼顧上面所提到的三個指標,標記/整理算法相對來講更平滑一些,但效率上依然不盡如人意,它比複製算法多了一個標記的階段,又比標記/清除多了一個整理內存的過程。

有沒有最好的算法呢?只能說:沒有最好的,只有最適合的——分代收集:

針對年輕代

年輕代特色是區域相對老年代較小,對像存活率低。這種狀況複製算法的回收整理,速度是最快的。複製算法的效率只和當前存活對像大小有關,於是很適用於年輕代的回收。而複製算法內存利用率不高的問題,經過hotspot中的兩個survivor的設計獲得緩解。

針對老年代

老年代的特色是區域較大,對像存活率高。這種狀況,存在大量存活率高的對像,複製算法明顯變得不合適。通常是由標記清除或者是標記清除與標記整理的混合實現。Mark階段的開銷與存活對像的數量成正比,這點上說來,對於老年代,標記清除或者標記整理有一些不符,但能夠經過多核/線程利用,對併發、並行的形式提標記效率。Sweep階段的開銷與所管理區域的大小形正相關,但Sweep「就地處決」的特色,回收的過程沒有對像的移動。使其相對其它有對像移動步驟的回收算法,仍然是效率最好的。可是須要解決內存碎片問題。Compact階段的開銷與存活對像的數據成開比,如上一條所描述,對於大量對像的移動是很大開銷的,作爲老年代的第一選擇並不合適。基於上面的考慮,老年代通常是由標記清除或者是標記清除與標記整理的混合實現。以hotspot中的CMS回收器爲例,CMS是基於Mark-Sweep實現的,對於對像的回收效率很高,而對於碎片問題,CMS採用基於Mark-Compact算法的Serial Old回收器作爲補償措施:當內存回收不佳(碎片致使的Concurrent Mode Failure時),將採用Serial Old執行Full GC以達到對老年代內存的整理。

相關文章
相關標籤/搜索