13.G1垃圾收集器

G1收集器是一款面向伺服器的垃圾收集器,也是HotSpot在JVM上力推的垃圾收集器,並賦予取代CMS的使命。為什麼對G1收集器給予如此高的期望呢?既然對G1收集器寄予了如此高的期望,那麼他一定是有其特別之處。他和其他的垃圾收集器有何不同呢?下面我們將從以下幾個方面研究G1收集器。

一、 為什麼會誕生G1收集器?

我們知道一個新事物的誕生並且能夠取代舊事物,那他一定具備了舊事物所不具備的優點。在G1之前,我們使用的是Serial、Parller、ParNew、CMS垃圾收集器,那麼這些收集器有什麼特點呢?

  1. 分代收集:整個堆空間分為新生代和老年代,新生代又分為Eden區,Survivor區。
  2. 垃圾收集觸發機制:新生代垃圾收集觸發機制是在新生代快要滿的時候,觸發垃圾回收;老年代也是如此,在老年代空間快要滿的時候觸發垃圾回收。
  3. 垃圾回收面臨的問題:Stop The World,這是所有垃圾回收面臨的嚴峻問題,因為Stop The World,很可能會影響用戶體驗。所以,CMS在Stop The World上面大做文章,讓耗時短的初始標記和重新標記Stop The World,而耗時長的並發標記,並發清除和用戶執行緒並發執行,以減少用戶感知。

以上是之前的垃圾收集器都遵循的特點。有的時候,優點同時也會成為瓶頸。能不能打破瓶頸,開闢出新的空間呢?既然有瓶頸,我們就逐個分析,看能夠各個擊破。

  1. 分代收集:放開思路,垃圾收集器一定都要分代么,分為年輕代和老年代?分代是為了方便收集,缺點是如果分配不合理,可能會浪費記憶體,頻繁觸發垃圾回收。那麼能不能不分代呢?
  2. 垃圾收集觸發機制:一定要等到記憶體快滿的時候才收集么?之前這麼設置的原因是為了減少垃圾收集的次數,降低對用戶體驗的影響。原因是垃圾回收的時候回Stop The World。如果能像CMS一樣,不STW,是不是就會減少對用戶的影響,或者STW的時間非常短,短到用戶根本無法感知,如果這樣的話,是不是就可以頻繁觸發垃圾回收了?不用等到達到極限的時候才觸發了?
  3. 用戶體驗:這也是終極問題,如何才能對用戶的影響最小,運行效率最高呢?

這些G1都更完美的實現了。所以G1才被官網如此寵愛。G1最大的特點是引入分區的思路,弱化了分代的概念,合理利用垃圾收集各個周期的資源,解決了其他收集器甚至CMS的眾多缺陷。

二、 GC收集器的三個考量指標

GC收集器的三個考量指標:

  • 佔用的記憶體(Capacity)
  • 延遲(Latency)
  • 吞吐量(Throughput)

這幾個方面G1表現得怎麼樣呢?

我們都知道隨著電腦的發展,硬體的成本是越來越低了,電腦的記憶體越來越大,原來最怕的就是GC的過程中佔用過多的記憶體資源,現在在大記憶體的時代也都能容忍了。

吞吐量如何解決呢?我們現在使用的都是分散式系統,可以通過擴容的方式來解決吞吐量的問題。

隨著JVM中記憶體的增大,垃圾回收的間隔變得更長,然後回收一次垃圾耗時也越來越多,現在STW的時間問題就是JVM需要迫切解決的問題,如果還是按照傳統的分代模型,使用傳統的垃圾收集器,那麼STW的時間將會越來越長。在傳統的垃圾收集器中,SWT的時間是無法預估的,那麼有沒有辦法能夠控制垃圾收集的時間呢?這樣我們可以將垃圾收集的時間設置的足夠短,讓用戶無感知,然後增加垃圾回收的次數。

G1就做了這樣一件事,它不要求每次都把垃圾清理的乾乾淨淨,它只是每次根據設置的垃圾收集的時間來收集有限的垃圾,其他的垃圾留到下一次收集。

我們對G1的要求是:在任意1秒的時間內,停頓不得超過10ms,這就是在給它制定KPI。G1會盡量達成這個目標,它能夠反向推算出本次要收集的大體區域,以增量的方式完成收集。

因此,G1垃圾回收器(-XX:+UseG1GC不得不設置的一個參數是:-XX:MaxGCPauseMillis=10

三、 G1垃圾收集器設計原理

  • G1的設計原則是”首先收集儘可能多的垃圾(Garbage First)”。因此,G1並不會等記憶體耗盡(串列、並行)或者快耗盡(CMS)的時候開始垃圾收集,而是在內部採用了啟發式演算法,在老年代找出具有高收集收益的分區進行收集。同時G1可以根據用戶設置的暫停時間目標自動調整年輕代和總堆大小,暫停目標越短年輕代空間越小、總空間就越大;
  • G1採用記憶體分區(Region)的思路,將記憶體劃分為一個個相等大小的記憶體分區,回收時則以分區為單位進行回收,存活的對象複製到另一個空閑分區中。由於都是以相等大小的分區為單位進行操作,因此G1天然就是一種壓縮方案(局部壓縮);
  • G1雖然也是分代收集器,但整個記憶體分區不存在物理上的年輕代與老年代的區別,也不需要完全獨立的survivor(to space)堆做複製準備。G1隻有邏輯上的分代概念,或者說每個分區都可能隨G1的運行在不同代之間前後切換;
  • G1的收集器對年輕代和老年代的收集界限比較模糊,採用了混合(mixed)收集的方式。即每次收集既可能只收集年輕代分區(年輕代收集),也可能在收集年輕代的同時,包含部分老年代分區(混合收集),這樣即使堆記憶體很大時,也可以限制收集範圍,從而降低停頓。

四、 Region區概念

G1將堆記憶體空間劃分成多個大小相等獨立的區域(Region),從下圖中我們可以看出這時一塊完整的空間。和CMS不同,這裡沒有物理上的分帶概念了,但每一個小格式在邏輯上還是有分代概念的。 JVM最多可以有2048個Region。一般Region大小等於堆空間大小除以2048,比如堆大小為4096M,則Region大小為2M。當然也可以用參數”- XX:G1HeapRegionSize”手動指定Region大小,但是推薦默認的計算方式。

G1保留了年輕代和老年代的概念,但不再是物理隔閡了,它們都是(可以不連續)Region的集合。每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。一個Region之前可能是新生代,在垃圾回收以後,這塊空間可能就變成老年代了。收集器能夠對扮演不同角色的Region採用不同的策略去處理,這樣無論是新創建的對象還是已經存活了一段時間、熬過多次收集的舊對象都能獲取很好的收集效果。

Region有五種狀態:

  1. 存放Eden區對象,
  2. 存放Survivor對象
  3. 存放Old對象
  4. 還有一類特殊的Humongous對象,專門用來存儲大對象。什麼事大對象呢?G1認為只要大小超過了一個Region容量一半的對象即可判定為大對象。對於那些超過了整個Region容量的超級大對象,將會被存放在N個連續的Humongous Region之中,G1的進行回收大多數情況下都把Humongous Region作為老年代的一部分來進行回收。

每個Region的大小可以通過參數-XX:G1HeapRegionSize設定,取值範圍為1MB~32MB,且應為2的N次冪。

G1垃圾收集器對於對象什麼時候會轉移到老年代跟之前講過的原則一樣,唯一不同的是對大對象的處理,G1有專門分配 大對象的Region叫Humongous區,而不是讓大對象直接進入老年代的Region中,這樣可以節約老年代的空間,避免因為老年代空間不夠的GC開銷。

默認年輕代對堆記憶體的佔比是5%,如果堆大小為4096M,那麼年輕代佔據200MB左右的記憶體,對應大概是100個 Region,可以通過「-XX:G1NewSizePercent」設置新生代初始佔比,在系統運行中,JVM會不停的給年輕代增加更多 的Region,但是最多新生代的佔比不會超過60%,可以通過「-XX:G1MaxNewSizePercent」調整。年輕代中的Eden和 Survivor對應的region也跟之前一樣,默認8:1:1,假設年輕代現在有1000個region,eden區對應800個,s0對應100 個,s1對應100個。

五、 G1收集器的運行原理

我們之前詳細研究過CMS垃圾收集器,G1的收集過程一部分和CMS差不多,下面來看看G1收集器的運行步驟:

1.初始標記(initial mark,STW)

這一部分和CMS的一樣,會Stop The World。

初始標記只是標記一下 GC Roots 能直接關聯的對象,速度很快,仍然需要暫停所有的工作執行緒(STW)。

這裡有一個詞「直接關聯」很重要,也就是我們只標記根節點GC Roots。以下面的程式碼為例說明:

public class Math {
    public static int initData = 666;
    public static User user = new User();
    public User user1;

    public int compute() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }

    public static void main(String[] args) {
        Math math = new Math();
        math.compute();
        Class<? extends Math> mathClass = math.getClass();
        new Thread().start();
    }
}

看main方法,在堆中創建了一塊空間new Math(), 棧中有一個變數指向了堆空間的地址,這裡math是一個根節點。初始標記的時候只會標記math這樣的根節點。new Math()裡面有一個成員變數User user1,這個變數不會被標記,因為他不是根節點GC Root。也就是說,在初始標記的時候只會標記GC Root,對象裡面的非GC Root不會被標記。

所以,這個過程是很快的。一個應用程式的根節點是有限的,沒有多少,所以標記的速度也很快。

為什麼初始標記要STW呢?因為如果不STW,那麼用戶執行緒會不停的創建新的對象,這樣就標記不完了。

2.並發標記(Concurrent Marking)

和CMS的並發標記類似。

並發標記進行 GC Roots 跟蹤的過程,和用戶執行緒一起工作,不需要暫停工作執行緒。

還是用Math類來說,在初始標記的時候,只標記GC Roots,接下來並發標記標記的是GC Root下面的其他對象,比如user1對象。相比於根節點來說,非根節點會更多,因此這個過程也會很慢。

這是可能會有各種情況發生,比如初始標記的時候被標記為垃圾的對象,通過並發標記不是垃圾了;也可能最開始的時候不是垃圾,但是經過並發標記後變成垃圾了。

當對象掃描完成以後,並發時引用變動的對象可能會產生多標和漏標的問題,多標不用處理,下次GC會重新標記。漏標的問題,G1中會使用SATB(snapshot-at-the-beginning)演算法來解決。

3.最終標記(Remark,STW)

和CMS的重新標記類似。有所不同的是,CMS在標記的時候使用的是增量標記,G1使用的是原始快照。

並發把所有的對象都標記完了。但是有些情況對象的垃圾狀態發生了變化,原來的垃圾對象現在不是垃圾了,或者原來的非垃圾對象後來變成垃圾了,這時就需要重新標記。重新標記就是為了修復在並發標記中,狀態已經改變的對象。比如:處理並發標記階段仍遺留下來的最後那少量的SATB記錄(漏標對象)。仍然需要暫停所有的工作執行緒。

4.篩選回收(Cleanup,STW)

​ 這個過程是和CMS不同的,CMS並發清理是和用戶執行緒並發執行的。而G1的篩選回收是Stop The World的。

​ 為什麼會選擇Stop The World呢?這時因為在篩選回收階段首先會對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間(可以用JVM參數 -XX:MaxGCPauseMillis指定)來制定回收計劃,可以自由選擇任意多個Region構成回收集,然後把決定回收的那一部分Region的存活對象複製到空的Region中,再清理掉整箇舊Region的全部空間。這裡的操作涉及存活對象的移動,是必須暫停用戶執行緒,由多個收集器執行緒並行完成的。

​ 比如說老年代此時有1000個 Region都滿了,但是我們設置了預期停頓時間-XX:MaxGCPauseMillis=200ms,本次垃圾回收可能只能停頓200毫秒。如果要對1000個Region全部進行垃圾回收,需要超過200ms,那麼這時通過之前回收成本計算得出,回收其中800個Region剛好需要200ms,那麼這次就只會回收800個Region(Collection Set,要回收的集合),盡量保證GC導致的停頓時間控制在我們指定的範圍內,保證不影響用戶的體驗。多出來的200個Regin怎麼辦呢?下次GC的時候再回收。

​ 這個階段其實也可以做到與用戶程式一起並發執行,但是因為只回收一部分Region,時間是用戶可控制的,而且停頓用戶執行緒將大幅提高收集效率。

六、 G1回收的過程

其實G1回收的過程中使用的是「複製」演算法。比如老年代有一塊空間要回收了,他是怎麼做的呢?之前在標記階段,我們在老年代標記了很多非垃圾對象,把這些非垃圾對象複製到相鄰的還沒有被佔用的空里去。然後把原來那塊空間直接個清理掉。這是G1底層挪動對象的演算法。那麼G1和CMS有什麼區別呢?CMS底層使用的是「標記-清除」,而G1底層使用的是「標記-複製」,他們的區別是—碎片。複製最好的一個地方就是複製過去以後會產生很少的記憶體碎片。雖然G1底層使用的是複製演算法,但最終達到的效果和「標記-整理」是一樣的。

問題:以下面這個圖為例,老年代有6塊Region需要被回收,但是根據設置的收集時間,只能回收3塊。那麼應該回收哪3塊呢?

G1收集器會按照回收收益比去選擇。有一定的演算法,按照演算法計算選擇的。

G1收集器在後台維護了一個優先列表,每次根據允許的收集時間,優先選擇回收價值最大的Region(這也就是它的名字 Garbage-First的由來),比如一個Region花200ms能回收10M垃圾,另外一個Region花50ms能回收20M垃圾,在回 收時間有限情況下,G1當然會優先選擇後面這個Region回收。這種使用Region劃分記憶體空間以及有優先順序的區域回收方式,保證了G1收集器在有限時間內儘可能多的回收垃圾。

整個過程哪一塊最耗時呢?複製的過程最耗時,Region塊中的對象越多,越耗時,垃圾回收的效益比就越低。清理原來的空間是很快的。還是上面的案例, 有一塊Region空間只有一個對象需要複製,另一塊Region空間,有50個對象需要複製,在選擇的時候,垃圾收集器會優先選擇複製只有一個對象的空間。這樣耗時少,騰出的空間卻很大。

不管是年輕代或是老年代,回收演算法主要用的是複製演算法,將一個region中的存活對象複製到另一個region中,這種不會像CMS那樣 回收完因為有很多記憶體碎片還需要整理一次,G1採用複製演算法回收幾乎不會有太多記憶體碎片。(注意:CMS回收階 段是跟用戶執行緒一起並發執行的,G1因為內部實現太複雜暫時沒實現並發回收,不過到了Shenandoah就實現了並 發收集,Shenandoah可以看成是G1的升級版本)

七、 G1垃圾收集器的分類

G1垃圾收集分為三種:一種是YoungGC,一種是MixedGC,另一種是Full GC

1.YoungGC

​ YoungGC就是MinorGC,原來的垃圾收集器都是Eden區放滿了就出MinorGC,但是G1有所不同。之前說過,新生代占整個記憶體的5%,Eden區和Survivor的比例是8:1:1,不到5%。假如這些空間全部都放滿了,會怎麼樣呢?是不是就立刻觸發MinorGC了呢?不是的。那麼何時觸發minorGC呢?觸發MinorGC的時間和-XX:MaxGCPauseMillis參數的值有關係。假如-XX:MaxGCPauseMillis=200ms,G1會計算回收這5%的空間耗時是不是接近200ms,如果是,那麼就會觸發MinorGC。如果不是,假如只有50ms,遠遠低於200ms,那麼就不會觸發MinorGC,他會把新的對象放到新的沒有被佔用的Region區中。直到Eden園區回收的時間接近200ms了,這時才會觸發MinorGC。這就是剛開始新生代設置的空間是5%,但是在實際運行的過程中,很可能會超過5%,最大不能超過60%,60%是默認值,這個值可以通過「-XX:G1MaxNewSizePercent」參數調整。

​ 也就是說,YoungGC並不是說現有的Eden區放滿了就會馬上觸發,G1會計算下現在Eden區回收大概要多久時間,如果回收時間遠遠小於參數 -XX:MaxGCPauseMills 設定的值,那麼增加年輕代的region,繼續給新對象存放,不會馬上做Young GC,直到下一次Eden區放滿,G1計算回收時間接近參數 -XX:MaxGCPauseMills 設定的值,那麼就會觸發Young GC 。

​ G1非常的重視最大停頓時間,所以會非常重視回收效益比,所以G1的性能是比較高的, 性能高帶來的後果就是,G1底層的演算法會比CMS複雜很多。演算法細節也會比CMS多很多。演算法複雜,對於大記憶體的機器來說會比較有效果,但是對於記憶體不太大的機器來說,運行效果可能還不如CMS,這就是為什麼很長一段時間,jdk8這個版本還是用的CMS+ParNew。在jdk8版本的時候,已經有G1垃圾收集器了,但是對G1底層演算法還沒有優化的很好,直到jdk9,把G1演算法再優化以後,且記憶體增大以後,效率才越來越高。

2.MixedGC

MixedGC和之前的FullGC優點相似,但他不是Full GC。G1有專門的Full GC。當老年代的堆佔有率達到參數(-XX:InitiatingHeapOccupancyPercent)設定的值時則觸發Mixed GC。回收時會回收所有的 Young區和部分Old區以及大對象區。為什麼是一部分的Old區呢?它會根據GC的最大停頓時間來計算最高效益比,來確定old區垃圾收集的先後順序。

正常情況G1的垃圾收集是先做 MixedGC,主要使用複製演算法,需要把各個region中存活的對象拷貝到別的region里去,拷貝過程中如果發現沒有足夠 的空region能夠承載拷貝對象就會觸發一次Full GC.

3.Full GC

Full GC會停止應用執行緒,然後採用單執行緒進行收集,就類似於Serial Old垃圾收集器。採用單執行緒進行標記、清理和壓縮整理,目的是騰出一批Region來供下一次MixedGC使用,這個過程是非常耗時的,單執行緒的效率是很低的。(Shenandoah優化成多執行緒收集了)

什麼時候會觸發Full GC呢?

在老年代,需要把region中存活的對象拷貝到別的region中去的時候,拷貝過程中發現沒有足夠的空region能夠承載的拷貝對象了,就會觸發Full GC。舉個例子:假如MixedGC觸發的條件-XX:InitiatingHeapOccupancyPercent=45%,而剩餘50%的空間被新生代佔了。那麼還剩5%的空間。當-XX:InitiatingHeapOccupancyPercent的值達到了45%,觸發MixedGC的時候,這個時候需要複製老年代對象到新的未被佔用的Region區,很顯然這時沒有足夠的Region區,這時會觸發Full GC。

八、 總結G1收集器的特點

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

  • 分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念。

  • 空間整合:與CMS的「標記–清理」演算法不同,G1從整體來看是基於「標記整理」演算法實現的收集器;從局部 上來看是基於「複製」演算法實現的。

  • **可預測的停頓: **這是G1相對於CMS的另一個大優勢,降低停頓時間是G1 和 CMS 共同的關注點,但G1 除了 追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段(通過參數”- XX:MaxGCPauseMillis”指定)內完成垃圾收集。

九、 G1收集器參數設置

-XX:+UseG1GC:使用G1收集器
  
-XX:ParallelGCThreads:指定GC工作的執行緒數量 
  
-XX:G1HeapRegionSize:指定分區大小(1MB~32MB,且必須是2的N次冪),默認將整堆劃分為2048個分區. 
  
-XX:MaxGCPauseMillis:目標暫停時間(默認200ms)
  		也就是垃圾回收的時候允許停頓的時間
  
-XX:G1NewSizePercent:新生代記憶體初始空間(默認整堆5%)
  
-XX:G1MaxNewSizePercent:新生代記憶體最大空間,默認是60%。
  
-XX:TargetSurvivorRatio:Survivor區的填充容量(默認50%),Survivor區域里的一批對象(年齡1+年齡2+年齡n的多個
年齡對象)總和超過了Survivor區域的50%,此時就會把年齡n(含)以上的對象都放入老年代 
  
-XX:MaxTenuringThreshold:最大年齡閾值(默認15) ,當在年輕代經歷了15次GC還沒有被回收掉,那麼進入老年代。
  
-XX:InitiatingHeapOccupancyPercent:老年代佔用空間達到整堆記憶體閾值(默認45%),則執行新生代和老年代的混合收集(MixedGC),比如我們之前說的堆默認有2048個region,如果有接近1000個region都是老年代的region,則可能 就要觸發MixedGC了。
  			也就是MixedGC觸發的條件
  
-XX:G1MixedGCLiveThresholdPercent(默認85%) region中的存活對象低於這個值時才會回收該region,如果超過這 個值,存活對象過多,回收的的意義不大。
  		在我們回收老年代的時候,我們需要知道老年代每個region中有多少存活對象。比如一個region中有100個對象,其中有85個是存活對象,垃圾對象是15個。那麼這樣的region回收的意義不大。反過來,如果有80格式垃圾對象,存活對象有隻有20個,那麼這時候就應該被回收。
  
-XX:G1MixedGCCountTarget:在一次回收過程中指定做幾次篩選回收(默認8次),在最後一個篩選回收階段可以回收一 會,然後暫停回收,恢復系統運行,一會再開始回收,這樣可以讓系統不至於單次停頓時間過長。
  
-XX:G1HeapWastePercent(默認5%): gc過程中空出來的region是否充足閾值,在混合回收的時候,對Region回收都 是基於複製演算法進行的,都是把要回收的Region里的存活對象放入其他Region,然後這個Region中的垃圾對象全部清 理掉,這樣的話在回收過程就會不斷空出來新的Region,一旦空閑出來的Region數量達到了堆記憶體的5%,此時就會立 即停止混合回收,意味著本次混合回收就結束了。

十、 G1收集器優化建議

假設參數 -XX:MaxGCPauseMills 設置的值很大,導致系統運行很久,年輕代可能都佔用了堆記憶體的60%了,此時才

觸發年輕代gc。 那麼存活下來的對象可能就會很多,此時就會導致Survivor區域放不下那麼多的對象,就會進入老年代中。 或者是你年輕代gc過後,存活下來的對象過多,導致進入Survivor區域後觸發了動態年齡判定規則,達到了Survivor

區域的50%,也會快速導致一些對象進入老年代中。

所以這裡核心還是在於調節 -XX:MaxGCPauseMills 這個參數的值,在保證他的年輕代gc別太頻繁的同時,還得考慮 每次gc過後的存活對象有多少,避免存活對象太多快速進入老年代,頻繁觸發mixed gc.

十一、G1的使用場景

  1. 50%以上的堆被存活對象佔用
  2. 對象分配和晉陞的速度變化非常大
  3. 垃圾回收時間特別長,超過1秒
  4. 8GB以上的堆記憶體(建議值)
  5. 停頓時間是500ms以內

十二、每秒幾十萬並發量的系統如何優化JVM

需求:現在有一個每秒有幾十萬並發量的需求,該如何優化呢?是一台伺服器有幾十萬並發。

分析:通常我們的伺服器是4核8G,承載每秒上千上萬的並發量應該都還可以。但是幾十萬上百萬的並發量,4核8G的配置肯定是承受不住的。 為什麼受不住呢?我們設想每個執行緒請求產生的對象是1kb,有100萬並發進來,1秒鐘將產生多少垃圾呢?1k*100萬/1024=976M的垃圾,將近1G的垃圾,這樣的話,過不了幾秒就要觸發一次Full GC。這樣GC將會很頻繁,這是不可以的,GC過於頻繁,而且垃圾堆積肯定會影響用戶的體驗。所以,4核8G不滿足我們的需求。假如使用了4核8G會產生什麼樣的後果呢?來分析一下:

上圖是根據參數配置的記憶體空間。

‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8

根據分析,我們知道,當第10s進行垃圾回收的時候,首先會STW,這時候前9s的對象都已經變成了垃圾,但是最後1s的對象不是垃圾,會被放到survivor區。976M遠大於Survivor區的200M,直接進入老年代。老年代也放不下,就會觸發Full GC,然後Full GC還沒有處理完,新的垃圾又來了,最後就會觸發OOM。

那麼,這種高並發量的問題如何解決呢?

我們知道kafka的並發量非常大,每秒可以處理幾十萬甚至上百萬的消息,可以借鑒kafka的處理思想。一般來說部署kafka需要用大記憶體機器,比如64G。如果我們要處理幾十甚至上百萬的並發消息,也用64G記憶體的伺服器,堆記憶體該如何分配呢?

按照之前的經驗,其實大部分對象在1s內就已經死亡了,而這些對象都是放在Eden區,Eden區對象有朝生夕死的特點。對象很大,我們就給新生代分配更大的記憶體空間,比如三十或者四十G。但是,如果給新生代分配三四十G也會有問題。以前常說的對於eden區的young gc是很快的,這種情況下它的執行還會很快嗎? 通常Eden區執行是很快的,但這裡有三四十個G,就是遍歷對象也會耗用很長時間。假設三四十G記憶體回收可能最快也要幾秒鐘,按kafka這個並發量放滿三 四十G的eden區可能也就一兩分鐘吧,那麼意味著整個系統每運行一兩分鐘就會因為young gc卡頓幾秒鐘沒法處理新消 息,顯然是不行的。

對於這種情況如何優化呢?我們可以使用G1收集器,設置 -XX:MaxGCPauseMills 為50ms,假設50ms能夠回收三到四個G記憶體,一共有三四十G,先回收三四G,剩下的下次在回收啊。50ms的卡頓用戶也是完全能夠接受的,幾乎無感知,那麼整個系統就可以在卡頓幾 乎無感知的情況下一邊處理業務一邊收集垃圾。

G1天生就適合這種大記憶體機器的JVM運行,可以比較完美的解決大記憶體垃圾回收時間過長的問題。

通常4~6G使用CMS;8G以上使用G1