在實踐中咱們發現對於大多數的應用領域,評估一個垃圾收集(GC)算法如何根據以下兩個標準:算法
首先讓咱們來明確垃圾收集(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毫秒。安全
高吞吐量最好由於這會讓應用程序的最終用戶感受只有應用程序線程在作「生產性」工做。 直覺上,吞吐量越高程序運行越快。 低暫停時間最好由於從最終用戶的角度來看無論是GC仍是其餘緣由致使一個應用被掛起始終是很差的。 這取決於應用程序的類型,有時候甚至短暫的200毫秒暫停均可能打斷終端用戶體驗。 所以,具備低的最大暫停時間是很是重要的,特別是對於一個交互式應用程序。服務器
不幸的是」高吞吐量」和」低暫停時間」是一對相互競爭的目標(矛盾)。這樣想一想看,爲了清晰起見簡化一下:GC須要必定的前提條件以便安全地運行。 例如,必須保證應用程序線程在GC線程試圖肯定哪些對象仍然被引用和哪些沒有被引用的時候不修改對象的狀態。 爲此,應用程序在GC期間必須中止(或者僅在GC的特定階段,這取決於所使用的算法)。 然而這會增長額外的線程調度開銷:直接開銷是上下文切換,間接開銷是由於緩存的影響。 加上JVM內部安全措施的開銷,這意味着GC及隨之而來的不可忽略的開銷,將增長GC線程執行實際工做的時間。 所以咱們能夠經過儘量少運行GC來最大化吞吐量,例如,只有在不可避免的時候進行GC,來節省全部與它相關的開銷。多線程
然而,僅僅偶爾運行GC意味着每當GC運行時將有許多工做要作,由於在此期間積累在堆中的對象數量很高。 單個GC須要花更多時間來完成, 從而致使更高的平均和最大暫停時間。 所以,考慮到低暫停時間,最好頻繁地運行GC以便更快速地完成。 這反過來又增長了開銷並致使吞吐量降低,咱們又回到了起點。
綜上所述,在設計(或使用)GC算法時,咱們必須肯定咱們的目標:一個GC算法只可能針對兩個目標之一(即只專一於最大吞吐量或最小暫停時間),或嘗試找到一個兩者的折衷。app
Throughput收集器有兩個基本操做:一是回收新生代垃圾(Minor GC);二是回收老年代垃圾(Full GC)。性能
下圖是新生代回收以前和以後的狀況spa
Minor GC一般在新生代空間用盡時。新生代收集器會把Eden空間中的全部對象挪走:一部分被移到Survivor中,其它被移到老年代。線程
下圖是Full gc以前以後的對比圖設計
老年代垃圾收集會收集新生代中全部的對象(包括Survivor中的對象),只有那些有活躍引用的對象,或者已經通過壓縮整理的對象會繼續在老年代中留存下來。其他對象將被回收。
Throughput收集器的調優幾乎都是圍繞停頓時間進行的,尋求堆的整體大小、新生代的大小以及老年代大小之間的平衡。
兩種取捨:
一、經典技術上的取捨,時間和空間上的取捨;
二、第二個取捨與完成垃圾回收所需的時長有關,增大堆可以減小Full GC停頓發送的頻率,但也有侷限性:因爲GC時間變得更長,平均響應時間也會變長(老年代空間很大,在回收老年代耗時很長)。相似的,爲新生代分配更多的堆空間能夠縮短Full GC的停頓時間,不過這又增大老年代垃圾回收的頻率(由於老年代空間保持不變或者變得更小了)。
看一個示例說明堆大小與吞吐量的關係,在該示例中是關閉了JVM自適應調整堆大小的狀況
在Throughput收集器中,可自適應調整會從新分配堆(以及代)的大小。使用這些標誌能夠設置相應的性能指標:-XX:MaxGCPauseMillis=N和-XX:GCTimeRatio=N。
-XX:MaxGCPauseMillis:標誌用於設定應用可承受的最大停頓時間。能夠設置爲0或者一些很是小的值,譬如50毫秒。請注意,這個標誌設定的值同時影響Minor GC和Full GC。若是設置的值很是小,那麼應用的老年代最終就會很是小,致使頻繁的Full GC(例如,但願程序在50毫秒內完成垃圾回收,這將會觸發很是頻繁的Full GC,對應用程序的性能而言將是災難性的。)。所以,設定該值時,儘可能保持理性,將該值設定爲可達到的合理值。缺省狀況下, 不設置該參數。
-XX:GCTimeRatio:標誌能夠設置你但願應用程序在垃圾回收上花費多少時間(與應用線程的運行時間相比較)。它是一個百分比,默認值是99。
N值得計算稍微有些複雜。將N值代入下面的公式能夠計算理想狀況下應用線程的運行時間所佔的百分比:
將GCTimeRatio的默認值99代入公式獲得0.99,這意味着應用程序的運行時間佔總時間的99%,只有1%的時間消耗在垃圾回收上。
再例如GCTimeRatio=95時,並非意味使用總時間5%去作垃圾回收;它表示的是最多會使用總時間的1.94%去作垃圾回收。因此,若是你指望程序工做時間在95%,則須要變換一下公式計算GCTimeRatio是一個更容易操做的方法。
對於95%(0.95)的吞吐量目標,利用該公式計算出的GCTimeRatio是19。
JVM使用這兩個標誌在堆的初始值(-Xms,-XmX)之間的堆的大小。MaxGCPauseMillis的優先級別最高:若是設置了這個值,新生代和老年代會隨之進行調整,直到知足對應停頓時間的目標。一旦這個目標達成,堆的總容量就開始逐漸增大,直到運行時間的比率達到設定值。這兩個目標都達成後,開始縮減堆大小,儘量的以最小堆大小來知足這兩個目標。
該系列的第五部分咱們已經討論過年輕代的垃圾收集器。 對於年老代,HotSpot虛擬機提供兩類垃圾收集算法(除了新的G1垃圾收集算法),第一類算法試圖最大限度地提升吞吐量,而第二類算法試圖最小化暫停時間。 今天咱們的重點是第一類,」面向吞吐量」的垃圾收集算法。
咱們但願把重點放在JVM配置參數上,因此我只會簡要概述HotSpot提供的面向吞吐量(throughput-oriented)垃圾收集算法。 當年老代中因爲缺少空間致使對象分配失敗時會觸發垃圾收集器(事實上,」分配」的一般是指從年輕代提高到年老代的對象)。 從所謂的」GC根」(GC roots)開始,搜索堆中的可達對象並將其標記爲活着的,以後,垃圾收集器將活着的對象移到年老代的一塊無碎片(non-fragmented)內存塊中,並標記剩餘的內存空間是空閒的。 也就是說,咱們不像複製策略那樣移到一個不一樣的堆區域,像年輕代垃圾收集算法所作的那樣。 相反地,咱們把全部的對象放在一個堆區域中,從而對該堆區域進行碎片整理。 垃圾收集器使用一個或多個線程來執行垃圾收集。 當使用多個線程時,算法的不一樣步驟被分解,使得每一個收集線程大多時候工做在本身的區域而不干擾其餘線程。 在垃圾收集期間,全部的應用程序線程暫停,只有垃圾收集完成以後纔會從新開始。 如今讓咱們來看看跟面向吞吐量垃圾收集算法有關的重要JVM配置參數。
咱們使用該標誌來激活串行垃圾收集器,例如單線程面向吞吐量垃圾收集器。 不管年輕代仍是年老代都將只有一個線程執行垃圾收集。 該標誌被推薦用於只有單個可用處理器核心的JVM。 在這種狀況下,使用多個垃圾收集線程甚至會拔苗助長,由於這些線程將爭用CPU資源,形成同步開銷,卻從未真正並行運行。
有了這個標誌,咱們告訴JVM使用多線程並行執行年輕代垃圾收集。 在我看來,Java 6中不該該使用該標誌由於-XX:+UseParallelOldGC顯然更合適。 須要注意的是Java 7中該狀況改變了一點(詳見本概述),就是-XX:+UseParallelGC能達到-XX:+UseParallelOldGC同樣的效果。
該標誌的命名有點不巧,由於」老」聽起來像」過期」。 然而,」老」其實是指年老代,這也解釋了爲何-XX:+UseParallelOldGC要優於-XX:+UseParallelGC:除了激活年輕代並行垃圾收集,也激活了年老代並行垃圾收集。 當指望高吞吐量,而且JVM有兩個或更多可用處理器核心時,我建議使用該標誌。
做爲旁註,HotSpot的並行面向吞吐量垃圾收集算法一般稱爲」吞吐量收集器」,由於它們旨在經過並行執行來提升吞吐量。
經過-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的垃圾收集器不會相互干擾。
吞吐量垃圾收集器提供了一個有趣的(但常見,至少在現代JVM上)機制以提升垃圾收集配置的用戶友好性。 這種機制被看作是HotSpot在Java 5中引入的」人體工程學」概念的一部分。 經過人體工程學,垃圾收集器能將堆大小動態變更像GC設置同樣應用到不一樣的堆區域,只要有證據代表這些變更將能提升GC性能。 「提升GC性能」的確切含義能夠由用戶經過-XX:GCTimeRatio和-XX:MaxGCPauseMillis(見下文)標記來指定。
重要的是要知道人體工程學是默認激活的。 這很好,由於自適應行爲是JVM最大優點之一。 不過,有時咱們須要很是清楚對於特定應用什麼樣的設置是最合適的,在這些狀況下,咱們可能不但願JVM混亂咱們的設置。 每當咱們發現處於這種狀況時,咱們能夠考慮經過-XX:-UseAdaptiveSizePolicy停用一些人體工程學。
經過-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:GCTimeRatio=<value>告訴JVM最大暫停時間的目標值(以毫秒爲單位)。 在運行時,吞吐量收集器計算在暫停期間觀察到的統計數據(加權平均和標準誤差)。 若是統計代表正在經歷的暫停其時間存在超過目標值的風險時,JVM會修改堆和GC設置以下降它們。 須要注意的是,年輕代和年老代垃圾收集的統計數據是分開計算的,還要注意,默認狀況下,最大暫停時間沒有被設置。若是最大暫停時間和最小吞吐量同時設置了目標值,實現最大暫停時間目標具備更高的優先級。 固然,沒法保證JVM將必定能達到任一目標,即便它會努力去作。 最後,一切都取決於手頭應用程序的行爲。當設置最大暫停時間目標時,咱們應注意不要選擇過小的值。 正如咱們如今所知道的,爲了保持低暫停時間,JVM須要增長GC次數,那樣可能會嚴重影響可達到的吞吐量。 這就是爲何對於要求低暫停時間做爲主要目標的應用程序(大多數是Web應用程序),我會建議不要使用吞吐量收集器,而是選擇CMS收集器。 CMS收集器是本系列下一部分的主題。