GC的四種清理演算法

風靈使發表於2018-06-28

1.標記-清除:

這是垃圾收集演算法中最基礎的,根據名字就可以知道,它的思想就是標記哪些要被回收的物件,然後統一回收。這種方法很簡單,但是會有兩個主要問題:

  1. 效率不高,標記和清除的效率都很低;
  2. 會產生大量不連續的記憶體碎片,導致以後程式在分配較大的物件時,由於沒有充足的連續記憶體而提前觸發一次GC動作。

2.複製演算法:

為了解決效率問題,複製演算法將可用記憶體按容量劃分為相等的兩部分,然後每次只使用其中的一塊,當一塊記憶體用完時,就將還存活的物件複製到第二塊記憶體上,然後一次性清楚完第一塊記憶體,再將第二塊上的物件複製到第一塊。但是這種方式,記憶體的代價太高,每次基本上都要浪費一般的記憶體。
於是將該演算法進行了改進,記憶體區域不再是按照1:1去劃分,而是將記憶體劃分為8:1:1三部分,較大那份記憶體交Eden區,其餘是兩塊較小的記憶體區叫Survior區。每次都會優先使用Eden區,若Eden區滿,就將物件複製到第二塊記憶體區上,然後清除Eden區,如果此時存活的物件太多,以至於Survivor不夠時,會將這些物件通過分配擔保機制複製到老年代中。(java堆又分為新生代和老年代)

3.標記-整理

該演算法主要是為了解決標記-清除,產生大量記憶體碎片的問題;當物件存活率較高時,也解決了複製演算法的效率問題。它的不同之處就是在清除物件的時候現將可回收物件移動到一端,然後清除掉端邊界以外的物件,這樣就不會產生記憶體碎片了。

4.分代收集

現在的虛擬機器垃圾收集大多采用這種方式,它根據物件的生存週期,將堆分為新生代和老年代。在新生代中,由於物件生存期短,每次回收都會有大量物件死去,那麼這時就採用複製演算法。老年代裡的物件存活率較高,沒有額外的空間進行分配擔保,所以可以使用標記-整理 或者 標記-清除。


記憶體清除演算法

1.標記-清除:

標記 GC roots可達的物件,清理掉沒有被標記的物件。

做法:當堆中的有效記憶體空間被耗盡的時候,就會停止整個程式,然後進行兩項工作,第一項是標記,第二項是清除。

  1. 標記:遍歷所有的GC Roots,然後將GC Roots可達的物件標記為存活的物件。
  2. 清除:清除的過程將遍歷對中所有的物件,將沒有標記的物件全部清除掉。

當程式執行期間,若可以使用的記憶體被耗盡的時候,GC執行緒就會被觸發並將程式暫停,隨後將依舊存活的物件標記一遍,最終再將堆中所有沒被標記的物件全部清除掉,接下來便讓程式恢復執行。

標記的時候為什麼有停止程式執行呢?
假設我們剛標記完圖中最右邊的那個物件,暫且記為A,結果此時在程式當中又new了一個新物件B,且A物件可以到達B物件。但是由於此時A物件已經標記結束,B物件此時的標記位依然是0,因為它錯過了標記階段。因此當接下來輪到清除階段的時候,新物件B將會被苦逼的清除掉。如此一來,不難想象結果,GC執行緒將會導致程式無法正常工作。上面的結果當然令人無法接受,我們剛new了一個物件,結果經過一次GC,忽然變成null了,這還怎麼玩?

缺點:

  1. 效率較低(遞迴,遍歷整個堆的物件)而且在進行GC的時候,需要停止應用程式,這會導致使用者體驗非常差勁。
  2. 清理出來的記憶體空間不是連續的(死亡物件都是隨機出現在記憶體的各個角落的)。再分配陣列物件的時候,尋找連續的記憶體空間不太好找。

2.複製演算法:

將記憶體分為兩塊

做法:當記憶體空間耗盡時,暫停程式執行,開啟複製演算法GC執行緒。將活動區間(記憶體)的存活物件複製到空閒的那一塊記憶體區域,並且嚴格的按照記憶體地址依次排列,與此同時GC執行緒將更新存活物件的記憶體引用地址指向新的記憶體地址。 將標記為死亡物件一次清除掉。之後(活動)的那一塊記憶體區域變為空閒,空閒的變為忙。

缺點:
1. 浪費了一半記憶體。
2. 如果物件的存活率很高,假設為100%存活,那麼就需要將所有物件都複製一遍,並且將所有引用地址複製一遍。複製這一工作所話費的時間,在物件存活率達到一定程度時,(複製所用時間)將會變的不可忽視。

總結:要想使用複製演算法,最起碼物件的存活率要非常低才行,而且最重要的是,我們必須要克服50%的記憶體浪費。

3.標記-整理演算法:

做法:

  1. 和標記-清除演算法一樣,標記出存活的物件。
  2. 按照記憶體地址依次排列,而未被標記的記憶體會被清理掉。

不難看出,標記-整理演算法不僅可以彌補 標記-清除 演算法中記憶體區要分散的缺點,也消除了複製演算法中記憶體減半的高額代價。(但是從效率上講,標記-整理演算法要低於複製演算法)

演算法總結:

  1. 都是基於根搜尋演算法GC Roots)的來判斷一個物件是否應該被回收
  2. GC執行緒開啟時,或者說GC過程開始時,它們都要暫停服務
  3. 效率:
    • 複製演算法 > 標記/整理演算法 > 標記/清除演算法(此處的效率只是簡單的對比時間複雜度,實際情況不一定如此)。
    • 記憶體整齊度:複製演算法 = 標記/整理演算法 > 標記/清除演算法。
    • 記憶體利用率:標記/整理演算法 = 標記/清除演算法 > 複製演算法。

結束語

到此我們已經將三個演算法瞭解清楚了,可以看出,效率上來說,複製演算法是當之無愧的老大,但是卻浪費了太多記憶體,而為了儘量兼顧上面所提到的三個指標,標記/整理演算法相對來說更平滑一些,但效率上依然不盡如人意,它比複製演算法多了一個標記的階段,又比標記/清除多了一個整理記憶體的過程。
難道就沒有一種最優演算法嗎?
當然是沒有的,這個世界是公平的,任何東西都有兩面性,試想一下,你怎麼可能找到一個又漂亮又勤快又有錢又通情達理,性格又合適,家境也合適,身高長相等等等等都合適的女人?就算你找到了,至少有一點這個女人也肯定不滿足,那就是多半不會恰巧又愛上了與LZ相似的各位苦逼猿友們。你是不是想說你比LZ強太多了,那LZ只想對你說,高富帥是不會爬在電腦前看技術文章的,0.0。
但是古人就是給力,古人說了,找媳婦不一定要找最好的,而是要找最合適的,聽完這句話,瞬間感覺世界美好了許多。
演算法也是一樣的,沒有最好的演算法,只有最合適的演算法。

既然這三種演算法都各有缺陷,高人們自然不會容許這種情況發生。因此,高人們提出可以根據物件的不同特性,使用不同的演算法處理,類似於蘿蔔白菜各有所愛的原理。於是奇蹟發生了,高人們終於找到了GC演算法中的神級演算法—–分代蒐集演算法。

4.!分代蒐集演算法:

本質 屬於前三種演算法的實際應用 新生代,老年代,永久代

新生代:朝生夕滅,存活時間短。eg:某一個方法的區域性變數,迴圈內的臨時變數等等。
老年代:生存時間長,但總會死亡。eg:快取物件,資料庫連線物件,單例物件等等。
永久代:幾乎一直不滅。eg:String池中的物件,載入過的類資訊。

java堆:新生代,老年代 方法區(永久代):

使用這樣的方式,我們只浪費了10%的記憶體,這個是可以接受的,因為我們換來了記憶體的整齊排列與GC速度。第二點是,這個策略的前提是,每次存活的物件佔用的記憶體不能超過這10%的大小,一旦超過,多出的物件將無法複製

為了解決上面的意外情況,也就是存活物件佔用的記憶體太大時的情況,高手們將JAVA堆分成兩部分來處理,上述三個區域則是第一部分,稱為新生代或者年輕代。而餘下的一部分,專門存放老不死物件的則稱為年老代

JVM在進行GC時,並非每次都對上面三個區域一起回收,大部分回收的是新生代。因此GC按照回收的區域又分為兩種:普通GC,全域性GC
普通GC:只針對新生代區域的GC
全域性GC:針對老年代的GC,偶爾伴隨新生代的GC以及對永久帶的GC

由於年老代與永久代相對來說GC效果不好,而且二者的記憶體使用增長速度也慢,因此一般情況下,需要經過好幾次普通GC,才會觸發一次全域性GC

相關文章