jvm垃圾回收GC調優基礎原理分析

說明:

Capacity: 性能,能力,系統容量; 文中翻譯為”系統容量“; 意為硬件配置。

GC調優(Tuning Garbage Collection)和其他性能調優是同樣的原理。初學者可能會被 200 多個 GC參數弄得一頭霧水, 然後隨便調整幾個來試試結果,又或者修改幾行代碼來測試。其實隻要參照下面的步驟,就能保證你的調優方向正確:

  • 列出性能調優指標(State your performance goals)
  • 執行測試(Run tests)
  • 檢查結果(Measure the results)
  • 與目標進行對比(Compare the results with the goals)
  • 如果達不到指標, 修改配置參數, 然後繼續測試(go back to running tests)

第一步, 我們需要做的事情就是: 制定明確的GC性能指標。對所有性能監控和管理來說, 有三個維度是通用的:

  • Latency(延遲)
  • Throughput(吞吐量)
  • Capacity(系統容量)

我們先講解基本概念,然後再演示如何使用這些指標。如果您對 延遲、吞吐量和系統容量等概念很熟悉, 可以跳過這一小節。

核心概念(Core Concepts)

我們先來看一傢工廠的裝配流水線。工人在流水線將現成的組件按順序拼接,組裝成自行車。通過實地觀測, 我們發現從組件進入生產線,到另一端組裝成自行車需要4小時。

繼續觀察,我們還發現,此後每分鐘就有1輛自行車完成組裝, 每天24小時,一直如此。將這個模型簡化, 並忽略維護窗口期後得出結論: 這條流水線每小時可以組裝60輛自行車。

說明: 時間窗口/窗口期,請類比車站賣票的窗口,是一段規定/限定做某件事的時間段。

通過這兩種測量方法, 就知道瞭生產線的相關性能信息: 延遲與吞吐量:

  • 生產線的延遲: 4小時
  • 生產線的吞吐量: 60輛/小時

請註意, 衡量延遲的時間單位根據具體需要而確定 —— 從納秒(nanosecond)到幾千年(millennia)都有可能。系統的吞吐量是每個單位時間內完成的操作。操作(Operations)一般是特定系統相關的東西。在本例中,選擇的時間單位是小時, 操作就是對自行車的組裝。

掌握瞭延遲和吞吐量兩個概念之後, 讓我們對這個工廠來進行實際的調優。自行車的需求在一段時間內都很穩定, 生產線組裝自行車有四個小時延遲, 而吞吐量在幾個月以來都很穩定: 60輛/小時。假設某個銷售團隊突然業績暴漲, 對自行車的需求增加瞭1倍。客戶每天需要的自行車不再是 60 24 = 1440輛, 而是 21440 = 2880輛/天。老板對工廠的產能不滿意,想要做些調整以提升產能。

看起來總經理很容易得出正確的判斷, 系統的延遲沒法子進行處理 —— 他關註的是每天的自行車生產總量。得出這個結論以後, 假若工廠資金充足, 那麼應該立即采取措施, 改善吞吐量以增加產能。

我們很快會看到, 這傢工廠有兩條相同的生產線。每條生產線一分鐘可以組裝一輛成品自行車。 可以想象,每天生產的自行車數量會增加一倍。達到 2880輛/天。要註意的是, 不需要減少自行車的裝配時間 —— 從開始到結束依然需要 4 小時。

巧合的是,這樣進行的性能優化,同時增加瞭吞吐量和產能。一般來說,我們會先測量當前的系統性能, 再設定新目標, 隻優化系統的某個方面來滿足性能指標。

在這裡做瞭一個很重要的決定 —— 要增加吞吐量,而不是減小延遲。在增加吞吐量的同時, 也需要增加系統容量。比起原來的情況, 現在需要兩條流水線來生產出所需的自行車。在這種情況下, 增加系統的吞吐量並不是免費的, 需要水平擴展, 以滿足增加的吞吐量需求。

在處理性能問題時, 應該考慮到還有另一種看似不相關的解決辦法。假如生產線的延遲從1分鐘降低為30秒,那麼吞吐量同樣可以增長 1 倍。

或者是降低延遲, 或者是客戶非常有錢。軟件工程裡有一種相似的說法 —— 每個性能問題背後,總有兩種不同的解決辦法。 可以用更多的機器, 或者是花精力來改善性能低下的代碼。

Latency(延遲)

GC的延遲指標由一般的延遲需求決定。延遲指標通常如下所述:

  • 所有交易必須在10秒內得到響應
  • 90%的訂單付款操作必須在3秒以內處理完成
  • 推薦商品必須在 100 ms 內展示到用戶面前

面對這類性能指標時, 需要確保在交易過程中, GC暫停不能占用太多時間,否則就滿足不瞭指標。“不能占用太多” 的意思需要視具體情況而定, 還要考慮到其他因素, 比如外部數據源的交互時間(round-trips), 鎖競爭(lock contention), 以及其他的安全點等等。

假設性能需求為: 90%的交易要在 1000ms 以內完成, 每次交易最長不能超過 10秒。 根據經驗, 假設GC暫停時間比例不能超過10%。 也就是說, 90%的GC暫停必須在 100ms 內結束, 也不能有超過 1000ms 的GC暫停。為簡單起見, 我們忽略在同一次交易過程中發生多次GC停頓的可能性。

有瞭正式的需求,下一步就是檢查暫停時間。有許多工具可以使用, 在接下來的 6. GC 調優(工具篇) 中會進行詳細的介紹, 在本節中我們通過查看GC日志, 檢查一下GC暫停的時間。相關的信息散落在不同的日志片段中, 看下面的數據:

****-06-04T13:34:16.974-0200: 2.578: [Full GC (Ergonomics)
[PSYoungGen: 93677K->70109K(254976K)]
[ParOldGen: 499597K->511230K(761856K)]
593275K->581339K(1016832K),
[Metaspace: 2936K->2936K(1056768K)]
, 0.0713174 secs]
[Times: user=0.21 sys=0.02, real=0.07 secs

這表示一次GC暫停, 在 ****-06-04T13:34:16 這個時刻觸發. 對應於JVM啟動之後的 2,578 ms

此事件將應用線程暫停瞭 0.0713174 秒。雖然花費的總時間為 210 ms, 但因為是多核CPU機器, 所以最重要的數字是應用線程被暫停的總時間, 這裡使用的是並行GC, 所以暫停時間大約為 70ms 。 這次GC的暫停時間小於 100ms 的閾值,滿足需求。

繼續分析, 從所有GC日志中提取出暫停相關的數據, 匯總之後就可以得知是否滿足需求。

Throughput(吞吐量)

吞吐量和延遲指標有很大區別。當然兩者都是根據一般吞吐量需求而得出的。一般吞吐量需求(Generic requirements for throughput) 類似這樣:

  • 解決方案每天必須處理 100萬個訂單
  • 解決方案必須支持1000個登錄用戶,同時在5-10秒內執行某個操作: A、B或C
  • 每周對所有客戶進行統計, 時間不能超過6小時,時間窗口為每周日晚12點到次日6點之間。

可以看出,吞吐量需求不是針對單個操作的, 而是在給定的時間內, 系統必須完成多少個操作。和延遲需求類似, GC調優也需要確定GC行為所消耗的總時間。每個系統能接受的時間不同, 一般來說, GC占用的總時間比不能超過 10%

現在假設需求為: 每分鐘處理 1000 筆交易。同時, 每分鐘GC暫停的總時間不能超過6秒(即10%)。

有瞭正式的需求, 下一步就是獲取相關的信息。依然是從GC日志中提取數據, 可以看到類似這樣的信息:

****-06-04T13:34:16.974-0200: 2.578: [Full GC (Ergonomics)
[PSYoungGen: 93677K->70109K(254976K)]
[ParOldGen: 499597K->511230K(761856K)]
593275K->581339K(1016832K),
[Metaspace: 2936K->2936K(1056768K)],
0.0713174 secs]
[Times: user=0.21 sys=0.02, real=0.07 secs

此時我們對 用戶耗時(user)和系統耗時(sys)感興趣, 而不關心實際耗時(real)。在這裡, 我們關心的時間為 0.23s(user + sys = 0.21 + 0.02 s), 這段時間內, GC暫停占用瞭 cpu 資源。 重要的是, 系統運行在多核機器上, 轉換為實際的停頓時間(stop-the-world)為 0.0713174秒, 下面的計算會用到這個數字。

提取出有用的信息後, 剩下要做的就是統計每分鐘內GC暫停的總時間。看看是否滿足需求: 每分鐘內總的暫停時間不得超過6000毫秒(6秒)。

Capacity(系統容量)

系統容量(Capacity)需求,是在達成吞吐量和延遲指標的情況下,對硬件環境的額外約束。這類需求大多是來源於計算資源或者預算方面的原因。例如:

  • 系統必須能部署到小於512 MB內存的Android設備上
  • 系統必須部署在Amazon EC2實例上, 配置不得超過 c3.xlarge(4核8GB)。
  • 每月的 Amazon EC2 賬單不得超過 $12,000

因此, 在滿足延遲和吞吐量需求的基礎上必須考慮系統容量。可以說, 假若有無限的計算資源可供揮霍, 那麼任何 延遲和吞吐量指標 都不成問題, 但現實情況是, 預算(budget)和其他約束限制瞭可用的資源。

相關示例

介紹完性能調優的三個維度後, 我們來進行實際的操作以達成GC性能指標。

請看下面的代碼:

//imports skipped for brevity
public class Producer implements Runnable {
  private static ScheduledExecutorService executorService
         = Executors.newScheduledThreadPool(2);
  private Deque<byte[]> deque;
  private int objectSize;
  private int queueSize;
  public Producer(int objectSize, int ttl) {
    this.deque = new ArrayDeque<byte[]>();
    this.objectSize = objectSize;
    this.queueSize = ttl * 1000;
  }
  @Override
  public void run() {
    for (int i = 0; i < 100; i++) { 
        deque.add(new byte[objectSize]); 
        if (deque.size() > queueSize) {
            deque.poll();
        }
    }
  }
  public static void main(String[] args) 
        throws InterruptedException {
    executorService.scheduleAtFixedRate(
        new Producer(200 * 1024 * 1024 / 1000, 5), 
        0, 100, TimeUnit.MILLISECONDS
    );
    executorService.scheduleAtFixedRate(
        new Producer(50 * 1024 * 1024 / 1000, 120), 
        0, 100, TimeUnit.MILLISECONDS);
    TimeUnit.MINUTES.sleep(10);
    executorService.shutdownNow();
  }
}

這段程序代碼, 每 100毫秒 提交兩個作業(job)來。每個作業都模擬特定的生命周期: 創建對象, 然後在預定的時間釋放, 接著就不管瞭, 由GC來自動回收占用的內存。

在運行這個示例程序時,通過以下JVM參數打開GC日志記錄:

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps

還應該加上JVM參數 -Xloggc以指定GC日志的存儲位置,類似這樣:

-Xloggc:C:\\Producer_gc.log

在日志文件中可以看到GC的行為, 類似下面這樣:

****-06-04T13:34:16.119-0200: 1.723: [GC (Allocation Failure) 
        [PSYoungGen: 114016K->73191K(234496K)] 
    421540K->421269K(745984K), 
    0.0858176 secs] 
    [Times: user=0.04 sys=0.06, real=0.09 secs] 
****-06-04T13:34:16.738-0200: 2.342: [GC (Allocation Failure) 
        [PSYoungGen: 234462K->93677K(254976K)] 
    582540K->593275K(766464K), 
    0.2357086 secs] 
    [Times: user=0.11 sys=0.14, real=0.24 secs] 
****-06-04T13:34:16.974-0200: 2.578: [Full GC (Ergonomics) 
        [PSYoungGen: 93677K->70109K(254976K)] 
        [ParOldGen: 499597K->511230K(761856K)] 
    593275K->581339K(1016832K), 
        [Metaspace: 2936K->2936K(1056768K)], 
    0.0713174 secs] 
    [Times: user=0.21 sys=0.02, real=0.07 secs]

基於日志中的信息, 可以通過三個優化目標來提升性能:

  • 確保最壞情況下,GC暫停時間不超過預定閥值
  • 確保線程暫停的總時間不超過預定閥值
  • 在確保達到延遲和吞吐量指標的情況下, 降低硬件配置以及成本。

為此, 用三種不同的配置, 將代碼運行10分鐘, 得到瞭三種不同的結果, 匯總如下:

堆內存大小(Heap) GC算法(GC Algorithm) 有效時間比(Useful work) 最長停頓時間(Longest pause)
-Xmx12g -XX:+UseConcMarkSweepGC 89.8% 560 ms
-Xmx12g -XX:+UseParallelGC 91.5% 1,104 ms
-Xmx8g -XX:+UseConcMarkSweepGC 66.3% 1,610 ms

使用不同的GC算法,和不同的內存配置,運行相同的代碼, 以測量GC暫停時間與 延遲、吞吐量的關系。實驗的細節和結果在後面章節詳細介紹。

註意, 為瞭盡量簡單, 示例中隻改變瞭很少的輸入參數, 此實驗也沒有在不同CPU數量或者不同的堆佈局下進行測試。

Tuning for Latency(調優延遲指標)

假設有一個需求, 每次作業必須在 1000ms 內處理完成。我們知道, 實際的作業處理隻需要100 ms,簡化後, 兩者相減就可以算出對 GC暫停的延遲要求。現在需求變成: GC暫停不能超過900ms。這個問題很容易找到答案, 隻需要解析GC日志文件, 並找出GC暫停中最大的那個暫停時間即可。

再來看測試所用的三個配置:

堆內存大小(Heap) GC算法(GC Algorithm) 有效時間比(Useful work) 最長停頓時間(Longest pause)
-Xmx12g -XX:+UseConcMarkSweepGC 89.8% 560 ms
-Xmx12g -XX:+UseParallelGC 91.5% 1,104 ms
-Xmx8g -XX:+UseConcMarkSweepGC 66.3% 1,610 ms

可以看到,其中有一個配置達到瞭要求。運行的參數為:

java -Xmx12g -XX:+UseConcMarkSweepGC Producer

對應的GC日志中,暫停時間最大為 560 ms, 這達到瞭延遲指標 900 ms 的要求。如果還滿足吞吐量和系統容量需求的話,就可以說成功達成瞭GC調優目標, 調優結束。

Tuning for Throughput(吞吐量調優)

假定吞吐量指標為: 每小時完成 1300萬次操作處理。同樣是上面的配置, 其中有一種配置滿足瞭需求:

堆內存大小(Heap) GC算法(GC Algorithm) 有效時間比(Useful work) 最長停頓時間(Longest pause)
-Xmx12g -XX:+UseConcMarkSweepGC 89.8% 560 ms
-Xmx12g -XX:+UseParallelGC 91.5% 1,104 ms
-Xmx8g -XX:+UseConcMarkSweepGC 66.3% 1,610 ms

此配置對應的命令行參數為:

java -Xmx12g -XX:+UseParallelGC Producer

可以看到,GC占用瞭 8.5%的CPU時間,剩下的 91.5% 是有效的計算時間。為簡單起見, 忽略示例中的其他安全點。現在需要考慮:

  • 每個CPU核心處理一次作業需要耗時 100ms
  • 因此, 一分鐘內每個核心可以執行 60,000 次操作(每個job完成100次操作)
  • 一小時內, 一個核心可以執行 360萬次操作
  • 有四個CPU內核, 則每小時可以執行: 4 x 3.6M = 1440萬次操作

理論上,通過簡單的計算就可以得出結論, 每小時可以執行的操作數為:  次, 滿足需求。

值得一提的是, 假若還要滿足延遲指標, 那就有問題瞭, 最壞情況下, GC暫停時間為 1,104 ms最大延遲時間是前一種配置的兩倍。

Tuning for Capacity(調優系統容量)

假設需要將軟件部署到服務器上(commodity-class hardware), 配置為 4核10G。這樣的話, 系統容量的要求就變成: 最大的堆內存空間不能超過 8GB有瞭這個需求, 我們需要調整為第三套配置進行測試:

堆內存大小(Heap) GC算法(GC Algorithm) 有效時間比(Useful work) 最長停頓時間(Longest pause)
-Xmx12g -XX:+UseConcMarkSweepGC 89.8% 560 ms
-Xmx12g -XX:+UseParallelGC 91.5% 1,104 ms
-Xmx8g -XX:+UseConcMarkSweepGC 66.3% 1,610 ms

程序可以通過如下參數執行:

java -Xmx8g -XX:+UseConcMarkSweepGC Producer

測試結果是延遲大幅增長, 吞吐量同樣大幅降低:

  • 現在,GC占用瞭更多的CPU資源, 這個配置隻有 66.3%的有效CPU時間。因此,這個配置讓吞吐量從最好的情況 13,176,000 操作/小時 下降到 不足 9,547,200次操作/小時.
  • 最壞情況下的延遲變成瞭 1,610 ms, 而不再是 560ms。

通過對這三個維度的介紹, 你應該瞭解, 不是簡單的進行“性能(performance)”優化, 而是需要從三種不同的維度來進行考慮, 測量, 並調優延遲和吞吐量, 此外還需要考慮系統容量的約束。

以上就是jvm垃圾回收GC調優基礎原理分析的詳細內容,更多關於jvm垃圾回收GC調優的資料請關註WalkonNet其它相關文章!

原文鏈接:https://plumbr.io/handbook/gc-tuning

推薦閱讀: