在實踐中我們發(fā)現(xiàn)對于大多數(shù)的應(yīng)用領(lǐng)域,評估一個垃圾收集 (GC) 算法如何根據(jù)如下兩個標(biāo)準(zhǔn):
首先讓我們來明確垃圾收集 (GC) 中的兩個術(shù)語: 吞吐量 (throughput) 和暫停時間 (pause times)。 JVM 在專門的線程 (GC threads) 中執(zhí)行 GC。 只要 GC 線程是活動的,它們將與應(yīng)用程序線程 (application threads) 爭用當(dāng)前可用 CPU 的時鐘周期。 簡單點來說,吞吐量是指應(yīng)用程序線程用時占程序總用時的比例。 例如,吞吐量 99/100 意味著 100 秒的程序執(zhí)行時間應(yīng)用程序線程運行了 99 秒, 而在這一時間段內(nèi) GC 線程只運行了 1 秒。
術(shù)語” 暫停時間” 是指一個時間段內(nèi)應(yīng)用程序線程讓與 GC 線程執(zhí)行而完全暫停。 例如,GC 期間 100 毫秒的暫停時間意味著在這 100 毫秒期間內(nèi)沒有應(yīng)用程序線程是活動的。 如果說一個正在運行的應(yīng)用程序有 100 毫秒的 “平均暫停時間”,那么就是說該應(yīng)用程序所有的暫停時間平均長度為 100 毫秒。 同樣,100 毫秒的 “最大暫停時間” 是指該應(yīng)用程序所有的暫停時間最大不超過 100 毫秒。
高吞吐量最好因為這會讓應(yīng)用程序的最終用戶感覺只有應(yīng)用程序線程在做 “生產(chǎn)性” 工作。 直覺上,吞吐量越高程序運行越快。 低暫停時間最好因為從最終用戶的角度來看不管是 GC 還是其他原因?qū)е乱粋€應(yīng)用被掛起始終是不好的。 這取決于應(yīng)用程序的類型,有時候甚至短暫的 200 毫秒暫停都可能打斷終端用戶體驗。 因此,具有低的最大暫停時間是非常重要的,特別是對于一個交互式應(yīng)用程序。
不幸的是” 高吞吐量” 和” 低暫停時間” 是一對相互競爭的目標(biāo)(矛盾)。這樣想想看,為了清晰起見簡化一下:GC 需要一定的前提條件以便安全地運行。 例如,必須保證應(yīng)用程序線程在 GC 線程試圖確定哪些對象仍然被引用和哪些沒有被引用的時候不修改對象的狀態(tài)。 為此,應(yīng)用程序在 GC 期間必須停止 (或者僅在 GC 的特定階段,這取決于所使用的算法)。 然而這會增加額外的線程調(diào)度開銷:直接開銷是上下文切換,間接開銷是因為緩存的影響。 加上 JVM 內(nèi)部安全措施的開銷,這意味著 GC 及隨之而來的不可忽略的開銷,將增加 GC 線程執(zhí)行實際工作的時間。 因此我們可以通過盡可能少運行 GC 來最大化吞吐量,例如,只有在不可避免的時候進行 GC,來節(jié)省所有與它相關(guān)的開銷。
然而,僅僅偶爾運行 GC 意味著每當(dāng) GC 運行時將有許多工作要做,因為在此期間積累在堆中的對象數(shù)量很高。 單個 GC 需要花更多時間來完成, 從而導(dǎo)致更高的平均和最大暫停時間。 因此,考慮到低暫停時間,最好頻繁地運行 GC 以便更快速地完成。 這反過來又增加了開銷并導(dǎo)致吞吐量下降,我們又回到了起點。
綜上所述,在設(shè)計(或使用)GC 算法時??,我們必須確定我們的目標(biāo):一個 GC 算法??只可能針對兩個目標(biāo)之一(即只專注于最大吞吐量或最小暫停時間),或嘗試找到一個二者的折衷。
該系列的第五部分我們已經(jīng)討論過年輕代的垃圾收集器。 對于年老代,HotSpot 虛擬機提供兩類垃圾收集算法 (除了新的 G1 垃圾收集算法),第一類算法試圖最大限度地提高吞吐量,而第二類算法試圖最小化暫停時間。 今天我們的重點是第一類,” 面向吞吐量” 的垃圾收集算法。
我們希望把重點放在 JVM 配置參數(shù)上,所以我只會簡要概述 HotSpot 提供的面向吞吐量 (throughput-oriented) 垃圾收集算法。 當(dāng)年老代中由于缺乏空間導(dǎo)致對象分配失敗時會觸發(fā)垃圾收集器 (事實上,” 分配” 的通常是指從年輕代提升到年老代的對象)。 從所謂的”GC 根”(GC roots) 開始,搜索堆中的可達對象并將其標(biāo)記為活著的,之后,垃圾收集器將活著的對象移到年老代的一塊無碎片 (non-fragmented) 內(nèi)存塊中,并標(biāo)記剩余的內(nèi)存空間是空閑的。 也就是說,我們不像復(fù)制策略那樣移到一個不同的堆區(qū)域,像年輕代垃圾收集算法所做的那樣。 相反地,我們把所有的對象放在一個堆區(qū)域中,從而對該堆區(qū)域進行碎片整理。 垃圾收集器使用一個或多個線程來執(zhí)行垃圾收集。 當(dāng)使用多個線程時,算法的不同步驟被分解,使得每個收集線程大多時候工作在自己的區(qū)域而不干擾其他線程。 在垃圾收集期間,所有的應(yīng)用程序線程暫停,只有垃圾收集完成之后才會重新開始。 現(xiàn)在讓我們來看看跟面向吞吐量垃圾收集算法有關(guān)的重要 JVM 配置參數(shù)。
我們使用該標(biāo)志來激活串行垃圾收集器,例如單線程面向吞吐量垃圾收集器。 無論年輕代還是年老代都將只有一個線程執(zhí)行垃圾收集。 該標(biāo)志被推薦用于只有單個可用處理器核心的 JVM。 在這種情況下,使用多個垃圾收集線程甚至?xí)m得其反,因為這些線程將爭用 CPU 資源,造成同步開銷,卻從未真正并行運行。
有了這個標(biāo)志,我們告訴 JVM 使用多線程并行執(zhí)行年輕代垃圾收集。 在我看來,Java 6 中不應(yīng)該使用該標(biāo)志因為 - XX:+UseParallelOldGC 顯然更合適。 需要注意的是 Java 7 中該情況改變了一點 (詳見本概述),就是 - XX:+UseParallelGC 能達到 - XX:+UseParallelOldGC 一樣的效果。
該標(biāo)志的命名有點不巧,因為” 老” 聽起來像” 過時”。 然而,” 老” 實際上是指年老代,這也解釋了為什么 - XX:+UseParallelOldGC 要優(yōu)于 - XX:+UseParallelGC:除了激活年輕代并行垃圾收集,也激活了年老代并行垃圾收集。 當(dāng)期望高吞吐量,并且 JVM 有兩個或更多可用處理器核心時,我建議使用該標(biāo)志。 作為旁注,HotSpot 的并行面向吞吐量垃圾收集算法通常稱為” 吞吐量收集器”,因為它們旨在通過并行執(zhí)行來提高吞吐量。
通過 - XX:ParallelGCThreads=
吞吐量垃圾收集器提供了一個有趣的 (但常見,至少在現(xiàn)代 JVM 上) 機制以提高垃圾收集配置的用戶友好性。 這種機制被看做是 HotSpot 在 Java 5 中引入的” 人體工程學(xué)” 概念的一部分。 通過人體工程學(xué),垃圾收集器能將堆大小動態(tài)變動像 GC 設(shè)置一樣應(yīng)用到不同的堆區(qū)域,只要有證據(jù)表明這些變動將能提高 GC 性能。 “提高 GC 性能” 的確切含義可以由用戶通過 - XX:GCTimeRatio 和 - XX:MaxGCPauseMillis(見下文) 標(biāo)記來指定。 重要的是要知道人體工程學(xué)是默認激活的。 這很好,因為自適應(yīng)行為是 JVM 最大優(yōu)勢之一。 不過,有時我們需要非常清楚對于特定應(yīng)用什么樣的設(shè)置是最合適的,在這些情況下,我們可能不希望 JVM 混亂我們的設(shè)置。 每當(dāng)我們發(fā)現(xiàn)處于這種情況時,我們可以考慮通過 - XX:-UseAdaptiveSizePolicy 停用一些人體工程學(xué)。
通過 - XX:GCTimeRatio=
通過 - XX:GCTimeRatio=<value> 告訴 JVM 最大暫停時間的目標(biāo)值 (以毫秒為單位)。 在運行時,吞吐量收集器計算在暫停期間觀察到的統(tǒng)計數(shù)據(jù) (加權(quán)平均和標(biāo)準(zhǔn)偏差)。 如果統(tǒng)計表明正在經(jīng)歷的暫停其時間存在超過目標(biāo)值的風(fēng)險時,JVM 會修改堆和 GC 設(shè)置以降低它們。 需要注意的是,年輕代和年老代垃圾收集的統(tǒng)計數(shù)據(jù)是分開計算的,還要注意,默認情況下,最大暫停時間沒有被設(shè)置。 如果最大暫停時間和最小吞吐量同時設(shè)置了目標(biāo)值,實現(xiàn)最大暫停時間目標(biāo)具有更高的優(yōu)先級。 當(dāng)然,無法保證 JVM 將一定能達到任一目標(biāo),即使它會努力去做。 最后,一切都取決于手頭應(yīng)用程序的行為。
當(dāng)設(shè)置最大暫停時間目標(biāo)時,我們應(yīng)注意不要選擇太小的值。 正如我們現(xiàn)在所知道的,為了保持低暫停時間,JVM 需要增加 GC 次數(shù),那樣可能會嚴(yán)重影響可達到的吞吐量。 這就是為什么對于要求低暫停時間作為主要目標(biāo)的應(yīng)用程序 (大多數(shù)是 Web 應(yīng)用程序),我會建議不要使用吞吐量收集器,而是選擇 CMS 收集器。 CMS 收集器是本系列下一部分的主題。