JVM垃圾收集器與內存分配策略(二)—— 垃圾收集算法

1、垃圾收集算法

從如何判定對象消亡的角度出發,垃圾收集算法可劃分爲 “引用計數式垃圾收集”(Reference Counting GC)和 “追蹤式垃圾收集”(Tracing GC)兩大類,這兩類也常被稱作 “直接垃圾收集” 和 “間接垃圾收集”。

1.1、分代收集理論

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

  1. 弱分代假說(Weak Generational Hypothesis):絕大數對象都是朝生夕滅的。
  2. 強分代假說(Strong Generational Hypothesis):熬過越多次垃圾收集過程的對象就越難以消亡。
    這兩個分代假說共同奠定了多款常用的垃圾收集器的一致的設計原則:收集器應該將Java堆劃分出不同的區域,然後將回收對象依據其年齡(年齡即對象熬過垃圾收集過程的次數)分配到不同的區域之中存儲。在Java堆劃分出不同的區域之後,垃圾收集器纔可以每次只回收其中某一個或者某些部分區域——因而纔有了“Minor GC”、“Major GC”、“Full GC”這樣的回收類型劃分;也才能夠針對不同的區域安排與裏面存儲對象存亡特徵相匹配的垃圾收集算法——因而發展出了“標記-複製算法”、“標記-清楚算法”、“標記-整理算法”等針對性的垃圾收集算法。

1.2、標記-清除算法

最早出現也是最基礎的垃圾收集算法——“標記-清除算法”(Mark-Sweep)算法,在1960年由Lisp之父John McCarthy所提出。如它的名字一樣,算法分爲“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後,統一回收掉所有被標記的對象。也可以反過來,標記存活的對象,統一回收所有未被標記的對象。它的主要缺點有兩個:

  1. 執行效率不穩定,如果Java堆中包含大量對象,而且其中大部分是需要被回收的,這時必須進行的大量標記和清除動作,導致標記和清除的兩個過程的執行效率都隨着對象數量增長而降低;
  2. 內存空間碎片化問題,標記、清除之後會產生大量不連續內存碎片,空間碎片太多可能會導致以後在程序運行過程中需要分配較大對象時無法找到足夠的連續內存空間而不得不提前觸發另一次垃圾收集動作。
    標記-清除算法執行過程如圖:
    “標記-清除”算法示意圖

1.3、標記-複製算法

標記-複製算法常被稱爲複製算法。爲了解決標記-清除面對大量可回收對象時執行效率低的問題,1969年Fenichel提出了一種稱爲“半複製區”(Semispace Copying)的垃圾收集算法,它將可用內存容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,將還存活的對象複製到另一塊上面,然後再把自己使用過的內存空間一次清理掉。如果內存中的多數對象都是存活的,這種算法將會產生大量的內存間複製的開銷,但對於多數對象都是可回收的情況,算法需要複製的就是佔少數的存活對象,而且每次都是針對整個半區進行內存回收,分配內存時也就不用考慮有空間碎片的複雜情況,只要移動堆頂指針,按順序分配即可。這樣實現簡單,運行高效,不過其缺陷也顯而易見,這種複製算法的代價是將可用內存縮小了原來的一半,空間資源浪費。標記-複製算法執行過程如圖:
標記-複製算法示意圖
現在的商用虛擬機大都優先採用了這種收集算法回收新生代,IBM曾有一項專門研究對新生代“朝生夕滅”的特點做了更量化的詮釋——新生代中的對象有98%熬不過第一輪收集。因此不需要按照1 :1的比例來劃分新生代的內存空間。
在1989年,Andrew Appel針對具備“朝生夕滅”特點的對象,提出了一種更優化的半區複製分代策略,現在稱爲”Appel式回收”。HotSpot虛擬機的Serial、ParNew等新生代收集器均採用了這種策略會設計新生代的內存佈局。Appel式回收的具體做法是把新生代分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次分配內存只使用Eden和其中一塊Survivor。發生垃圾收集時,將Eden和Survivor中任然存活的對象一次性複製到另一塊Survivor空間上,然後直接清理掉Eden和已經用過的那塊Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8 : 1,也就是每次新生代可用的空間爲整個新生代容量的90%,(Eden的80%加上一個Survivor的10%),只有一個Survivor空間,即10%的新生代是會被“浪費”的。當然98%的對象可被回收僅僅是“普通場景”下測得的數據,任何人都沒辦法百分百保證每次回收都只有不超過10%的對象存活,一次Appel式回收還有一個充當罕見情況的“逃生門”的安全設計,當Survivor空間不足以容納一次Minor GC之後村後的對象時,就需要依賴其他內存區域(實際上大多就是老年代)進行分配擔保(Handle Promotion)。

1.4、標記-整理算法

針對老年代的對象存亡特徵,1974年Eward Lueders提出了另外一種有針對性的“標記-整理”(Mark-Sweep)算法,其中標記過程任然與標記-清除算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活對象都向着內存空間一端移動,然後直接清理掉邊界意外的內存。“標記-整理”算法示意圖:
“標記-整理”算法示意圖
標記-清除算法與標記-整理算法的本質差異在於前者是一種非移動式的會後算法,而後者是移動式的。
是否移動回收後的存活對象是一項優缺點並存的風險決策:
如果移動存活對象,尤其是老年代這種每次回收都有大量對象存活區域,移動存活對象並更新所有引用這對象的地方將會是一種幾位負重的操作,而且這種對象移動操作必須全程暫停用戶應用程序才能進行,像這樣的停頓被最初的設計者形象地描述爲“Stop The World”。
但如果跟標記-清除算法一樣完全不考慮移動和整理存活對象的話,彌散與堆中的存活對象導致的空間碎片化問題只能依賴更爲複雜的內存分配器和內存訪問器來解決。譬如通過“分區空閒分配鏈表”來解決內存分配問題(計算機硬盤存儲大文件就不要求連續的磁盤空間,能夠在碎片化的硬盤上存儲和訪問就是通過硬盤分區表實現的)。內存的訪問是用戶程序最頻繁的操作,甚至沒有之一,加入這個環節增加額外負擔,勢必會直接影響用戶應用程序的吞吐量。
基於以上兩點,是否移動對象都存在弊端,移動則內存回收時會更復雜,不移動則內存分配時更復雜。從垃圾收集的停頓時間來看,不移動對象停頓時間更短,甚至可以不需要停頓,但是從整個應用程序的吞吐量來看,移動對象會更划算。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章