給祖傳系統做了點 GC調優,暫停時間降低了 90%

来源:https://www.cnblogs.com/Jcloud/archive/2023/12/13/17898336.html
-Advertisement-
Play Games

公司某規則引擎系統,在每次發版啟動會手動預熱,預熱完成當流量切進來之後會偶發的出現一次長達1-2秒的Young GC(流量並不大,並且LB下的每個節點都會出現該情況)在這次長暫停之後,每一次的年輕代GC暫停時間又都恢覆在20-100ms以內2秒雖然看起來不算長吧,但規則引擎每次執行也才幾毫秒,這誰能... ...


問題描述

公司某規則引擎系統,在每次發版啟動會手動預熱,預熱完成當流量切進來之後會偶發的出現一次長達1-2秒的Young GC(流量並不大,並且LB下的每個節點都會出現該情況)

在這次長暫停之後,每一次的年輕代GC暫停時間又都恢覆在20-100ms以內

2秒雖然看起來不算長吧,但規則引擎每次執行也才幾毫秒,這誰能忍?而且這玩意一旦超時,出單可能也跟著超時失敗!

問題分析

在分析該系統GC日誌後發現,2s暫停發生在Young GC階段,而且每次發生長暫停的Young GC都會伴隨著新生代對象的晉升(Promotion)

核心JVM參數(Oracle JDK7)

-Xms10G 
-Xmx10G 
-XX:NewSize=4G 
-XX:PermSize=1g 
-XX:MaxPermSize=4g 
-XX:+



可能有人會問,為什麼給這麼大記憶體?祖傳代碼,記憶體小了跑不動!

啟動後第一次年輕代GC日誌

2023-04-23T16:28:31.108+0800: [GC2023-04-23T16:28:31.108+0800: [ParNew2023-04-23T16:28:31.229+0800: [SoftReference, 0 refs, 0.0000950 secs]2023-04-23T16:28:31.229+0800: [WeakReference, 1156 refs, 0.0001040 secs]2023-04-23T16:28:31.229+0800: [FinalReference, 10410 refs, 0.0103720 secs]2023-04-23T16:28:31.240+0800: [PhantomReference, 286 refs, 2 refs, 0.0129420 secs]2023-04-23T16:28:31.253+0800: [JNI Weak Reference, 0.0000000 secs]
Desired survivor size 214728704 bytes, new threshold 1 (max 15)
- age   1:  315529928 bytes,  315529928 total
- age   2:   40956656 bytes,  356486584 total
- age   3:    8408040 bytes,  364894624 total
: 3544342K->374555K(3774912K), 0.1444710 secs] 3544342K->374555K(10066368K), 0.1446290 secs] [Times: user=1.46 sys=0.09, real=0.15 secs] 


長暫停年輕代GC日誌

2023-04-23T17:18:28.514+0800: [GC2023-04-23T17:18:28.514+0800: [ParNew2023-04-23T17:18:29.975+0800: [SoftReference, 0 refs, 0.0000660 secs]2023-04-23T17:18:29.975+0800: [WeakReference, 1224 refs, 0.0001400 secs]2023-04-23T17:18:29.975+0800: [FinalReference, 8898 refs, 0.0149670 secs]2023-04-23T17:18:29.990+0800: [PhantomReference, 600 refs, 1 refs, 0.0344300 secs]2023-04-23T17:18:30.025+0800: [JNI Weak Reference, 0.0000210 secs]
Desired survivor size 214728704 bytes, new threshold 15 (max 15)
- age   1:   79203576 bytes,   79203576 total
: 3730075K->304371K(3774912K), 1.5114000 secs] 3730075K->676858K(10066368K), 1.5114870 secs] [Times: user=6.32 sys=0.58, real=1.51 secs] 


從這個長暫停的GC日誌來看,是發生了晉升的,在Young GC後,有363M+的對象晉升到了老年代,這個晉升操作因該就是耗時原因(ps: 檢查過safepoint原因,不存在異常)

由於日誌參數中沒有配置-XX:+PrintHeapAtGC參數,這裡是手動計算的晉升大小:

年輕代年輕變化 - 全堆容量變化 = 晉升大小
(304371K - 3730075K) - (676858K - 3730075K) = 372487K(363M)


下一次年輕代GC日誌

2023-04-23T17:23:39.749+0800: [GC2023-04-23T17:23:39.749+0800: [ParNew2023-04-23T17:23:39.774+0800: [SoftReference, 0 refs, 0.0000500 secs]2023-04-23T17:23:39.774+0800: [WeakReference, 3165 refs, 0.0002720 secs]2023-04-23T17:23:39.774+0800: [FinalReference, 3520 refs, 0.0021520 secs]2023-04-23T17:23:39.776+0800: [PhantomReference, 150 refs, 1 refs, 0.0051910 secs]2023-04-23T17:23:39.782+0800: [JNI Weak Reference, 0.0000100 secs]
Desired survivor size 214728704 bytes, new threshold 15 (max 15)
- age   1:   17076040 bytes,   17076040 total
- age   2:   40832336 bytes,   57908376 total
: 3659891K->90428K(3774912K), 0.0321300 secs] 4032378K->462914K(10066368K), 0.0322210 secs] [Times: user=0.30 sys=0.00, real=0.03 secs] 


乍一看好像沒什麼問題,仔細想想還是發現了不對勁,為什麼程式剛啟動第二次gc就發生了晉升?

image.png

推測這裡應該是動態年齡判定導致的,GC中晉升年齡閾值並不是固定的15,而是jvm每次gc後動態計算的

年輕代晉升機制

為了能更好地適應不同程式的記憶體狀況,虛擬機並不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡

《深入理解Java虛擬機》一書中提到,對象晉升年齡的閾值是動態判定的。

不過經查閱其他資料和驗證後,發現此處和《深入理解Java虛擬機》解釋的有些出入

其實就是按年齡給對象分組,取total(累加值,小於等與當前年齡的對象總大小)最大的年齡分組,如果該分組的total大於survivor的一半,就將晉升年齡閾值更新為該分組的年齡

註意:不是是超過survivor一半就晉升,超過survivor一半隻會重新設置晉升閾值(threshold),在下一次GC才會使用該新閾值

3544342K->374555K(3774912K), 0.1444710 secs] 年輕代

3544342K->374555K(10066368K), 0.1446290 secs] 全堆


從上面第一次的GC日誌也可以證明這個結論,在這次GC中全堆的記憶體變化和年輕代記憶體變化是相等的,所以並沒有發生對象的晉升

就像上面的日誌中,第一次GC只是將threshold設置為1,因為此時survivor一半為214728704 bytes,而年齡為1的對象總和有315529928 bytes,超過了Desired survivor size,所以在本次GC後將threshold設置為年齡為1的對象年齡1

這裡更新了對象晉升年齡閾值為1
Desired survivor size 214728704 bytes, new threshold 1 (max 15)
- age   1:  315529928 bytes,  315529928 total
- age   2:   40956656 bytes,  356486584 total
- age   3:    8408040 bytes,  364894624 total


這裡順便解釋下這個年齡分佈的輸出內容:

- age   1:  315529928 bytes,  315529928 total 


- age 1表示年齡為1的對象分組,315529928 bytes表示年齡為1的對象占用記憶體大小

315529928 total這個是一個累加值,表示小於等於當前分組年齡的對象總大小。先把對象按年齡分組,age 1的分組total為age 1總大小(前面的xxx bytes),age 2的分組total為age 1 + age 2總大小,age n的分組total為age 1 + age 2 + ... +age n的總大小,累加規則如下圖所示

image.png

當total最大的分組的total值超過了survivor/2時,就會更新晉升閾值

在第二次年輕代GC“長暫停年輕代GC日誌”中,由於新的晉升年齡閾值為1,所以那些經歷了一次GC並存活並且現在仍然可達(reachable)的對象們就會發生晉升了

由於此次GC發生了363M的對象晉升,所以導致了長暫停

思考

JVM中這個“動態對象年齡判定”真的合理嗎?

個人認為機制是好的,可以更好的適應不同程式的記憶體狀況,但不是任何場景都適合,比如在本文中這個剛啟動不就GC的場景下就會有問題

因為在程式剛啟動時,大多數對象年齡都是0或者1,很容易出現年齡為1的大量存活對象;在這個“動態對象年齡判定”機制下,就會導致新的晉升閾值被設置為1,導致這些不該晉升的對象發生了晉升

比如程式在初始化,正在載入各種資源時發生了Young GC,載入邏輯還在執行中,很多新建的對象年齡在這次GC時還是可達的(reachable)

經歷了這次GC後,這些對象年齡更新為1,但是由於“動態對象年齡判定”機制的影響,晉升年齡閾值更新為了“最大的對象年齡分組”的年齡,也就是這批剛經歷了一次GC的對象們

在這次GC之後不久,資源初始化完成了,涉及的相關對象有很可能不可達了,但是由於剛纔晉升年齡閾值被更新為了1,在下一次正常的Young GC這批年齡為1的對象會直接發生晉升,提前或者說錯誤的發生了晉升

解決方案

經查閱文檔、資料,發現“動態年齡判定”這個機制並不能禁用,所以如果想解決這個問題,只有靠“繞過”這個計算規則了

動態年齡的判定,是根據Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半來判定的,那麼根據這個機制解決也很簡單

由於我們足夠瞭解自己的系統,清楚的知道載入資源所需的大概記憶體,完全可以設定一個大於這些暫時可達的對象總和的數值來作為survivor的容量

比如上面的日誌中,第一次GC後年齡為1的對象有315529928 Bytes(300M),Desired survivor size為(survivor size /2)214728704 bytes(204M),那麼survivor就可以設置為600M以上。

不過為了穩妥,還是將survivor調到800M,這樣desired survivor size就是400M左右,在第一次Young GC後,就不會因年齡為1的對象總和超過了desired survivor size而導致晉升年齡閾值的更新了,從而也就不會有提前/錯誤晉升而導致的GC長暫停問題

survivor不可以直接指定大小,不過可以通過-XX:SurvivorRatio這種調節比例的方式來調節survivor大小

-XX:SurvivorRatio=8


表示兩個Survivor和Edgen區的比,8表示兩個Survivor:Eden=2:8,即一個Survivor占新生代的1/10。

計算方式為:
CleanShot 2023-12-08 at 09.24.23@2x.png

變形一下,Eden 的大小計算公式為:

CleanShot 2023-12-08 at 09.28.35@2x.png

這裡用一張堆疊柱狀圖來詳細的解釋 SurvivorRatio 不同數值下 Eden/Survivor 的空間比例:

image.png

好了,現在直接通過比例,強行給 Survivor 調大

-XX:SurvivorRatio=3


調整之後,Survivor 總占比為 40%,大小為 1717829632 Bytes,單個 S0/S1的一半也有 10% - 429457408 Bytes,遠超 age=1 的分組總大小 315529928 Bytes。

這樣一來, Young GC 後複製到 Survivor 的對象(最大年齡分組)占總比例的大小就不會到 50% 了,也就不會把 MaxTenuringThreshold 更新為 1 ,自然就解決了這個“亂晉升”的問題**

改完收工,再次發版手動預熱後,再也沒有切量後長暫停的問題了,Young GC穩定在 30-100ms,成功解決!

擴展

為什麼晉升300M比年輕代回收3G還要慢這麼多倍

根據複製演算法的特性,複製演算法的時間消耗主要取決於存活對象的大小,而不是總空間的大小

比如上面4G的年輕代(實際只有Eden+S0可用),GC時只需要從GC ROOTS開始遍歷對象圖,將可達的對象複製至S1即可,並不需要遍歷整個年輕代

複製演算法的詳細介紹可以參考我的另一篇《垃圾回收演算法實現之 - 複製演算法(完整可運行C語言代碼)》

在上面那次長暫停GC日誌中,發生了363M的晉升,300M左右的回收,對比第一次GC基本可以得出,花費的1.5S基本上都是在晉升操作

為什麼晉升操作這麼耗時?

晉升畢竟涉及跨代複製啊(其實都年輕代和老年代都是heap,在複製這件事上本質上沒什麼區別,都是memcpy而已,只是需要額外處理的邏輯更多了)

,所需處理的邏輯會更複雜,比如指針的更新等操作,更耗時也是可以理解嗎嘛,

本地代碼模擬

這裡也附上一段可以在本地模擬問題的代碼,Oracle JDK7下可直接運行測試

//jdk7.。

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class PromotionTest {
    public static void main(String[] args) throws IOException {
        //模擬初始化資源場景
        List<Object> dataList = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            dataList.add(new InnerObject());
        }
        //模擬流量進入場景
        for (int i = 0; i < 73; i++) {
            if(i == 72){
                System.out.println("Execute young gc...Adjust promotion threshold to 1");
            }
            new InnerObject();
        }
        System.out.println("Execute full gc...dataList has been promoted to cms old space");
        //這裡註意dataList中的對象在這次Full GC後會進入老年代
        System.gc();
    }
    public static byte[] createData(){
        int dataSize = 1024*1024*4;//4m
        byte[] data = new byte[dataSize];
        for (int j = 0; j < dataSize; j++) {
            data[j] = 1;
        }
        return data;
    }
    static class InnerObject{
        private Object data;

        public InnerObject() {
            this.data = createData();
        }
    }
}


jvm options

-server -Xmn400M -XX:SurvivorRatio=9 -Xms1000M -Xmx1000M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintHeapAtGC -XX:+PrintReferenceGC -XX:+PrintGCApplicationStoppedTime -XX:+UseConcMarkSweepGC


註意,文中垃圾回收相關的機制解釋,都是基於 HotSpot JVM,Parallel New + CMS Old 。

參考

作者:京東保險 蔣信

來源:京東雲開發者社區 轉載請註明來源


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 上一期我們瞭解了 小程式底部導航欄 的實現效果,今天一起來瞭解下如何設置小程式標題欄~ 基礎標題欄 小程式標題欄主要包含返回、標題、收藏、菜單、收起 5 個模塊,其中能夠調整的部分只有標題和背景色。 另外 IDE上無法展示收藏按鈕,如果測試收藏按鈕的相關功能 需要使用真機模式調試。 1、基礎屬性 � ...
  • 今天,我們來看這麼一個非常常見的切圖場景,我們需要一個帶圓角的虛線邊框,像是這樣: 這個我們使用 CSS 還是可以輕鬆解決的,代碼也很簡單,核心代碼: div { border-radius: 25px; border: 2px dashed #aaa; } 但是,原生的 dashed 有一個問題, ...
  • 題目:輸入兩個正整數 m 和 n,求其最大公約數和最小公倍數。 求出最大公約數就行,最小公倍數用m*n除以最大公約數就行 package myself; import java.util.Scanner; /** * @Auther QY * @Date 2023/12/11 */ public c ...
  • 開始 日期格式化可以說是最常用的一個小知識了,例如格式化成"年-月-日"、"年-月-日 時-分-秒"、“年/月/日”等等,隨之也就出現了“yyyy-MM-dd”、"yyyy-MM-dd HH-mm-ss"等格式,使用不當就會一臉懵逼。 運行 public static void main(Strin ...
  • Qt 是一個跨平臺C++圖形界面開發庫,利用Qt可以快速開發跨平臺窗體應用程式,在Qt中我們可以通過拖拽的方式將不同組件放到指定的位置,實現圖形化開發極大的方便了開發效率,本章將重點介紹CheckBox單行輸入框組件的常用方法及靈活運用。QCheckBox是 Qt 中用於實現覆選框的組件,它提供了豐... ...
  • 隨著金融科技的不斷發展,越來越多的線上查詢工具被應用到汽車管理領域。一款名為汽車管理線上查詢工具,定位車輛,輕鬆追蹤的工具就是其中之一。此工具通過API介面代碼實現了車牌號查車輛信息、車輛故障碼、VIN查詢汽車品牌以及二手車估值等功能,為用戶提供了準確、便捷、高效的汽車管理服務。 首先,車牌號查車輛 ...
  • 數據的預處理是數據分析,或者機器學習訓練前的重要步驟。通過數據預處理,可以 提高數據質量,處理數據的缺失值、異常值和重覆值等問題,增加數據的準確性和可靠性 整合不同數據,數據的來源和結構可能多種多樣,分析和訓練前要整合成一個數據集 提高數據性能,對數據的值進行變換,規約等(比如無量綱化),讓演算法更加 ...
  • 1. 同城雙活是什麼 同城雙活是一種容災架構的設計模式,主要用於提高系統的可用性和容錯性。它通常涉及在同一個城市內建立兩個數據中心(機房),這兩個數據中心同時對外提供服務,實現了高可用性和冗餘。 關鍵特點和優勢包括: 雙活部署: 兩個數據中心都處於活躍狀態,同時處理用戶請求。這樣,當一個數據中心發生 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...