JVM GC之垃圾收集算法

1.垃圾收集概念

GC目的

  • 分配內存,爲每一個新建的對象分配空間
  • 確保還在使用的對象的內存一直還在,不能把有用的空間當垃圾回收了
  • 釋放再也不使用的對象所佔用的空間

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

咱們把 GC 管理的內存稱爲 堆(heap),垃圾收集啓動的時機取決於各個垃圾收集器,一般,垃圾收集發生於整個堆或堆的部分已經被使用光了,或者使用的空間達到了某個百分比閾值web

對於內存分配,實現的難點在於在堆中找到一塊沒有被使用的肯定大小的內存空間。因此,對於大部分垃圾回收算法來講避免內存碎片化是很是重要的,它將使得空間分配更加高效。算法

GC收集器理想狀態

安全和全面安全

活的對象必定不能被清理掉,死的對象必定不能在幾個回收週期結束後還在內存中。多線程

高效併發

不能將咱們的應用程序掛起太長時間。咱們須要在時間、空間、頻次上做出權衡。好比,若是堆內存很小,每次垃圾收集就會很快,可是頻次會增長。若是堆內存很大,好久纔會被填滿,可是每一次回收須要的時間很長。oracle

內存碎片限制svg

當對垃圾對象的內存被釋放時,空閒空間可能會出如今不一樣區域的小塊中,這樣在任何一個相鄰區域中均可能沒有足夠的空間用於分配一個大型對象。消除分段的一種方法稱爲壓縮,在下面的各類垃圾收集器設計選擇中將討論。性能

可伸縮性線程

可伸縮性也很重要。在多處理器系統中,分配不該該成爲多線程應用程序的可伸縮性瓶頸,並且收集也不該該成爲瓶頸。

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.標記-清除算法

概念

最基礎的垃圾收集算法就是「標記-清除算法」,如同它的名字,該算法分爲「標記」和「清除」兩個階段。之因此是最基礎算法是由於後續的幾種收集算法都是基於這種思路並對其不足進行改進而獲得的。

不足

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

第二,空間問題,標記清除以後會產生大量不連續的內存碎片,空間碎片太多會致使須要分配較大對象時,沒法找到足夠的連續內存而不得不提早出發另外一次垃圾收集動做。

標記-清除算法示意圖

image

4.2.複製算法

概念

爲了解決效率問題,複製算法便出現了,它將可用的內存按照容量等分紅兩塊,每次只使用其中的一塊,當這一塊的內存用完了,就將存活的對象複製到另一塊內存,而後將原有內存塊的空間清理掉。這樣每次只對整個半區內存進行垃圾回收,內存分配時就無需考慮內存碎片等複雜的狀況了,只須要移動堆頂的指針,按順序分配內存便可,實現簡單,運行效率高。

不足

將內存大小一分爲二,只使用其中一份,代價過高

複製算法示意圖

image

使用場景
新生代正是採用複製算法進行垃圾收集,將新生代內存分爲一塊較大的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)」就應運而生了,標記過程與「標記-清除算法」同樣,但後續不是直接對可回收對象進行清理,而是讓全部存活的對象都像一端移動,而後清理掉邊界之外的內存。

標記-整理算法示意圖

image

使用場景:老年代

4.4.分代收集算法

當使用分代收集算法時,內存將被分爲不一樣的代(generation),最多見的就是分爲年輕代和老年代。
image
在不一樣的分代中,能夠根據不一樣的特色使用不一樣的算法:
- 在新生代,每次垃圾收集時會發現有大批對象死去,只有少許存活,那就選擇「複製算法」
- 在老年代,由於對象存活率高、沒有額外空間爲它進行分配擔保,就必須使用「標記-清理」或「標記-整理」算法來進行回收

年輕代中的收集是很是頻繁的、高效的、快速的,由於年輕代空間中,一般都是小對象,同時有很是多的再也不被引用的對象。

那些經歷過屢次年輕代垃圾收集還存活的對象會晉升到老年代中,老年代的空間更大,並且佔用空間增加比較慢。這樣,老年代的垃圾收集是不頻繁的,可是進行一次垃圾收集須要的時間更長。

對於新生代,須要選擇速度比較快的垃圾回收算法,由於新生代的垃圾回收是頻繁的。

對於老年代,須要考慮的是空間,由於老年代佔用了大部分堆內存,並且針對該部分的垃圾回收算法,須要考慮到這個區域的垃圾密度比較低。

參考資料:

《深刻理解Java虛擬機:Java高級特性與最佳實踐》

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

相關文章
相關標籤/搜索