深入理解Java虛擬機(第三版)-- 垃圾回收算法

標記-清除算法

最早出現也是最基礎的垃圾收集算法是“標記-清除”(M ark-Sweep)算法,在1960年由Lisp之父 John McCarthy所提出。如它的名字一樣,算法分爲“標記”和“清除”兩個階段:

首先標記出所有需要回收的對象,在標記完成後,統一回收掉所有被標記的對象,也可以反過來,標記存活的對象,統一回收所有未被標記的對象。標記過程就是對象是否屬於垃圾的判定過程,這在前一節講述垃圾對象標記判定算法時其實已經介紹過了。

之所以說它是最基礎的收集算法,是因爲後續的收集算法大多都是以標記-清除算法爲基礎,對其缺點進行改進而得到的。

它的主要缺點有兩個:

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

標記-清除算法的執行過程如圖3-2所示。
在這裏插入圖片描述

標記-複製算法

標記-複製算法常被簡稱爲複製算法。爲了解決標記-清除算法面對大量可回收對象時執行效率低的問題,1969年Fenichel提出了一種稱爲“半區複製”(Semispace Copying)的垃圾收集算法,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。

如果內存中多數對象都是存活的,這種算法將會產生大量的內存間複製的開銷,但對於多數對象都是可回收的情況,算法需要複製的就是佔少數的存活對象,而且每次都是針對整個半區進行內存回收,分配內存時也就不用考慮有空間碎片的複雜情況,只要移動堆頂指針,按順序分配即可。這樣實現簡單,運行高效,不過其缺陷也顯而易見,這種複製回收算法的代價是將可用內存縮小爲了原來的一半,空間浪費未免太多了一 點。標記-複製算法的執行過程如圖3-3所示。

在這裏插入圖片描述

現在的商用Java虛擬機大多都優先採用了這種收集算法去回收新生代,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)。

內存的分配擔保好比我們去銀行借款,如果我們信譽很好,在98%的情況下都能按時償還,於是 銀行可能會默認我們下一次也能按時按量地償還貸款,只需要有一個擔保人能保證如果我不能還款時,可以從他的賬戶扣錢,那銀行就認爲沒有什麼風險了。內存的分配擔保也一樣,如果另外一塊 Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象,這些對象便將通過分配擔保機制直接進入老年代,這對虛擬機來說就是安全的。關於對新生代進行分配擔保的內容,在稍後的3.8.5節介 紹垃圾收集器執行規則時還會再進行講解。

標記-整理算法

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

針對老年代對象的存亡特徵,1974年Edward Lueders提出了另外一種有針對性的“標記-整 理”(Mark-Compact)算法,其中的標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向內存空間一端移動,然後直接清理掉邊界以外的內存,“標記-整理”算法的示意圖如圖3-4所示。

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

在這裏插入圖片描述

如果移動存活對象,尤其是在老年代這種每次回收都有大量對象存活區域,移動存活對象並更新所有引用這些對象的地方將會是一種極爲負重的操作,而且這種對象移動操作必須全程暫停用戶應用程序才能進行,這就更加讓使用者不得不小心翼翼地權衡其弊端了,像這樣的停頓被最初的虛擬機 設計者形象地描述爲“Stop The World”。

但如果跟標記-清除算法那樣完全不考慮移動和整理存活對象的話,彌散於堆中的存活對象導致的空間碎片化問題就只能依賴更爲複雜的內存分配器和內存訪問器來解決。譬如通過“分區空閒分配鏈 表”來解決內存分配問題(計算機硬盤存儲大文件就不要求物理連續的磁盤空間,能夠在碎片化的硬盤上存儲和訪問就是通過硬盤分區表實現的)。內存的訪問是用戶程序最頻繁的操作,甚至都沒有之一,假如在這個環節上增加了額外的負擔,勢必會直接影響應用程序的吞吐量。

基於以上兩點,是否移動對象都存在弊端,移動則內存回收時會更復雜,不移動則內存分配時會更復雜。從垃圾收集的停頓時間來看,不移動對象停頓時間會更短,甚至可以不需要停頓,但是從整個程序的吞吐量來看,移動對象會更划算。

此語境中,吞吐量的實質是賦值器(Mutator,可以理解爲使用垃圾收集的用戶程序,本書爲便於理解,多數地方用“用戶程序”或“用戶線程”代替)與收集器的效率總和。即使不移動對象會使得收集器的效率提升一些,但因內存分配和訪問相比垃圾收集頻率要高得多,這部分的耗時增加,總吞吐量仍然是下降的。

HotSpot虛擬機裏面關注吞吐量的Parallel Scavenge收集器是基於標記-整理算法的,而關注延遲的CMS收集器則是基於標記-清除算法的,這也從側面印證這點。

另外,還有一種“和稀泥式”解決方案可以不在內存分配和訪問上增加太大額外負擔,做法是讓虛擬機平時多數時間都採用標記-清除算法,暫時容忍內存碎片的存在,直到內存空間的碎片化程度已經大到影響對象分配時,再採用標記-整理算法收集一次,以獲得規整的內存空間。前面提到的基於標記-清除算法的CMS收集器面臨空間碎片過多時採用的就是這種處理辦法。

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