1. 程式人生 > >JVM實用引數(六) 吞吐量收集器

JVM實用引數(六) 吞吐量收集器

原文連結 本文連線 譯者:張軍  校對:樑海艦

在實踐中我們發現對於大多數的應用領域,評估一個垃圾收集(GC)演算法如何根據如下兩個標準:

  1. 吞吐量越高演算法越好
  2. 暫停時間越短演算法越好

首先讓我們來明確垃圾收集(GC)中的兩個術語:吞吐量(throughput)和暫停時間(pause times)。 JVM在專門的執行緒(GC threads)中執行GC。 只要GC執行緒是活動的,它們將與應用程式執行緒(application threads)爭用當前可用CPU的時鐘週期。 簡單點來說,吞吐量是指應用程式執行緒用時佔程式總用時的比例。 例如,吞吐量99/100意味著100秒的程式執行時間應用程式執行緒運行了99秒, 而在這一時間段內GC執行緒只運行了1秒。

術語”暫停時間”是指一個時間段內應用程式執行緒讓與GC執行緒執行而完全暫停。 例如,GC期間100毫秒的暫停時間意味著在這100毫秒期間內沒有應用程式執行緒是活動的。 如果說一個正在執行的應用程式有100毫秒的“平均暫停時間”,那麼就是說該應用程式所有的暫停時間平均長度為100毫秒。 同樣,100毫秒的“最大暫停時間”是指該應用程式所有的暫停時間最大不超過100毫秒。

吞吐量 VS 暫停時間

高吞吐量最好因為這會讓應用程式的終端使用者感覺只有應用程式執行緒在做“生產性”工作。 直覺上,吞吐量越高程式執行越快。 低暫停時間最好因為從終端使用者的角度來看不管是GC還是其他原因導致一個應用被掛起始終是不好的。 這取決於應用程式的型別,有時候甚至短暫的200毫秒暫停都可能打斷終端使用者體驗。 因此,具有低的最大暫停時間是非常重要的,特別是對於一個互動式應用程式。

不幸的是”高吞吐量”和”低暫停時間”是一對相互競爭的目標(矛盾)。這樣想想看,為了清晰起見簡化一下:GC需要一定的前提條件以便安全地執行。 例如,必須保證應用程式執行緒在GC執行緒試圖確定哪些物件仍然被引用和哪些沒有被引用的時候不修改物件的狀態。 為此,應用程式在GC期間必須停止(或者僅在GC的特定階段,這取決於所使用的演算法)。 然而這會增加額外的執行緒排程開銷:直接開銷是上下文切換,間接開銷是因為快取的影響。 加上JVM內部安全措施的開銷,這意味著GC及隨之而來的不可忽略的開銷,將增加GC執行緒執行實際工作的時間。 因此我們可以通過儘可能少執行GC來最大化吞吐量,例如,只有在不可避免的時候進行GC,來節省所有與它相關的開銷。

然而,僅僅偶爾執行GC意味著每當GC執行時將有許多工作要做,因為在此期間積累在堆中的物件數量很高。 單個GC需要花更多時間來完成, 從而導致更高的平均和最大暫停時間。 因此,考慮到低暫停時間,最好頻繁地執行GC以便更快速地完成。 這反過來又增加了開銷並導致吞吐量下降,我們又回到了起點。
綜上所述,在設計(或使用)GC演算法時​​,我們必須確定我們的目標:一個GC演算法​​只可能針對兩個目標之一(即只專注於最大吞吐量或最小暫停時間),或嘗試找到一個二者的折衷。

HotSpot虛擬機器上的垃圾收集

該系列的第五部分我們已經討論過年輕代的垃圾收集器。 對於年老代,HotSpot虛擬機器提供兩類垃圾收集演算法(除了新的G1垃圾收集演算法),第一類演算法試圖最大限度地提高吞吐量,而第二類演算法試圖最小化暫停時間。 今天我們的重點是第一類,”面向吞吐量”的垃圾收集演算法。
我們希望把重點放在JVM配置引數上,所以我只會簡要概述HotSpot提供的面向吞吐量(throughput-oriented)垃圾收集演算法。 當年老代中由於缺乏空間導致物件分配失敗時會觸發垃圾收集器(事實上,”分配”的通常是指從年輕代提升到年老代的物件)。 從所謂的”GC根”(GC roots)開始,搜尋堆中的可達物件並將其標記為活著的,之後,垃圾收集器將活著的物件移到年老代的一塊無碎片(non-fragmented)記憶體塊中,並標記剩餘的記憶體空間是空閒的。 也就是說,我們不像複製策略那樣移到一個不同的堆區域,像年輕代垃圾收集演算法所做的那樣。 相反地,我們把所有的物件放在一個堆區域中,從而對該堆區域進行碎片整理。 垃圾收集器使用一個或多個執行緒來執行垃圾收集。 當使用多個執行緒時,演算法的不同步驟被分解,使得每個收集執行緒大多時候工作在自己的區域而不干擾其他執行緒。 在垃圾收集期間,所有的應用程式執行緒暫停,只有垃圾收集完成之後才會重新開始。 現在讓我們來看看跟面向吞吐量垃圾收集演算法有關的重要JVM配置引數。

-XX:+UseSerialGC

我們使用該標誌來啟用序列垃圾收集器,例如單執行緒面向吞吐量垃圾收集器。 無論年輕代還是年老代都將只有一個執行緒執行垃圾收集。 該標誌被推薦用於只有單個可用處理器核心的JVM。 在這種情況下,使用多個垃圾收集執行緒甚至會適得其反,因為這些執行緒將爭用CPU資源,造成同步開銷,卻從未真正並行執行。

-XX:+UseParallelGC

有了這個標誌,我們告訴JVM使用多執行緒並行執行年輕代垃圾收集。 在我看來,Java 6中不應該使用該標誌因為-XX:+UseParallelOldGC顯然更合適。 需要注意的是Java 7中該情況改變了一點(詳見本概述),就是-XX:+UseParallelGC能達到-XX:+UseParallelOldGC一樣的效果。

-XX:+UseParallelOldGC

該標誌的命名有點不巧,因為”老”聽起來像”過時”。 然而,”老”實際上是指年老代,這也解釋了為什麼-XX:+UseParallelOldGC要優於-XX:+UseParallelGC:除了啟用年輕代並行垃圾收集,也激活了年老代並行垃圾收集。 當期望高吞吐量,並且JVM有兩個或更多可用處理器核心時,我建議使用該標誌。
作為旁註,HotSpot的並行面向吞吐量垃圾收集演算法通常稱為”吞吐量收集器”,因為它們旨在通過並行執行來提高吞吐量。

-XX:ParallelGCThreads

通過-XX:ParallelGCThreads=<value>我們可以指定並行垃圾收集的執行緒數量。 例如,-XX:ParallelGCThreads=6表示每次並行垃圾收集將有6個執行緒執行。 如果不明確設定該標誌,虛擬機器將使用基於可用(虛擬)處理器數量計算的預設值。 決定因素是由Java Runtime。availableProcessors()方法的返回值N,如果N<=8,並行垃圾收集器將使用N個垃圾收集執行緒,如果N>8個可用處理器,垃圾收集執行緒數量應為3+5N/8。
當JVM獨佔地使用系統和處理器時使用預設設定更有意義。 但是,如果有多個JVM(或其他耗CPU的系統)在同一臺機器上執行,我們應該使用-XX:ParallelGCThreads來減少垃圾收集執行緒數到一個適當的值。 例如,如果4個以伺服器方式執行的JVM同時跑在在一個具有16核處理器的機器上,設定-XX:ParallelGCThreads=4是明智的,它能使不同JVM的垃圾收集器不會相互干擾。

-XX:-UseAdaptiveSizePolicy

吞吐量垃圾收集器提供了一個有趣的(但常見,至少在現代JVM上)機制以提高垃圾收集配置的使用者友好性。 這種機制被看做是HotSpot在Java 5中引入的”人體工程學”概念的一部分。 通過人體工程學,垃圾收集器能將堆大小動態變動像GC設定一樣應用到不同的堆區域,只要有證據表明這些變動將能提高GC效能。 “提高GC效能”的確切含義可以由使用者通過-XX:GCTimeRatio和-XX:MaxGCPauseMillis(見下文)標記來指定。
重要的是要知道人體工程學是預設啟用的。 這很好,因為自適應行為是JVM最大優勢之一。 不過,有時我們需要非常清楚對於特定應用什麼樣的設定是最合適的,在這些情況下,我們可能不希望JVM混亂我們的設定。 每當我們發現處於這種情況時,我們可以考慮通過-XX:-UseAdaptiveSizePolicy停用一些人體工程學。

-XX:GCTimeRatio

通過-XX:GCTimeRatio=<value>我們告訴JVM吞吐量要達到的目標值。 更準確地說,-XX:GCTimeRatio=N指定目標應用程式執行緒的執行時間(與總的程式執行時間)達到N/(N+1)的目標比值。 例如,通過-XX:GCTimeRatio=9我們要求應用程式執行緒在整個執行時間中至少9/10是活動的(因此,GC執行緒佔用其餘1/10)。 基於執行時的測量,JVM將會嘗試修改堆和GC設定以期達到目標吞吐量。 -XX:GCTimeRatio的預設值是99,也就是說,應用程式執行緒應該執行至少99%的總執行時間。

-XX:MaxGCPauseMillis

通過-XX:GCTimeRatio=<value>告訴JVM最大暫停時間的目標值(以毫秒為單位)。 在執行時,吞吐量收集器計算在暫停期間觀察到的統計資料(加權平均和標準偏差)。 如果統計表明正在經歷的暫停其時間存在超過目標值的風險時,JVM會修改堆和GC設定以降低它們。 需要注意的是,年輕代和年老代垃圾收集的統計資料是分開計算的,還要注意,預設情況下,最大暫停時間沒有被設定。
如果最大暫停時間和最小吞吐量同時設定了目標值,實現最大暫停時間目標具有更高的優先順序。 當然,無法保證JVM將一定能達到任一目標,即使它會努力去做。 最後,一切都取決於手頭應用程式的行為。
當設定最大暫停時間目標時,我們應注意不要選擇太小的值。 正如我們現在所知道的,為了保持低暫停時間,JVM需要增加GC次數,那樣可能會嚴重影響可達到的吞吐量。 這就是為什麼對於要求低暫停時間作為主要目標的應用程式(大多數是Web應用程式),我會建議不要使用吞吐量收集器,而是選擇CMS收集器。 CMS收集器是本系列下一部分的主題。


張軍

張軍,併發程式設計網譯者,網際網路研發工程師,對併發程式設計,分散式計算,NoSQL,Erlang,後端架構,IM即時通訊感興趣,可通過[email protected]跟我聯絡^_^