天天看點

垃圾收集器與記憶體配置設定政策

1.引用計數算法

  給對象中添加一個引用計數器,每當有一個地方引用它時,計數器就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。

  考慮一種情形:對象objA和objB都有字段instance,指派令objA.instance=objB和objB.instance=objA;除此之外,這兩個對象再無任何引用,實際上這兩個對象以及不可能再被通路,但是它們因為互相引用着對方,導緻它們的引用計數都不為0,于是引用計數算法無法通知GC收集器回收它們。如果這個對象特别大,則會造成嚴重的記憶體洩露。

2.可達性分析算法

  基本思想:通過一系列的稱為”GC Roots”的對象作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鍊(Reference Chain),當一個對象到GC Roots沒有任何引用鍊時,則證明此對象是不可用的。

GC Roots的對象包括下面幾種:

虛拟機棧(棧幀中的本地變量表)中引用的對象。

方法區中類靜态屬性引用的對象。

方法區中常量引用的對象。

本地方法棧中JNI引用的對象。

标記-清除算法(Mark-Sweep)

  最基礎的收集算法,分為”标記”和”清除”兩個階段:首先标記處所有需要回收的對象,在标記完成後統一回收所有被标記的對象。

  主要不足有兩個:一是效率問題,标記和清除兩個過程的效率都不高;另一個是空間問題,标記清除之後會産生大量不連續的記憶體碎片,空間碎片大多可能會導緻以後再程式運作過程中需要配置設定較大對象時,無法找到足夠的連續記憶體而不得不提前出發另一次垃圾收集動作。

複制算法(Copying)

  為了解決效率問題,一種稱為”複制“的收集算法出現,它将可用記憶體按容量劃分為大小相等的兩塊,每次隻使用其中的一塊。當這一塊的記憶體用完了,就将還存活的對象複制到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣使得每次都是對整個半區進行記憶體回收,記憶體配置設定時也就不用考慮記憶體碎片等複雜情況,隻要移動堆頂指正按順序配置設定記憶體即可,實作簡單,運作高效。隻是這種算法的代價是将記憶體縮小為原來的一般,未免太高了一點。

  現代的商業虛拟機都采用這種收集算法來回收新生代,IBM公司研究表明,新生代的對象98%都是”朝生夕死“的,是以并不需要1:1的比例來劃分記憶體空間,而是将記憶體劃分為一塊較大的Eden空間和兩塊較小的Survivor空間。HotSpot預設Eden和Survivor的大小比例是8:1.如果Survivor空間不夠用時,需要依賴其他記憶體(老年代)進行配置設定擔保(Handle Promotion)。

标記-整理算法(Mark-Compact)

  複制收集算法在對象存活率較高時就要進行較多的複制操作,效率将會變低。根據老年代的特點,提出此種算法,标記過程仍然與”标記-清除“算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界意外的記憶體。

分代收集算(Generational Collection)

  目前商業虛拟機的垃圾收集都采用”分代收集“算法。一般是把java堆分成新生代和老年代。在新生代中,每次垃圾收集時都發現有大批對象死去,隻有少量存活,那就選用複制算法,隻需要付出少量存活對象的複制成本就可以完成收集。而老年代中因為對象存活率高、沒有額外空間對它進行配置設定擔保,就必須使用”标記——清理“或者”标記——整理“算法來進行回收。

  HotSpot虛拟機的垃圾收集器如圖所示。

垃圾收集器與記憶體配置設定政策

Serial收集器

  這是一個單線程的收集器,但它的“單線程”的意義并不僅僅說明它隻會使用一個CPU或一條手機線程去完成垃圾手機工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。“Stop the world”,由虛拟機在背景自動發起和自動完成的,在使用者不可見的情況下把使用者正常工作的線程全部停掉,這對很多應用來說都是難以接受的。它是虛拟機運作在Client模式下的預設新生代收集器。

  優點:簡單而高效(與其他收集器的單線程比),對于限定單個CPU的環境來說,Serial收集器由于沒有線程互動的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。

ParNew收集器

  ParNew收集器其實就是serial收集器的多線程版本,除了使用多條線程進行垃圾收集之外,其餘行為與Serial收集器一樣。ParNew收集器也是使用-XX:+UseConcMarkSweepGC選項後的預設新生代收集器,也可以使用-XX:+UseParNewGC選項來強制指定它。

Parallel Scavenge收集器

  Parallel Scavenge收集器也是一個新生代收集器,它也是使用複制算法的收集器,又是并行多線程收集器。parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是盡可能地縮短垃圾收集時使用者線程的停頓時間,而parallel Scavenge收集器的目标則是達到一個可控制的吞吐量。吞吐量= 程式運作時間/(程式運作時間 + 垃圾收集時間),虛拟機總共運作了100分鐘。其中垃圾收集花掉1分鐘,那吞吐量就是99%。

  Parallel Scavenge收集器提供了兩個參數用于精确控制吞吐量,分别是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數以及直接設定吞吐量大小的-XX:GCTimeRatio參數。

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同樣使用一個單線程執行收集,使用“标記-整理”算法。主要使用在Client模式下的虛拟機。

Parallel Old收集器

  Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“标記-整理”算法。

CMS收集器

  CMS(Concurrent Mark Sweep)收集器是一種以擷取最短回收停頓時間為目标的收集器。CMS收集器是基于“标記-清除”算法實作的,整個收集過程大緻分為4個步驟:

初始标記(CMS initial mark)

并發标記(CMS concurrenr mark)

重新标記(CMS remark)

并發清除(CMS concurrent sweep)

  其中初始标記、重新标記這兩個步驟任然需要停頓其他使用者線程。初始标記僅僅隻是标記出GC ROOTS能直接關聯到的對象,速度很快,并發标記階段是進行GC ROOTS 根搜尋算法階段,會判定對象是否存活。而重新标記階段則是為了修正并發标記期間,因使用者程式繼續運作而導緻标記産生變動的那一部分對象的标記記錄,這個階段的停頓時間會被初始标記階段稍長,但比并發标記階段要短。

  由于整個過程中耗時最長的并發标記和并發清除過程中,收集器線程都可以與使用者線程一起工作,是以整體來說,CMS收集器的記憶體回收過程是與使用者線程一起并發執行的。

  CMS收集器的優點:并發收集、低停頓,但是CMS還遠遠達不到完美,器主要有三個顯著缺點:

  CMS收集器對CPU資源非常敏感。在并發階段,雖然不會導緻使用者線程停頓,但是會占用CPU資源而導緻引用程式變慢,總吞吐量下降。CMS預設啟動的回收線程數是:(CPU數量+3) / 4。

  CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure“,失敗後而導緻另一次Full GC的産生。由于CMS并發清理階段使用者線程還在運作,伴随程式的運作自熱會有新的垃圾不斷産生,這一部分垃圾出現在标記過程之後,CMS無法在本次收集中處理它們,隻好留待下一次GC時将其清理掉。這一部分垃圾稱為“浮動垃圾”。也是由于在垃圾收集階段使用者線程還需要運作,即需要預留足夠的記憶體空間給使用者線程使用,是以CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分記憶體空間提供并發收集時的程式運作使用。在預設設定下,CMS收集器在老年代使用了68%的空間時就會被激活,也可以通過參數-XX:CMSInitiatingOccupancyFraction的值來提供觸發百分比,以降低記憶體回收次數提高性能。要是CMS運作期間預留的記憶體無法滿足程式其他線程需要,就會出現“Concurrent Mode Failure”失敗,這時候虛拟機将啟動後備預案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。是以說參數-XX:CMSInitiatingOccupancyFraction設定的過高将會很容易導緻“Concurrent Mode Failure”失敗,性能反而降低。

  最後一個缺點,CMS是基于“标記-清除”算法實作的收集器,使用“标記-清除”算法收集後,會産生大量碎片。空間碎片太多時,将會給對象配置設定帶來很多麻煩,比如說大對象,記憶體空間找不到連續的空間來配置設定不得不提前觸發一次Full GC。為了解決這個問題,CMS收集器提供了一個-XX:UseCMSCompactAtFullCollection開關參數,用于在Full GC之後增加一個碎片整理過程,還可通過-XX:CMSFullGCBeforeCompaction參數設定執行多少次不壓縮的Full GC之後,跟着來一次碎片整理過程。

G1收集器(Garbage-First)

  G1是一款面向伺服器應用垃圾收集器,與其他GC收集器想必,G1具備以下特點:

并行與并發:G1能充分利用多CPU、多核環境下的硬體優勢,使用多個CPU來縮短Stop-The-World停頓的時間,部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過并發的方式讓Java程式繼續執行。

分代收集:與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不要其他收集器配合就能獨立管理整個GC堆,但它能夠采用不同的方式去處理新建立的對象和已經存活了一半時間、熬過多次GC的舊對象以擷取更好的收集效果。

空間整合:與CMS的“标記-清理”算法不同,G1從整體上看是基于“标記-整理”算法實作的收集器,從局部(兩個Region之間)上來看是基于“複制”算法實作,無論如何,這兩種算法都意味着G1運作期間不會産生記憶體空間碎片,收集後能提供規整的可用記憶體。

可預測的停頓:這是G1相對于CMS的另一個大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明确指定在一個長度為M毫秒的時間片段内,小号在垃圾收集上的時間不能超過N毫秒,這幾乎已經是實時Java(RTSJ)的垃圾收集器的特征了。

  在G1收集器中,Region之間的對象引用以及其他收集器中的新生代與老年代之間的對象引用,虛拟機都是使用Remembered Set來避免全堆掃描的。G1中每個Region都有一個與之對應的Remebered Set,虛拟機發現程式在對Reference類型的資料進行寫操作時,會産生一個Write Barrier暫時中斷寫操作,檢查Reference引用的對象是否處于不同的Region之中(在分代的例子中,就是檢查是否老年代中的讀寫引用了新生代中的對象)。如果是,便通過CardTable把相關引用資訊記錄到被引用對象所屬的Region的Remembered Set之中。當進行記憶體回收時,在GC根節點的枚舉範圍中加入Rememered Set即可保證不對全隊掃描也不會有遺漏。

  如果不計算維護Remembered Set的操作,G1收集器的運作大緻可劃分為以下幾個步驟:

初始标記

并發标記

最終标記

篩選标記

參數

描述

-XX:+UseSerialGC

Jvm運作在Client模式下的預設值,打開此開關後,使用Serial + Serial Old的收集器組合進行記憶體回收

-XX:+UseParNewGC

打開此開關後,使用ParNew + Serial Old的收集器進行垃圾回收

-XX:+UseConcMarkSweepGC

使用ParNew + CMS + Serial Old的收集器組合進行記憶體回收,Serial Old作為CMS出現“Concurrent Mode Failure”失敗後的後備收集器使用。

-XX:+UseParallelGC

Jvm運作在Server模式下的預設值,打開此開關後,使用Parallel Scavenge + Serial Old的收集器組合進行回收

-XX:+UseParallelOldGC

使用Parallel Scavenge + Parallel Old的收集器組合進行回收

-XX:SurvivorRatio

新生代中Eden區域與Survivor區域的容量比值,預設為8,代表Eden:Subrvivor = 8:1

-XX:PretenureSizeThreshold

直接晉升到老年代對象的大小,設定這個參數後,大于這個參數的對象将直接在老年代配置設定

-XX:MaxTenuringThreshold

晉升到老年代的對象年齡,每次Minor GC之後,年齡就加1,當超過這個參數的值時進入老年代

-XX:UseAdaptiveSizePolicy

動态調整java堆中各個區域的大小以及進入老年代的年齡

-XX:+HandlePromotionFailure

是否允許新生代收集擔保,進行一次minor gc後, 另一塊Survivor空間不足時,将直接會在老年代中保留

-XX:ParallelGCThreads

設定并行GC進行記憶體回收的線程數

-XX:GCTimeRatio GC

時間占總時間的比列,預設值為99,即允許1%的GC時間,僅在使用Parallel Scavenge 收集器時有效

-XX:MaxGCPauseMillis

設定GC的最大停頓時間,在Parallel Scavenge 收集器下有效

-XX:CMSInitiatingOccupancyFraction

設定CMS收集器在老年代空間被使用多少後出發垃圾收集,預設值為68%,僅在CMS收集器時有效,-XX:CMSInitiatingOccupancyFraction=70

-XX:+UseCMSCompactAtFullCollection

由于CMS收集器會産生碎片,此參數設定在垃圾收集器後是否需要一次記憶體碎片整理過程,僅在CMS收集器時有效

-XX:+CMSFullGCBeforeCompaction

設定CMS收集器在進行若幹次垃圾收集後再進行一次記憶體碎片整理過程,通常與UseCMSCompactAtFullCollection參數一起使用

-XX:+UseFastAccessorMethods

原始類型優化

-XX:+DisableExplicitGC

是否關閉手動System.gc

-XX:+CMSParallelRemarkEnabled

降低标記停頓

-XX:LargePageSizeInBytes

記憶體頁的大小不可設定過大,會影響Perm的大小,-XX:LargePageSizeInBytes=128m

Client、Server模式預設GC

服務端or用戶端

新生代GC方式

老年代和持久代GC方式

Client

Serial 串行GC

Serial Old 串行GC

Server

Parallel Scavenge

并行回收GC Parallel Old 并行GC

Sun/oracle JDK GC組合方式

Parallel Scavenge 并行回收GC

Serial Old 并行GC

ParNew 并行GC

CMS 并發GC 當出現“Concurrent Mode Failure”時,采用Serial Old 串行GC

-XX:+UseConcMarkSweepGC -XX:+UseParNewGC

CMS 并發GC 當出現“Concurrent Mode Failure”時采用Serial Old 串行GC

參考資料

1. 《深入了解Java虛拟機》周志明著