天天看點

java垃圾回收精華java垃圾回收精華

本文系翻譯: 原文位址:mechanical-sympathy.blogspot.com/2013/07/java-garbage-collection-distilled.html

java垃圾回收精華

串行(Serial),并行(Parallel),并發(Concurrent),CMS,G1,年輕代(Young Gen),新生代(New Gen),老生代(Old Gen),永久代(Perm Gen),伊甸區(Eden), 年老區(Tenured), 幸存區(Survivor Spaces),安全點(Safepoints),和數百種JVM啟動标志。當你試圖調優垃圾回收器,使你的java應用能獲得所需要的吞吐量和延遲,這些概念難到你了嗎?如果它們使你困惑,我相信很多人也正和你一樣。閱讀垃圾回收的文檔感覺就像是閱讀飛機的幫助文檔一樣。每一個旋鈕和儀表盤都有詳細的解釋。但是沒有地方指導你怎麼讓它能飛起來。本文将試圖解釋在特定的工作中選擇和調優垃圾回收算法的一些權衡點。

 我們主要關注通常使用的Oracle的Hotspot JVM 和OpenJDK的收集器。在最後我們會讨論其他商業的JVMS來說明其他的方案。

權衡點(The Tradeoffs)

俗話說:“從來沒有不勞而獲”。當我們得到某些事物的時候,通常不得不需要放棄另外一些事物,當談論垃圾收集的時候,我們主要考慮三個收集器的名額:

1、吞吐量:花費在GC上的時間占整個應用程式工作的比例。通過‑XX:GCTimeRatio=99設定目标吞吐量,99表示1%的時間用于GC。

2、延遲:因為垃圾回收,而引起的響應暫停的時間。通過‑XX:MaxGCPauseMillis=<n>設定目标GC暫停的延遲。

3、記憶體:我們的系統使用記憶體來存儲狀态,在管理的時候它們常常需要複制和移動。在任意一個時間點系統中剩餘的存活對象稱之為存活集( Live Set)。通過–Xmx<n> 設定最大堆的大小,進而調節在應用程式中可用堆的大小。

注:通常Hotspot并不能達到這些目标,并且即使已經大大的偏離了目标,任然會沒有任何警告繼續運作。

延遲的影響會穿插在整個運作過程當中。可能我們能夠接受增加一些平均短的延遲,來減少最壞情況下的延遲,或則使其較不頻繁。術語“實時”并不是我們認為的盡可能最低的延遲。而是在不考慮吞吐量的時候,有一個明确的短的延遲。

對于某些大任務的應用來說,吞吐量是最重要的名額。比如一個長期運作的批處理作業,如果偶爾暫停幾秒來垃圾回收并不要緊,隻要整體的工作可以盡早的完成即可。

而對于幾乎所有其他的應用,從直面人類使用者的應用程式到金融交易系統,如果出現系統在幾秒甚至有時候幾毫秒無響應,将導緻災難性的後果,在金融交易系統中,往往需要犧牲一些吞吐量來換取一緻的延遲。我們也有可能需要應用程式限制實體記憶體,必須控制它占用的空間,在這種場景下,我們必須放棄延遲和吞吐量方面的性能考量。

權衡的通用結論如下:

  作為均攤的成本,垃圾回收在很大程度上可以通過使用更大的記憶體和相應的垃圾回收算法來消減成本。

  可以觀察到在最壞情況下,由延遲引發的響應暫停。可以通過限制存活集(live set),保持堆的大小在小的範圍來減少。

  暫停發生的頻率可以通過管理堆和代的大小,并且控制應用程式的對象配置設定率來減少。

  長時間暫停的頻率可以通過并行運作GC和應用程式來減少,但有時會影響吞吐量。

 對象生命周期

垃圾回收算法的優化通常都是期望大部分對象隻有很短的生命周期,隻有少部分對象有較長的生命周期。在大部分應用中,大部分對象的生命周期限制在一個明确的時間段裡,小部分對象的生命周期貫穿整個JVM生命周期。在垃圾收集理論中,這種現象通常被稱為“infant mortality(嬰兒死亡率,大量對象生存時間很短)” 或則  “weak generational hypothesis(弱年代假設)”。例如:循環疊代内的變量大多生命周期短暫,而靜态字元串則在JVM整個生命周期中都有效。

實驗表明,分代垃圾收集器的吞吐量通常比非分代垃圾回收器有一個數量級的提升,因而幾乎在所有的伺服器的JVM中,通常把對象分代。我們發現新配置設定的對象所在區域能存活的對象是非常稀疏的。是以使用一個收集器清理這個新生代裡面少數活着的對象,并且将它們拷貝到老生代裡是非常有效的。Hotspot垃圾回收器使用在GC周期中幸存的次數來作為一個對象的年紀。

注:如果你的應用程式不斷的産生大量的對象,并且存活相當長的時間,可以預見你的應用程式将會花費一段長的時間去回收垃圾,同樣可以預計到你也将花費一段時間來調優Hotspot的垃圾回收器。這是由于這種情況下分代的“過濾器”不太有效。并且結果還會導緻存活代的收集更頻繁,時間更長。老生代是緊密的,是以老生代的收集算法的效率會更低。分代垃圾回收器往往分為兩個不同回收周期:針對短時間存活對象的回收的新生代回收(Minor collections)和對年老區回收的更低頻率的老年代回收(Major collections)

世界為之暫停(Stop-The-World Events)

在垃圾回收過程中的應用程式暫停被稱之為“世界暫停事件(stop-the-world events)”。在實際工程中由于記憶體管理的需要,定期暫停正在運作的程式,對于垃圾回收器來說是必須的。根據不同的算法,不同的回收器在不同的時間,在不同的執行點上暫停應用程式(stop-the-world)。為了暫停整個應用程式,首先要暫停所有正在運作的線程。當系統在一個“安全點”的時候,垃圾回收器通過發送一個信号讓線程暫停,并開始垃圾回收,“安全點”是指在程式執行中,所有的GC根對象是已知的,并且所有的堆對象的内容是一緻的時間點。依賴于線程正在做的事情,它将花費一些時間達到“安全點”。“安全點”的檢查通常是執行方法的傳回,或則循環邊界結束,但是可以進行優化,在某些時候可以更加動态的判斷。比如:一個線程正在複制一個大的數組,克隆一個大的對象,或者執行一個有限次的單純計數的循環。它可能需要幾毫秒才能到達下一個“安全點”。對于低延遲的應用,到達安全點的時間(TTSP)是非常重要的。除了其他的GC标志之外,啟用‑XX:+PrintGCApplicationStoppedTime 标志可以輸出這個時間。

注:對于有大量正在運作的線程的應用程式來說,當暫停應用程式(stop-the-world)發生時,系統将會發生明顯的排程壓力。并在結束後恢複。是以較少的依賴暫停應用程式(stop-the-world)的算法将會更加有效。

Hotspot中的堆結構

去了解不同的收集器的方式,是探讨java堆結構如何支援分代機制的最好的方式。

伊甸區(Eden)的大部分對象都是剛剛被配置設定的。幸存區(survivor)是臨時存儲那些從伊甸區(Eden)裡幸存下來的對象。當我們讨論新生代回收(minor collections)的時候将描述幸存區(survivor)的用途。伊甸區(Eden)和幸存區(survivor)常常統稱為“年輕代(young)”或則“新生代(new)”

存活足夠久的對象,将最終移到年老(tenured )區裡。

永久代也是運作時存放對象的區域,它存儲像類(Classes)和靜态字元串(static Strings)一樣不被銷毀的對象。不幸的是在許多應用程式中,在持續運作的前提下,類加載的通常有一個激進的假設:即類是不會銷毀的。在java 7中的本地化的String會從永久(permgen)代移動到年老(tenured)區。并且java 8從HotSpot虛拟機中删除了“永久代(Permanent Generation),這不再本文的讨論範圍裡。大部分其他的商業收集器不使用一個單獨的永久代,而是往往把所有長期存活的對象放到老生代裡面。

注:虛拟空間(Virtual spaces)允許收集器調整區的大小,以滿足延遲和吞吐量的要求。收集器對每一次的收集做統計,并調整相應區的大小,來達到目标。

對象的配置設定

為了避免競争,每一個線程都配置設定一個線程本地配置設定緩沖區(Thread Local Allocation Buffer (TLAB)),線程在其中配置設定對象。使用TLABs允許對象配置設定的規模等于線程的數量,避免了單個記憶體資源的競争問題。憑借TLAB對象配置設定是一個廉價的操作。它簡單的為對象的大小配置設定一個指針,大部分平台上大約需要10個指令。java堆記憶體的配置設定比C在運作時使用malloc 函數配置設定記憶體更加廉價。 

注:鑒于個别對象配置設定是很廉價的,小集合配置設定的速率與對象配置設定的速度是成正比的。

當一個TLAB被耗盡率,線程可以簡單從伊甸區(Eden)請求一個新的。當伊甸區(Eden)用完後,開始一次新生代回收(minor collection)。

 大對象(-XX:PretenureSizeThreshold=<n>)在年輕代(young generation)的配置設定可能失敗,是以必須配置設定在老年代(old generation),比如:大數組。

如果門檻值的設定低于TLAB大小,适合在TLAB的對象将不會建立在老生代(old generation)。新的G1收集器在處理大對象的時候有所不同,在後面單獨的部分讨論。

 新生代的回收(Minor Collections)

當伊甸區(Eden)填滿之後,觸發一次新生代回收(Minor Collections)。通過将所有在新生代裡存活的對象适當的複制到幸存區(survivor space)和年老區(tenured space)來完成。複制到年老區(tenured space)通常稱為晉升(promotion)或則老年化(tenuring)。晉升針對那些足夠老的對象(– XX:MaxTenuringThreshold=<n>),或者幸存空間(survivor space)溢出。

存活的對象是指那些應用程式可以通路到的對象,不能通路的其他任何對象,可以被認為是死的。在新生代的收集(minor collection)中,存活對象的複制是通過從GC根對象(GC Roots)開始,反複地複制任何從GC根對象可到達的對象到幸存區(survivor space)來完成的。

GC根對象(GC Roots)通常包括應用程式、JVM内部的靜态字段和線程堆棧幀的引用,所有的這些有效的引用,構成了應用程式可到達對象的圖譜。

在分代收集中,新生代可到達對象圖譜的GC根(GC Roots)還包括老生代對新生代的任何引用。這些引用也必須進行處理,以確定在新生代裡面所有可到達對象在新生代的回收(minor collection)後任然是存活的。通過使用了“卡表(card table)”識别這些跨代引用。Hotspot 的卡表是一個bytes數組,其中每個位元組(byte)用于跟蹤的在相應的老生代的512位元組區域裡可能存在跨代引用,引用被存儲在堆裡,“store屏障(store barrier)”代碼将标記卡表(card table)的卡片來表明在相關的512位元組的堆裡面從老生代到新生代可能存在的一個潛在引用。  在收集時卡片表(card table)被用于掃描跨代引用,結果作為在新生代中有效的GC根(GC Roots)。是以在新生代收集(minor collections)中一個重要的固定成本是與老生代的大小成正比的。

在Hotspot裡面新生代有兩個幸存區(survivor spaces),交替的扮演“to-space”和“from-space”的角色。在新生代垃圾回收開始時,作為一個新生代回收中複制的目标區域,to-space的幸存區(survivor spaces)通常是空的。from-space的幸存區(survivor spaces)的一個組成部分是上一次新生代回收的目标幸存區(survivor space),和伊甸(Eden)區一樣,裡面的存活對象都需要複制到目标幸存區。

新生代回收的主要消耗就是複制對象到幸存區和年老區(tenured spaces)。在新生代回收中不存在對死亡對象的處理消耗。新生代回收的

 工作量直接與存活對象的數量相關,與新生代的大小無關。伊甸(Eden)區的大小每增加一倍,新生代回收的總時間幾乎會減少一半。是以,可以在記憶體和吞吐量中獲得平衡。伊甸(Eden)的大小翻倍,每一次收集周期裡的收集時間會增加,但是如果需要晉升(promoted)的對象數量和老生代的大小是固定的,那麼增加的時間是很少的。

注:在Hotspot中新生代是收集會導緻暫停應用(stop-the-world events),這在我們的堆越來越大和存活對象越來越多的情況下會是一個很大的問題。我們已經開始看到新生代中使用并發收集來達到減少暫停時間目标的需要。

老生代的收集(Major Collections):

老生代的收集(Major Collections)是指在老生代(old generation)上的垃圾收集,收集的對象是從年輕代晉升上來的對象。在大多數應用中,絕大部分的程式狀态都會在老年代裡結束生命周期。在老年代上存在的GC算法也是最多的。有一些是整個空間填滿時開始壓縮,另一些是回收與應用程式并行,提起防止整個空間填滿。

老年代的收集器會預測什麼時候需要收集,以避免年輕代的晉升失敗。收集器跟蹤設定在老年代上的門檻值,一旦門檻值被超過,則開始一次回收。如果這個門檻值不能滿足晉升需求,則觸發一次“FullGC”。一次FullGC将涉及從年輕代上晉升中的所有對象,并且壓縮老年代。晉升失敗是非常昂貴的操作,因為所有這個周期裡的狀态和晉升對象都必須回到原來的地方,然後觸發FullGC。

注:為了避免晉升失敗,你需要調整你的填充空間(為晉升失敗保留的buffer)),讓老年代可以容納晉升後的對象(‑XX:PromotedPadding=<n>)

注:當一次FullGC後堆需要增長 。可以通過将–Xms 和 –Xmx設定為一樣的值,來避免在FullGC時的堆調整大小。

與FullGC相比,一次對老生代的壓縮(compaction)可能是應用程式會經曆的最長的暫停應用(stop-the-world)。壓縮的時間和在年老區(tenured space)中存活對象的數量成線性增長關系。

年老區(tenured space)的填充速率可以通過增加幸存區(survivor spaces)的大小和延長晉升到老年區(tenured space)前的存活時間來減少。但是,由于在新生代收集(Minor collections)中,在幸存區之間的複制成本增加,幸存區(survivor spaces)大小的增加和在延長在晉升之前在新生代收集(Minor collections)(–XX:MaxTenuringThreshold=<n>)的存活時間,也會增加新生代收集(Minor collections)的成本和暫停時間,

串行收集(Serial Collector)

串行收集(Serial Collector)是最簡單的收集器,并且對于單處理器的系統也是最好的選擇。也是所有收集器裡面使用最少的。對于新生代的收集和老生代的收集均使用一個單獨的線程。在年老區的對象使用簡單的空閑指針(bump-the-pointer)算法(譯者:按照這種技術,JVM内部維護一個指針(allocatedTail),它始終指向先前已配置設定對象的尾部,當新的對象配置設定請求到來時,隻需檢查代中剩餘空間(從allocatedTail到代尾geneTail)是否足以容納該對象,并在“是”的情況下更新allocatedTail指針并初始化對象。下面的僞代碼具體展示了從連續記憶體塊中配置設定對象時配置設定操作的簡潔性和高效性)即可。當老年代填滿後會觸發老年代收集。

并行收集(Parallel Collector)

并行收集器有兩種形式。一種是并行收集器(-XX:+ UseParallelGC),它在新生代的收集中使用多線程來執行,在老生代的收集中使用單線程執行。另一種是從java 7U4開始預設使用并行老生代收集器(Parallel Old collector )(‑XX:+UseParallelOldGC),它在新生代的收集和老生代的收集均使用多線程。在年老區的對象使用簡單的空閑指針(bump-the-pointer)算法即可。當老生代填滿後會觸發老生代收集。

在多處理器系統上并行老生代收集器(Parallel Old collector )在所有收集器中有最大吞吐量。隻有收集開始時它才會影響到正在運作的程式,然後使用的最有效的算法并行的多個線程的收集。這使得并行老生代收集器(Parallel Old collector )非常适合批處理應用。

剩餘存活的對象的數量比堆的大小對收集老生代的成本影響更大。是以可以通過使用更大的記憶體和接受暫停的時間更長但是次數更少來提高并行老生代收集器(Parallel Old collector)的效率,以提供更大的吞吐量。

因為對象晉升到老年區是一個簡單的空閑指針(bump-the-pointer)和複制操作,可以預期這個對新生代的收集是最快的。

對于服務性應用程式來說,并行老生代收集器(Parallel Old collector )必須首先保持對端口的調用。如果老年代的收集暫停超過了你應用程式的容忍,你需要考慮使用可以與應用程式并發執行的并發收集器來收集老生代的對象,

注:基于現代的硬體,對老生代的壓縮每GB的存活對象預計需要暫停一到五秒。

注:在多插槽CPU的伺服器應用程式中使用-XX:+ UseNUMA  并行收集器有時能獲得更好的性能,它的伊甸區(Eden)的配置設定是線上程本地的CPU插槽上,可惜的這個功能是不提供給其他收集器。

并發标記清理收集器( Concurrent Mark Sweep (CMS) )

CMS(-XX:+ UseConcMarkSweepGC)收集器在老生代中使用,收集那些在老生代收集中不可能再到達的年老對象。它與應用程式并發的運作,在老生代中保持一直有足夠的空間以保證不會發生晉升失敗。

晉升失敗将會觸發一次FullGC,CMS按照下面多個步驟處理:

1、初始标記:尋找GC根對象;

2、并發标記:标記所有從GC根開始可到達的對象;

3、并發預清理:檢查被更新過的對象引用和在并發标記階段晉升的對象。

4、重新标記:捕捉預清潔階段以來已更新的對象引用。

5、并發清理:通過回收被死對象占用的記憶體更新可用空間清單。

6、并發重置:重置資料結構為下一次運作做準備。

當年老對象變成不可到達,占用空間被CMS回收并且放入到空閑空間清單中。當晉升發生的時候,會查詢空閑空間清單,為晉升對象找到适合的空間。這增加了晉升的成本,進而相比并行收集器也增加了新生代收集的成本。

 注:CMS 不是壓縮收集器,随着時間的推移在老生代中會導緻碎片。對象晉升可能失敗,因為一個大的對象可能在老生代在找不到一個可用空間。當發生這樣事件後,會記錄一條“晉升失敗”的消息,并且觸發一次FullGC來壓縮存活的年老對象。對于這種壓縮驅動的FullGCs,可以預計相比在老生代中使用并行老生代收集器(Parallel Old collector )暫停的時間為更長,因為CMS使用單線程壓縮。

CMS盡可能的與應用程式并發運作,它具有許多含義。首先,由于收集器會占用CPU的時間,是以CPU可用于應用程式的時間減少。CMS消耗的時間量與晉升到老年區的對象數量呈線性關系。第二、對于并發GC周期中的某些階段,所有的應用線程必須到達一個安全點,比如标記GC根和執行并行的重新标記檢查更新。

注:如果一個應用程式年老區的對象發生非常明顯的變化,重新标記階段将是非常耗時的,在極端情況下,它可能比一個完整的并行老生代收集器(Parallel Old collector)的壓縮時間還要長。

CMS通過降低吞吐量、更費時的新生代的收集,更大的空間占用,來降低FullGC的頻率。 根據不同的晉升率,與并行收集(Parallel Collector)相比吞吐量減少10%-40%。CMS也要求多于20%的空間來存放額外的資料結構和“漂浮垃圾(floating garbage)”,漂浮垃圾是值在并發标記階段丢掉的,到下一個收集周期處理的對象。

高晉升率和由此産生的碎片,可以通過增加新生代和老生代空間的大小來降低。

注:當CMS收集的空間不能滿足晉升的時候,它可能遇到“并發模式失敗”,在日志中可以找到記錄。産生這種情況的一個原因是收集的太遲了,這樣可以通過調整政策來解決。另外的原因是收集的空間空閑率跟不上高的晉升率或則某些應用高的對象更新率。如果你的應用的晉升率和更新率太高,你可能需要改變你的應用程式來減少晉升的壓力。使用更多的記憶體有時候可能會使得情況更糟,因為CMS需要掃描更多的記憶體。

Garbage First (G1) 收集器

G1 (-XX:+UseG1GC)收集器是一個在java 6中使用新的收集器,現在從java 7U4開始正式支援。它是一個部分并發的收集算法,它會嘗試通過小步增量暫停世界的方式壓縮老年區,來努力最小化FullGC,而因為碎片引起的FullGC正是CMS的一個噩夢。G1也是分代收集器,但是它與其他收集器器使用不同的堆組織方式,它根據不同的用途,它将堆分為大量((~2000))固定大小的區(regions),相同用途的堆也是不連續的(譯者:Java堆的記憶體布局與就與其他收集器有很大差别,它将整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是實體隔離的了,它們都是一部分Region(不需要連續)的集合)。

 G1采用并發的标記區域的方式來跟蹤區域之間的引用,并且隻關注收集能收集到最大空閑區的區域(譯者:G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在背景維護一個優先清單,每次根據允許的收集時間,優先回價值最大的Region(這也就是Garbage-First名稱的來由))。這些區域的收集是暫停程式的方式,增量的将存活的對象複制到一個空的區域裡面,進而收集的過程是壓縮的。在同一個周期裡收集的區域叫做收集組(Collection Set)

如果一個對象大小超過了區域大小的50%,那麼它會被配置設定到一個大區域裡面,可能是目前區域大小的幾倍。在G1下,收集和配置設定大對象是非常昂貴的操作,目前還沒有任何優化措施。

任何壓縮收集器所面臨的挑戰不是移動對象,而是對這些對象的引用更新。如果一個對象被許多區域引用,那麼更新這些引用會比移動對象更加耗時。G1通過“記憶集(Remembered Sets)” 跟蹤區域中的那些有來自其他區域引用的對象。記憶集(Remembered Sets)是一些卡片的集合,這些卡片上标記着更新資訊。如果記憶集(Remembered Sets)變大,那麼G1久明顯變慢了。當從一個區域轉移對象到另外區域的時候,那麼對應暫停時間的長度與需要掃面和更新引用的區域的數量成正比。

維護記憶集(Remembered Sets)會增加新生代收集的成本,導緻比并行老生代收集器(Parallel Old collector)和CMS收集器對新生代的收集時暫停更長的時間。

G1是目标驅動性,通過–XX:MaxGCPauseMillis=<n>設定延遲時間,預設是200ms,該目标将影響在每個周期做的工作量,也是竭盡所能要保證的唯一依據。設定目标在幾十毫秒大多是徒勞的,并且幾十毫秒的目标也不是G1的關注點。

當一個應用程式可以容忍0.5-1.0秒的暫停來增量壓縮,G1是對于擁有一個大堆,并且會逐漸碎片化的場景來說是很好的通用的收集器。G1 傾向于降低在最環情況下暫停的頻率,而正是CMS的問題,為了處理産生碎片而擴充了新生代收集和對老生代增量壓縮。大部分的暫停被限制在一個區域而不是整個堆的壓縮。

與CMS一樣,G1也會因為無法保證晉升率而失敗,最終回到暫停程式的FullGC上。就像CMS“并發模式失敗”一樣,G1也可能遭受轉移失敗,在日志中能看到“目标空間溢出(to-space overflow)”。這種情況發生在對象轉移的區域沒有足夠的空閑空間的時候,與晉升失敗類似。如果發生這種情況,請嘗試使用更大的堆,更多标記線程,但在某些情況下,需要應用程式作出改變,以減少配置設定比率。

 對G1來說一個具有挑戰性的問題是處理高關注率的對象和區域。 當區域裡存活的對象沒有被其他區域大量引用。增量停止世界的壓縮方法效果很好。如果一個對象或者區域是被大量引用的,記憶集(Remembered Sets)将會相應變大。并且G1将會避免收集這些對象。最終,不得不導緻頻繁的中等長度的暫停時間來壓縮堆。

其他并發收集器(Alternative Concurrent Collectors)

CMS 和 G1通常認為是最并發的收集器,但是當你觀察整個工作過程,很顯然新生代,晉升、甚至許多老生代的工作都不是并發的。對于老生代來說CMS是最并發的算法,G1更像是暫停程式的增量收集器。CMS和G1都會有明顯的和有規律的暫停應用的事件發生,并且最壞情況下往往使他們不适合嚴格的低延遲應用,如金融交易或互動型的使用者界面。

其他的收集器如:Oracle的JRockit Real Time,IBM WebSphere的Real Time的,和Azul 的Zing。 JRockit和Websphere的收集器在延遲上比CMS和G1更加有優勢,但是在大多數情況下它們有吞吐量的限制,并且仍然遭受明顯的暫停應用的事件。Zing是本作者知道的唯一一款Java收集器,能對所有代都真正并發收集和壓縮,同時保持了高吞吐率。Zing确實有一些亞毫秒級的暫停程式的事件,但這些是在收集周期的相移,并且與存活對象集的大小無關。

JRockit的RT在控制堆的大小,有高的對象配置設定率的時候可以實作暫停時間在幾十毫秒,但是偶爾會失敗而回到完全壓縮暫停。WEBSPHERE RT通過限制的配置設定比率和存活集的大小,可以實作毫秒級别的暫停時間。Zing在高配置設定率時通過在所有階段并發,能達到亞毫秒級的暫停。無論堆大小,Zing是能夠保持一緻的行為,并且允許使用者按照需要使用更大的堆,來保證應用程式的吞吐量,或則對象模型狀态的需求,而不用擔心增加暫停時間。

對于所有的并發收集器來說關注延遲目标,你就必須放棄一些吞吐量和空間。根據并發收集器的效率,你可能放棄一點點的吞吐量,但是通常你總是需要顯著增加空間。如果真正的并發,暫停程式的事件将很少發生,那麼需要更多的CPU核心來支援并發操作和維持吞吐量。

注:所有的并發收集器當有足夠的空間時候,往往能更有效地配置設定對象。根據經驗,為了能高效的操作,你應該預算至少兩到三倍于存活集的大小。然而,維持并發操作所需的空間随着應用程式的吞吐量,以及相關的對象配置設定和晉升率的增長而增長。是以,對于高吞吐量的應用,維持較高的堆大小堆存活對象的比例非常有必要。鑒于目前系統擁有的巨大的記憶體空間,這對于伺服器并不是什麼問題。

垃圾收集監控和調整(Garbage Collection Monitoring & Tuning)

為了了解你的應用程式和垃圾收集是如何工作的,啟動JVM的時間至少需要添加如下參數:

-verbose:gc

-Xloggc:

-XX:+PrintGCDetails

-XX:+PrintGCDateStamps

-XX:+PrintTenuringDistribution

-XX:+PrintGCApplicationConcurrentTime

-XX:+PrintGCApplicationStoppedTime

然後加載日志到像Chewiebug的工具進行分析。

為了看到動态的GC過程,可以使用JVisualVM并且安裝Visual GC插件。這将使你能看到你的應用程式的GC行為。

為了能獲得一個适合你應用的GC需要,你需要一個有代表性的可以重複執行的負載測試。當你掌握每個收集器是如何工作的,根據不同的配置運作負載測試,直到達到你理想的吞吐量和延遲目标。從最終使用者的角度來看,重要的是要測量延遲。可以通過捕獲每個測試請求的響應時間,并且使用直方圖來記錄結果, 如HDR直方圖(HdrHistogram)或幹擾物直方圖(Disruptor Histogram)。如果有延遲尖峰超出可接受範圍,然後嘗試關聯GC日志來判斷是否是GC問題。它是可能是其他問題導緻的延遲高峰。另一種有用的工具是

jHiccup,它可以用來跟蹤在JVM中暫停,并且可以整合多個系系統到一個整體。使用jHiccup測量你的空閑系統幾個小時,通常情況你會得到一個令人驚訝的結果。

如果延遲尖峰是由于GC導緻,那麼可以關注在調整CMS或G1看是否可滿足的延遲目标。有時這是不可能的,因為高配置設定和晉升率與低延遲時間的要求是沖突的。 GC優化是一個需要高度技巧的工作,往往需要修改應用程式,以減少對象配置設定或對象生存期。如果需要在時間、GC優化和應用程式的修改,精通方面權衡,那麼購買商業并發壓縮的JVMs,比如JRockit Real Time 和 Azul Zing可能也是必需的。