天天看點

JVM:GC之垃圾收集算法

1.垃圾收集概念

GC目的

  • 配置設定記憶體,為每個建立的對象配置設定空間
  • 確定還在使用的對象的記憶體一直還在,不能把有用的空間當垃圾回收了
  • 釋放不再使用的對象所占用的空間

我們把還被引用的對象稱為活的,把不再被引用的對象認為是死的,也就是我們說的垃圾。GC 的工作就是找到死的對象,釋放(也稱為回收)這些對象所使用的空間的過程稱為垃圾收集。

我們把 GC 管理的記憶體稱為 堆(heap),垃圾收集啟動的時機取決于各個垃圾收集器,通常,垃圾收集發生于整個堆或堆的部分已經被使用光了,或者使用的空間達到了某個百分比門檻值

對于記憶體配置設定,實作的難點在于在堆中找到一塊沒有被使用的确定大小的記憶體空間。是以,對于大部分垃圾回收算法來說避免記憶體碎片化是非常重要的,它将使得空間配置設定更加高效。

GC收集器理想狀态

安全和全面

活的對象一定不能被清理掉,死的對象一定不能在幾個回收周期結束後還在記憶體中。

高效

不能将我們的應用程式挂起太長時間。我們需要在時間、空間、頻次上作出權衡。比如,如果堆記憶體很小,每次垃圾收集就會很快,但是頻次會增加。如果堆記憶體很大,很久才會被填滿,但是每一次回收需要的時間很長。

記憶體碎片限制

當對垃圾對象的記憶體被釋放時,空閑空間可能會出現在不同區域的小塊中,這樣在任何一個相鄰區域中都可能沒有足夠的空間用于配置設定一個大型對象。消除分段的一種方法稱為壓縮,在下面的各種垃圾收集器設計選擇中将讨論。

可伸縮性

可伸縮性也很重要。在多處理器系統中,配置設定不應該成為多線程應用程式的可伸縮性瓶頸,而且收集也不應該成為瓶頸。

2.GC算法選擇

在設計或選擇垃圾收集算法時,必須做出一些選擇:

串行 vs 并行

并行:多個垃圾回收線程同時工作,互不影響
并發:垃圾回收線程和應用程式線程同時工作,應用程式不需要挂起

串行收集的情況,即使是多核 CPU,也隻有一個核心參與收集。使用并行收集器的話,垃圾收集的工作将配置設定給多個線程在不同的 CPU 上同時進行。并行可以讓收集工作更快,缺點是帶來的複雜性和記憶體碎片問題。

并發 vs Stop-the-world

當 stop-the-world 垃圾收集器工作的時候,應用将完全被挂起。與之相對的,并發收集器在大部分工作中都是并發進行的,也許會有少量的 stop-the-world。

stop-the-world 垃圾收集器比并發收集器簡單很多,因為應用挂起後堆空間不再發生變化,它的缺點是在某些場景下挂起的時間我們是不能接受的(如 web 應用)。

相應的,并發收集器能夠降低挂起時間,但是也更加複雜,因為在收集的過程中,也會有新的垃圾産生,同時,需要有額外的空間用于在垃圾收集過程中應用程式的繼續使用。

壓縮 vs 不壓縮 vs 複制

垃圾回收器确定了記憶體中哪些對象是活的,哪些是垃圾,它可以壓縮記憶體,将所有的活動對象一起移動,并完全回收剩餘的記憶體。在壓縮之後,在第一個空閑位置配置設定一個新對象是很容易和快速的。可以使用一個簡單的指針來跟蹤對象配置設定的下一個位置。

與壓縮收集器相反,不壓縮的收集器隻會就地釋放空間,不會移動存活對象。優點就是快速完成垃圾收集,缺點就是潛在的碎片問題。一般來說,從堆中進行配置設定比從壓縮堆中配置設定更昂貴。可能需要在堆中搜尋足夠大的連續記憶體區域以容納新對象。

第三種選擇是複制收集器,它将活動對象複制到另一個記憶體區域。這樣做的好處是,原有區域的空間被清空了,這樣後續配置設定對象空間非常迅速,缺點就是需要進行複制操作和占用額外的空間。

3.性能名額

以下幾個是評估垃圾收集器性能的一些名額:

  • 吞吐量:應用程式的執行時間占總時間的百分比,當然是越高越好
  • 垃圾收集開銷:垃圾收集時間占總時間的百分比
  • 停頓時間:垃圾收集過程中導緻的應用程式挂起時間
  • 頻次:相對于應用程式來說,垃圾收集的頻次
  • 空間:垃圾收集占用的記憶體
  • 及時性:當對象變為垃圾和記憶體可用時之間的時間

在互動式程式中,通常希望是低延時的,而對于非互動式程式,總運作時間比較重要。實時應用程式既要求每次停頓時間足夠短,也要求總的花費在收集的時間足夠短。在小型個人計算機和嵌入式系統中,則希望占用更小的空間。

4.垃圾收集算法

4.1.标記-清除算法

概念

最基礎的垃圾收集算法就是“标記-清除算法”,如同它的名字,該算法分為“标記”和“清除”兩個階段。之是以是最基礎算法是因為後續的幾種收集算法都是基于這種思路并對其不足進行改進而得到的。

不足

第一,效率問題,标記和清除兩個階段的效率都不高

第二,空間問題,标記清除之後會産生大量不連續的記憶體碎片,空間碎片太多會導緻需要配置設定較大對象時,無法找到足夠的連續記憶體而不得不提前出發另一次垃圾收集動作。

标記-清除算法示意圖
JVM:GC之垃圾收集算法

4.2.複制算法

概念

為了解決效率問題,複制算法便出現了,它将可用的記憶體按照容量等分成兩塊,每次隻使用其中的一塊,當這一塊的記憶體用完了,就将存活的對象複制到另外一塊記憶體,然後将原有記憶體塊的空間清理掉。這樣每次隻對整個半區記憶體進行垃圾回收,記憶體配置設定時就無需考慮記憶體碎片等複雜的情況了,隻需要移動堆頂的指針,按順序配置設定記憶體即可,實作簡單,運作效率高。

不足

将記憶體大小一分為二,隻使用其中一份,代價太高

複制算法示意圖
JVM:GC之垃圾收集算法

使用場景

新生代正是采用複制算法進行垃圾收集,将新生代記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一個Survivor。當回收時,将存活的對象複制到另外一塊Survivor空間上,最後清理掉剛才使用過的Eden和Survivor空間。

HotSpot虛拟機預設Eden和Survivor的大小比例是,8:1:1,也就是新生代中可用記憶體空間為整個新生代容量的90%(80%+10%),隻有10%的記憶體會被“浪費”。但是當Survivor空間不夠用時,需要依賴其它記憶體(老年代)進行配置設定擔保(Handle Promotion)。

如果另外一塊Survivor空間沒有足夠空間存放新生代收集下來的存活對象時,這些對象将直接通過配置設定擔保機制進入老年代。

4.3.标記-整理算法

概念

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

根據老年代的特點,“标記-整理算法(Mark-Compact)”就應運而生了,标記過程與“标記-清除算法”一樣,但後續不是直接對可回收對象進行清理,而是讓所有存活的對象都像一端移動,然後清理掉邊界以外的記憶體。

标記-整理算法示意圖
JVM:GC之垃圾收集算法
使用場景:老年代

4.4.分代收集算法

當使用分代收集算法時,記憶體将被分為不同的代(generation),最常見的就是分為年輕代和老年代。

JVM:GC之垃圾收集算法

在不同的分代中,可以根據不同的特點使用不同的算法:

  • 在新生代,每次垃圾收集時會發現有大批對象死去,隻有少量存活,那就選擇“複制算法”
  • 在老年代,因為對象存活率高、沒有額外空間為它進行配置設定擔保,就必須使用“标記-清理”或“标記-整理”算法來進行回收

年輕代中的收集是非常頻繁的、高效的、快速的,因為年輕代空間中,通常都是小對象,同時有非常多的不再被引用的對象。

那些經曆過多次年輕代垃圾收集還存活的對象會晉升到老年代中,老年代的空間更大,而且占用空間增長比較慢。這樣,老年代的垃圾收集是不頻繁的,但是進行一次垃圾收集需要的時間更長。

對于新生代,需要選擇速度比較快的垃圾回收算法,因為新生代的垃圾回收是頻繁的。

對于老年代,需要考慮的是空間,因為老年代占用了大部分堆記憶體,而且針對該部分的垃圾回收算法,需要考慮到這個區域的垃圾密度比較低。

參考資料:

《深入了解Java虛拟機:Java進階特性與最佳實踐》

《Java記憶體管理白皮書》:http://www.oracle.com/technetwork/java/javase/memorymanagement-whitepaper-150215.pdf

繼續閱讀