GC算法

一。引用計數算法java

  比較古老的回收算法。原理是此對象有一個引用,即增長一個計數,刪除一個引用則減小一個計數。算法

  垃圾回收時,只用收集計數爲0的對象。此算法最致命的是沒法處理循環引用的問題。數組

  兩種實現方式緩存

  侵入式與非侵入性,引用計數算法的垃圾收集通常有侵入式與非侵入式兩種,侵入式的實現就是將引用計數器直接根植在對象內部,用C++的思想進行解釋就是,在對象的構造或者拷貝構造中進行加一操做,在對象的析構中進行減一操做,非侵入式恩想就是有一塊單獨的內存區域,用做引用計數器多線程

圖解說明以下:併發

 

 

優勢:引用計數器實現簡單,效率高
缺點:它很難解決對象之間相互循環引用的問題,同時每次計數器的增長和減小都帶來了不少額外的開銷,因此在JDK1.1以後,這個算法已經再也不使用了。
(對象之間相互循環引用:例如一個父對象持有一個子對象的引用,子對象也持有父對象的引用,這種狀況下,父子對象將一直存在於JVM的堆中,沒法進行回收)
 

二。根搜索算法佈局

定義:根搜索方法是經過一些「GC Roots」對象做爲起點,從這些節點開始往下搜索,搜索經過的路徑成爲引用鏈(Reference Chain),當一個對象沒有被GC Roots的引用鏈鏈接的時候,說明這個對象是不可用的,這樣的對象被斷定爲是可回收的。優化

java中能夠做爲GC Roots對象包括如下幾種:spa

  1.虛擬機棧(棧幀中的本地變量表)中的引用對象。線程

  2.方法區中的類靜態屬性引用的對象。

  3.方法區中的常量引用的對象。

  4.本地方法棧中JNI(也即通常說的Native方法)的引用的對象。

根搜索算法判斷對象是否存活與引用有關。java將引用分爲四類:強引用、軟引用、弱引用、虛引用,這四種引用強度依次逐步減弱。

  強引用:就是咱們通常聲明對象是時虛擬機生成的引用,強引用環境下,垃圾回收時須要嚴格判斷當前對象是否被強引用,若是被強引用,則不會被垃圾回收。

  軟引用:軟引用通常被作爲緩存來使用。與強引用的區別是,軟引用在垃圾回收時,虛擬機會根據當前系統的剩餘內存來決定是否對軟引用進行回收。若是剩餘內存比較緊張,則虛擬機會回收軟引用所引用的空間;若是剩餘內存相對富裕,則不會進行回收。換句話說,虛擬機在發生OutOfMemory時,確定是沒有軟引用存在的。

  弱引用:弱引用與軟引用相似,都是做爲緩存來使用。但與軟引用不一樣,弱引用在進行垃圾回收時,是必定會被回收掉的,所以其生命週期只存在於一個垃圾回收週期內。

  虛引用:與其餘幾種引用都不一樣,虛引用並不會決定對象的生命週期。若是一個對象僅持有虛引用,那麼它就和沒有任何引用同樣,在任什麼時候候均可能被垃圾回收器回收。它不能單獨使用,必須和引用隊列聯合使用。虛引用的主要做用是跟蹤對象被垃圾回收的狀態。

圖解說明以下所示:

  

 

具體解析:

  根搜索算法中不可達的對象並不是「非死不可」,這時候它們暫時處於「緩刑」階段,真正宣告一個對象死亡,至少要經歷兩次標記過程。若是經過根搜索後發現沒有與GC Roots相連的引用鏈相連。它將會被第一次標記而且會進行篩選,當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種狀況都視爲沒有必要執行finalize()方法。

  若是這個對象被斷定爲有必要執行finalize()方法,那麼這個對象將會被放置在一個名爲F-Queue的隊列之中,並在稍後有一條虛擬機自動創建、低優先級的 finalize的線程去執行。虛擬機會觸發finalize()方法,但並不承諾等待它執行結束。finalize()方法是對象逃脫死亡的最後一次F-Queue中的對象機會。GC將會對F-QUEUEF-Queue中的對象進行第二次小規模的標記,若是某個對象從新與GC Roots引用鏈上的對象創建關聯關係,那麼第二次標記時它將被移除F-Queue。

  任何一個對象的finalize()方法都只會被系統自動調用一次,若是對象面臨下一次回收,它的finalize()方法不會被再次執行。

 

三。標記-清除算法

  定義:標記清除算法是最基礎的收集算法,其餘收集算法都是基於這種思想。標記清除算法分爲「標記」和「清除」兩個階段:首先標記出須要回收的對象,標記完成以後統一清除對象。

  算法過程:

    標記-清除算法是現代垃圾回收算法的思想基礎。標記-清除算法將垃圾回收分爲兩個階段:

  標記階段和清除階段。一種可行的實現是,在標記階段,首先經過根節點,標記全部從根節點開始的可達對象。

  所以,未被標記的對象就是未被引用的垃圾對象;而後,在清除階段,清除全部未被標記的對象。

  圖解說明一下:

 

                    

  標記-清除算法詳解:

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

    • 標記:標記的過程其實就是,遍歷全部的GC Roots,而後將全部GC Roots可達的對象標記爲存活的對象。
    • 清除:清除的過程將遍歷堆中全部的對象,將沒有標記的對象所有清除掉。

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

 

 

  標記-清除算法的缺點:

  (1)效率低:效率比較低(遞歸與全堆對象遍歷),致使stop the world的時間比較長,尤爲對於交互式的應用程序來講簡直是沒法接受。

  (2)空間問題:這種方式清理出來的空閒內存是不連續的,這點不難理解,咱們的死亡對象都是隨即的出如今內存的各個角落的,如今把它們清除以後,內存的佈局天然會亂七八糟。而爲了應付這一點,JVM就不得不維持一個內存的空閒列表,這又是一種開銷。並且在分配數組對象的時候,尋找連續的內存空間會不太好找。

 

四。複製算法

將內存平均分紅A、B兩塊,算法過程:

  1. 新生對象被分配到A塊中未使用的內存當中。當A塊的內存用完了, 把A塊的存活對象對象複製到B塊。
  2. 清理A塊全部對象。
  3. 新生對象被分配的B塊中未使用的內存當中。當B塊的內存用完了, 把B塊的存活對象對象複製到A塊。
  4. 清理B塊全部對象。
  5. goto 1。
  優勢:簡單高效。缺點:內存代價高,有效內存爲佔用內存的一半。
圖解說明以下所示:(圖中後觀是一個循環過程)
對複製算法進一步優化:使用Eden/S0/S1三個分區
平均分紅A/B塊太浪費內存,採用Eden/S0/S1三個區更合理,空間比例爲Eden:S0:S1==8:1:1,有效內存(便可分配新生對象的內存)是總內存的9/10。
算法過程:
1. Eden+S0可分配新生對象;
2. 對Eden+S0進行垃圾收集,存活對象複製到S1。清理Eden+S0。一次新生代GC結束。
3. Eden+S1可分配新生對象;
4. 對Eden+S1進行垃圾收集,存活對象複製到S0。清理Eden+S1。二次新生代GC結束。
5. goto 1。
 
默認Eden:S0:S1=8:1:1,所以,新生代中可使用的內存空間大小佔用新生代的9/10,那麼有人就會問,爲何不直接分紅兩個區,一個區佔9/10,另外一個區佔1/10,這樣作的緣由大概有如下幾種
1.S0與S1的區間明顯較小,有效新生代空間爲Eden+S0/S1,所以有效空間就大,增長了內存使用率
2.有利於對象代的計算,當一個對象在S0/S1中達到設置的XX:MaxTenuringThreshold值後,會將其分到老年代中,設想一下,若是沒有S0/S1,直接分紅兩個區,該如何計算對象通過了多少次GC還沒被釋放,你可能會說,在對象里加一個計數器記錄通過的GC次數,或者存在一張映射表記錄對象和GC次數的關係,是的,能夠,可是這樣的話,會掃描整個新生代中的對象, 有了S0/S1咱們就能夠用S0/S1記錄對象通過的代數【固然這取決於具體的Java虛擬機的實現了】
 
 
五。標記-整理算法
  定義:標記-壓縮算法適合用於存活對象較多的場合,如老年代。它在標記-清除算法的基礎上作了一些優化。和標記-清除算法同樣,標記-壓縮算法也首先須要從根節點開始,對全部可達對象作一次標記;但以後,它並不簡單的清理未標記的對象,而是將全部的存活對象壓縮到內存的一端;以後,清理邊界外全部的空間。
  算法過程:
  • 標記:它的第一個階段與標記/清除算法是如出一轍的,均是遍歷GC Roots,而後將存活的對象標記。
  • 整理:移動全部存活的對象,且按照內存地址次序依次排列,而後將末端內存地址之後的內存所有回收。所以,第二階段才稱爲整理階段。
 

            

  

上圖中能夠看到,標記的存活對象將會被整理,按照內存地址依次排列,而未被標記的內存會被清理掉。如此一來,當咱們須要給新對象分配內存時,JVM只須要持有一個內存的起始地址便可,這比維護一個空閒列表顯然少了許多開銷。

標記/整理算法不只能夠彌補標記/清除算法當中,內存區域分散的缺點,也消除了複製算法當中,內存減半的高額代價。

  • 可是,標記/整理算法惟一的缺點就是效率也不高。

不只要標記全部存活對象,還要整理全部存活對象的引用地址。從效率上來講,標記/整理算法要低於複製算法。(具體效果,須要根據實際狀況而定)

 

六。分代收集算法

  

  當前商業虛擬機的GC都是採用的「分代收集算法」,這並非什麼新的思想,只是根據對象的存活週期的不一樣將內存劃分爲幾塊兒。通常是把Java堆分爲新生代和老年代:短命對象歸爲新生代,長命對象歸爲老年代。

  • 新生代: 全部新生成的對象首先都是放在年輕代的。年輕代的目標就是儘量快速的收集掉那些生命週期短的對象。年輕代分三個區。一個Eden區,兩個Survivor區(通常而言)。大部分對象在Eden區中生成。當Eden區滿時,還存活的對象將被複制到Survivor區(兩個中的一個),當這個Survivor區滿時,此區的存活對象將被複制到另一個Survivor區,當這個Survivor去也滿了的時候,從第一個Survivor區複製過來的而且此時還存活的對象,將被複制「年老區(Tenured)」。須要注意,Survivor的兩個區是對稱的,沒前後關係,因此同一個區中可能同時存在從Eden複製過來 對象,和從前一個Survivor複製過來的對象,而複製到年老區的只有從第一個Survivor去過來的對象。並且,Survivor區總有一個是空的。同時,根據程序須要,Survivor區是能夠配置爲多個的(多於兩個),這樣能夠增長對象在年輕代中的存在時間,減小被放到年老代的可能。
  • 老年代:大量對象存活,適合用標記-清理/標記-整理;由於對象存活率高、沒有額外空間對他進行分配擔保,就必須使用「標記-清理」/「標記-整理」算法進行GC。
  • 持久代:基本固定不變,用於存放靜態文件,例如Java類和方法。持久代對GC沒有顯著的影響。持久代能夠經過-XX:MaxPermSize=<N>進行設置。

  

 

注:老年代的對象中,有一小部分是由於在新生代回收時,老年代作擔保,進來的對象;絕大部分對象是由於不少次GC都沒有被回收掉而進入老年代。

 

七。串行收集(拓展)

  串行收集使用單線程處理全部垃圾回收工做,由於無需多線程交互,實現容易,並且效率比較高。可是,其侷限性也比較明顯,即沒法使用多處理器的優點,因此此收集適合單處理器機器。固然,此收集器也能夠用在小數據量(100M左右)狀況下的多處理器機器上。

八。並行收集(拓展)

  並行收集使用多線程處理垃圾回收工做,於是速度快,效率高。並且理論上CPU數目越多,越能體現出並行收集器的優點。

九。併發收集(拓展)

  相對於串行收集和並行收集而言,前面兩個在進行垃圾回收工做時,須要暫停整個運行環境,而只有垃圾回收程序在運行,所以,系統在垃圾回收時會有明顯的暫停,並且暫停時間會由於堆越大而越長。

相關文章
相關標籤/搜索