天天看點

标記複制法、标記清除法和标記整理法的差別

寫在前面的話

讀者您好,本人目前同時在經營CSDN和微信公衆号,希望小夥伴們能夠給予支援,關注一下我的微信公衆号,公衆号是每天都會推送新文章,CSDN不定期發表新文章。

文末有公衆号二維碼,可以掃碼關注,或者微信直接搜尋“波波Tea”,帶哪吒頭像的那個就是我,謝謝!

垃圾收集算法的實作涉及大量的程式細節,且各個平台的虛拟機操作記憶體的方法都有差異,本文暫不過多讨論算法實作,隻重點介紹分代收集理論和幾種算法思想及其發展過程。

1、分代收集理論

目前商業虛拟機的垃圾收集器,大多數都遵循了“分代收集”的理論進行設計,它建立在兩個分代假說之上:

  • 弱分代假說:絕大多數對象都是朝生夕滅的
  • 強分代假說:熬過越多次垃圾收集過程的對象就越難以消亡

這兩個分代假說共同奠定了多款常用的垃圾收集器的一緻的設計原則:

  • 收集器應該将Java堆劃分出不同的區域
  • 然後将回收對象依據其年齡(年齡即對象熬過垃圾收集過程的次數)配置設定到不同的區域之中存儲
  • 如果一個區域中大多數對象都是朝生夕滅,那麼把它們集中放在一起,就能以較低代價回收到大量的空間
  • 如果剩下的都是難以消亡的對象,那把它們集中放在一塊,虛拟機便可以使用較低的頻率來回收這個區域
  • 這就同時兼顧了垃圾收集的時間開銷和記憶體的空間有效利用

在Java堆劃分出不同的區域之後,垃圾收集器才可以每次隻回收其中某一個或者某些部分的區域,因而才有了

  • Minor GC
  • Major GC
  • Full GC

這樣的回收類型的劃分;也才能夠針對不同的區域使用相比對的垃圾收集算法,因而發展出了

  • 标記-複制算法
  • 标記-清除算法
  • 标記-整理算法

等針對性的垃圾收集算法。

1.1、分代假說存在什麼問題?

但是分代收集并非隻是簡單劃分一下記憶體區域那麼容易,它至少存在一個明顯的困難:對象不是孤立的,對象之間會存在跨代引用。

假如現在隻進行一次新生代收集(Minor GC),但新生代中的對象是完全有可能被老年代所引用的,為了找出該區域中的存活對象,不得不在固定的GC Roots之外,再額外周遊整個老年代中所有對象來確定可達性分析結果的正确性,反過來也是一樣。

周遊整個老年代所有對象的方案雖然理論上可行,但無疑會為記憶體回收帶來很大的性能負擔,為了解決這個問題,就需要對分代收集理論添加第三條經驗法則:

  • 跨代引用假說:跨代引用相對于同代引用來說僅占極少數。

1.2、如何解決?

存在互相引用關系的兩個對象,是應該傾向于同時生存或者同時消亡的。舉個例子,如果某個新生代對象存在跨代引用,由于老年代對象難以消亡,該引用會使得新生代對象在收集時同樣難以消亡,進而在年齡增長之後晉升到老年代中,這時跨代引用也随即被消除了。

是以,我們就不應再為了少量的跨代引用去掃描整個老年代,也不必浪費空間專門記錄每一個對象是否存在及存在哪些跨代引用,隻需在新生代上建立一個全局的資料結構(該結構被稱為記憶集”),這個結構把老年代劃分成若幹小塊,辨別出老年代的哪一塊記憶體會存在跨代引用。此後當發生Minor GC時,隻有包含了跨代引用的小塊記憶體裡的對象才會被加入到GC Roots進行掃描。雖然這種方法需要在對象改變引用關系時維護記錄資料的正确性,會增加一些運作時的開銷,但比起收集時掃描整個老年代來說仍然是劃算的。

部分收集(Partial GC)又分為:

  • 新生代收集(Minor GC/Young GC):隻收集新生代
  • 老年代收集(Major GC/Old GC):隻收集老年代。目前隻有CMS收集器會有單獨收集老年代的行為,Major GC這個說法現在有點混淆,需按上下文區分到底是指老年代的收集還是整堆收集

混合收集(Mixed GC):收集整個新生代以及部分老年代,目前隻有G1收集器會有這種行為

整堆收集(Full GC):收集整個Java堆和方法區

2、标記清除法

最早出現也是最基礎的垃圾收集算法是标記-清除(Mark-Sweep)算法,分為“标記”和“清除”兩個階段:

  • 首先标記出所有需要回收的對象
  • 在标記完成後,統一回收掉所有被标記的對象
  • 也可以反過來,标記存活的對象,統一回收所有未被标記的對象

标記過程就是對象是否屬于垃圾的判定過程。

之是以說它是最基礎的收集算法,是因為後續的收集算法大多都是以标記-清除算法為基礎,對其缺點進行改進而得到的。它的主要缺點有兩個:

  • 執行效率不穩定,如果Java堆中包含大量對象,而且其中大部分是需要被回收的,這時必須進行大量标記和清除的動作,導緻标記和清除兩個過程的執行效率都随對象數量增長而降低
  • 記憶體空間的碎片化問題,标記、清除之後會産生大量不連續的記憶體碎片
标記複制法、标記清除法和标記整理法的差別

3、标記複制法

3.1、标記複制法的思想和優缺點

為了解決标記清除算法面對大量可回收對象時執行效率低的問題,它将可用記憶體按容量劃分為大小相等的兩塊,每次隻使用其中的一塊,當這一塊的記憶體用完了,就将還存活着的對象複制到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。

如果記憶體中多數對象都是存活的,這種算法将會産生大量的記憶體間複制的開銷,但對于多數對象都是可回收的情況,算法需要複制的就是占少數的存活對象,而且每次都是針對整個半區進行記憶體回收,配置設定記憶體時也就不用考慮有空間碎片的複雜情況,隻要移動堆頂指針,按順序配置設定即可。這樣實作簡單,運作高效。

其缺陷也顯而易見,這種複制回收算法的代價是将可用記憶體縮小為了原來的一半,空間浪費未免太多了一點。

3.2、HotSpot虛拟機的具體實作

HotSpot虛拟機預設Eden和Survivor的大小比例是8∶1,也即每次新生代中可用記憶體空間為整個新生代容量的90%(Eden的80%加上一個Survivor的10%)。當Survivor空間不足以容納一次Minor GC之後存活的對象時,就需要依賴其他記憶體區域(實際上大多就是老年代)進行配置設定擔保,這些對象便将通過配置設定擔保機制直接進入老年代,這對虛拟機來說就是安全的。

3.3、标記複制法的具體步驟

  1. 首先,當Eden區滿的時候會觸發第一次GC,把還活着的對象拷貝到SurvivorFrom區,當Eden區再次觸發GC的時候會掃描Eden區和From區域,對這兩個區域進行垃圾回收,經過這次回收後還存活着的對象,則直接複制到To區域(如果有對象的年齡已經達到了老年的标準,則指派到老年代區),同時把這些對象的年齡 + 1
  2. 然後,情況Eden和SurvivoFrom中的對象,也即複制之後有交換,誰空誰是To
  3. SurvivorTo和SurvivorFrom互換
  4. 最後,SurvivoTo和SurvivorFrom互換,原SurvivorTo成為下一次GC時的SurvivorFrom區。部分對象會在From和To區域中複制來複制去,如此交換15次(由JVM參數MaxTenuringThreshold決定,這個參數預設是15),最終如果還是存活,就存入老年代

4、标記整理法

4.1、标記清除法的思想

标記複制算法在對象存活率較高時就要進行較多的複制操作,效率将會降低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行配置設定擔保,以應對被使用的記憶體中所有對象都100%存活的極端情況,是以在老年代一般不能直接選用這種算法。

針對老年代對象的存亡特征,标記整理法其中的标記過程仍然與标記清除算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向記憶體空間一端移動,然後直接清理掉邊界以外的記憶體,如下圖所示。

标記複制法、标記清除法和标記整理法的差別

4.2、标記整理法的優缺點

标記清除算法與标記整理算法的本質差異在于前者是一種非移動式的回收算法,而後者是移動式的。是否移動回收後的存活對象是一項優缺點并存的風險決策:

  • 如果移動存活對象,尤其是在老年代這種每次回收都有大量對象存活區域,移動存活對象并更新所有引用這些對象的地方将會是一種極為負重的操作,而且這種對象移動操作必須全程暫停使用者應用程式才能進行,這就更加讓使用者不得不小心翼翼地權衡其弊端了。
  • 但如果跟标記清除算法那樣完全不考慮移動和整理存活對象的話,彌散于堆中的存活對象導緻的空間碎片化問題就隻能依賴更為複雜的記憶體配置設定器和記憶體通路器來解決。記憶體的通路是使用者程式最頻繁的操作,甚至都沒有之一,假如在這個環節上增加了額外的負擔,勢必會直接影響應用程式的吞吐量。

基于以上兩點,是否移動對象都存在弊端。

  • 從垃圾收集的停頓時間來看,不移動對象停頓時間會更短,甚至可以不需要停頓
  • 但是從整個程式的吞吐量來看,移動對象會更劃算

HotSpot虛拟機裡面關注吞吐量的Parallel Scavenge收集器是基于标記整理算法的,而關注延遲的CMS收集器則是基于标記清除算法的。另外還有一種“和稀泥式”解決方案,做法是讓虛拟機平時多數時間都采用标記清除算法,暫時容忍記憶體碎片的存在,直到記憶體空間的碎片化程度已經大到影響對象配置設定時,再采用一次标記整理法,前面提到的基于标記清除法的CMS收集器面臨空間碎片過多時采用的就是這種處理辦法。

标記複制法、标記清除法和标記整理法的差別