JVM 系列文章之 GC 算法淺析

Java的堆結構

再介紹垃圾回收算法以前,先來看看 Java中的堆,Java裏的堆指的是用於存放 Java 對象的內存區域。JVM的堆被同一個JVM實例中全部的Java線程共享,它一般由某種自動管理機制所管理,這種機制一般叫作"垃圾回收"html

在Java 1.8 中,堆的內存模型大體以下: java

heap

堆大小 = 新生代 + 老年代。其中堆的大小能夠經過參數 -Xms,-Xmx來指定。算法

默認的,新生代(Young) 與老年代(Old)的比例的值是 1:2 (該值能夠經過參數 -XX: NewRatio來指定),即: 新生代(Young) = 1/3的堆空間大小,老年代(Old) = 2/3的堆空間大小。oracle

其中,新生代(Young)被細分爲 Eden 和 兩個 Survivor區域,這兩個 Survivor區域分別被命名爲 from 和 to,以示區分。jvm

默認的,Eden:from:to = 8:1:1 (能夠經過參數 -XX: SurvivorRatio來設定),即: Eden = 8/10 的新生代空間大小,from = to = 1/10 的新生代空間大小。jsp

JVM每次只會使用 Eden和其中的一塊 Survivor區域來爲對象服務,因此不管何時,總有一塊 Survivor區域是空閒着的,新生代實際可用的內存空間爲 90% 的新生代空間。post

標記 - 清除算法

在GC算法中,最簡單的就是 "標記-清除"(Mark-Sweep)算法。它的原理比較簡單,首先根據可達性分析算法對不可達對象進行標記,在標記完成後統一回收全部被標記的對象。標記-清除算法的執行過程以下圖: 網站

mark_sweep
標記-清除算法有兩個缺點:

  • 效率問題,標記和清除兩個過程的效率都不高
  • 空間問題,標記清除以後產生大量不連續的內存碎片,若是這時候有大對象須要連續的內存空間進行分配時,極可能會由於沒有足夠的連續內存空間而又觸發一次 GC

基於Mark-Sweep的GC 多用於老年代.net

複製算法

複製算法的思路是它將可用內存按容量劃分爲大小相等的兩塊,每次只用其中的一塊。當這塊內存用完了,就將還存活的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。線程

這樣每次都是對半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可。可是這種算法是用空間換時間,代價是將內存縮小爲原來的一半,代價很高。而新生代的對象通常是存活時間較短的對象,GC頻率較高,佔內存較少,所以新生代通常都採用基於複製的GC。複製算法過程以下:

copy

HotSpot 虛擬機將新生代內存分爲 一塊較大的 Eden空間和兩塊較小的 Survivor空間,Eden和Survivor的大小比例是8:1。每次新生代中可用內存空間爲整個新生代容量的 90%。咱們沒有辦法保證每次回收都只有很少於 10%的對象存活,當 Survvivor 空間不夠用時,須要依賴老年代進行分配擔保

標記 - 整理算法

複製收集算法在對象存活率較高時就要進行較多的複製操做,效率會變低,它比較適合收集新生代對象,至於老年代這種通常不選用複製算法。根據老年代的特色,可使用 "標記-整理"算法或者"標記-清除"算法

標記 - 整理算法能夠解決內存碎片的問題,並且思路也比較簡單,它的思想就是,讓全部存活的對象都向一端移動,而後直接清理掉邊界之外的內存,以下圖所示:

mark-compact

分代收集

當前商業虛擬機的垃圾收集都採用"分代收集",將堆分爲新生代和老年代,根據各個年代的特色採用最適當的收集算法:

  • 新生代
    • 複製收集算法
  • 老年代
    • 標記 - 清理算法
    • 標記 - 整理算法

簡單對比三種基本算法

下面的分析參照 R大對於GC算法的分析:hllvm.group.iteye.com/group/topic…

分代式 GC裏,老年代經常使用 mark-sweep(標記 - 清除算法),或者是 mark-sweep /mark-compact 的混合方式,通常狀況下用 mark-sweep,統計估算碎片量達到必定程度時用 mark-compact(標記 - 整理)。這是由於傳統上你們認爲老年代的對象可能會長時間存活且存活率高,或者是比較大,這樣拷貝起來不划算,還不如採用就地收集的方式。 Mark-Sweep,Mark-compact,copying這三種基本算法裏,只有mark-sweep是不移動對象的(也就是不拷貝的),因此老年代常選用 mark-sweep。固然針對不一樣的垃圾收集器,GC 算法是有區別的

如下是三種算法的比較:

mark-sweep mark-compact copying
速度 中等 最慢 最快
空間開銷 少(但會堆積碎片) 少(不堆積碎片) 一般須要活對象的2倍大小(不堆積碎片)
移動對象?

關於時間開銷

  • mark-sweep: mark階段與活對象的數量成正比,sweep階段與整堆大小成正比
  • mark-compact: mark階段與活對象的數量成正比,compact階段與活對象的大小成正比
  • copy:與活對象的大小成正比

若是把 mark,sweep,compact,copying這幾種動做的耗時放在一塊兒看,大體有這樣的關係:

compaction >= copying > marking > sweeping marking + sweeping > copying

總結一下:

在分代式假設中,年輕代中對象在 minor GC 時的存活率應該很低,這樣用copying算法就是最合算的,由於其時間開銷與活對象的大小成正比,若是沒多少活對象,它就很是快。並且 young GC 自己應該比較小,就算須要2倍空間也只會浪費不太多的空間

而老年代被 GC 時對象存活率可能會很高,並且假定可用剩餘空間不太多,這樣copying 算法就不太合適,因而更可能選用另兩種算法,特別是不用移動對象的 Mark-Sweep算法

不過 HotSpot VM中除了CMS收集器以外的其餘收集器都是會移動對象的,也就是要麼是 copying,要麼是mark-compact的變種

JVM堆內存設置參數

  • -XX:+<option> 啓用選項 例如:-XX:+PrintGCDetails啓動打印GC信息的選項,其中+號表示true,開啓的意思
  • -XX:-<option>不啓用選項 ,例如:-XX:-PrintGCDetails關閉啓動打印GC信息的選項,其中-號表示false,關閉的意思
  • -XX:<option>=<number>
  • -XX:<option>=<string>

經常使用堆參數

  • -Xms: 初始堆大小
  • -Xmx: 最大堆大小,默認爲物理內存的1/4
  • -Xmn: 新生代大小,一般爲 Xmx的 1/3或1/4。新生代 = Eden + 2個Survivor空間。實際可用空間爲 = Eden + 1個 Survivor,即90%
  • -XX:NewSize = n:設置新生代大小
  • -XX:NewRatio = n: 設置新生代和老年代的比值,如 n = 3,表示新生代:老年代 = 1:3。
  • -XX:SurvivorRatio: 新生代中 Eden與Survivor的比值,默認值爲 8。即Eden佔新生代空間的 8/10,另外兩個 Survivor各佔 1/10
  • -XX:PermSize: 永久代(方法區)的初始大小,(前提是永久代存在的狀況下,在JDK 1.8及之後,永久代被移除了)
  • -XX:MaxPermSize:永久代(方法區)的最大值
  • -XX:+PrintGCDetails:打印 GC 信息
  • -XX:+HeapDumpOnOutOfMemoryError:讓虛擬機在發生內存溢出時 Dump 出當前的內存堆轉儲快照,以便分析用

更多JVM參數選項設置,請參考Oracle官方網站給出的相關信息: www.oracle.com/technetwork…

小結

以上主要參考了《深刻理解Java虛擬機》這本書以及R大對於GC算法的分析,本人對於JVM是渣渣級選手,若有問題之處,歡迎指出

另外關於垃圾算法更加詳解的解釋,三種算法的具體實現參考 中村成洋的《垃圾回收的算法與實現》

參考資料 & 鳴謝

相關文章
相關標籤/搜索