一次JVM GC長暫停的排查過程

来源:https://www.cnblogs.com/Jcloud/archive/2023/01/10/17039227.html
-Advertisement-
Play Games

在高併發下,Java程式的GC問題屬於很典型的一類問題,帶來的影響往往會被進一步放大。不管是「GC頻率過快」還是「GC耗時太長」,由於GC期間都存在Stop The World問題,因此很容易導致服務超時,引發性能問題。 ...


作者:京東科技 徐傳樂

背景

在高併發下,Java程式的GC問題屬於很典型的一類問題,帶來的影響往往會被進一步放大。不管是「GC頻率過快」還是「GC耗時太長」,由於GC期間都存在Stop The World問題,因此很容易導致服務超時,引發性能問題。

事情最初是線上某應用垃圾收集出現Full GC異常的現象,應用中個別實例Full GC時間特別長,持續時間約為15~30秒,平均每2周左右觸發一次;

JVM參數配置“-Xms2048M –Xmx2048M –Xmn1024M –XX:MaxPermSize=512M”

排查過程

Ø 分析GC 日誌

GC 日誌它記錄了每一次的 GC 的執行時間和執行結果,通過分析 GC 日誌可以調優堆設置和 GC 設置,或者改進應用程式的對象分配模式。

這裡Full GC的reason是Ergonomics,是因為開啟了UseAdaptiveSizePolicy,jvm自己進行自適應調整引發的Full GC。

這份日誌主要體現GC前後的變化,目前為止看不出個所以然來。

開啟GC日誌,需要添加如下 JVM 啟動參數:

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/export/log/risk_pillar/gc.log

常見的 Young GC、Full GC 日誌含義如下:

Ø 進一步查看伺服器性能指標

獲取到了GC耗時的時間後,通過監控平臺獲取到各個監控項,開始排查這個時點有異常的指標,最終分析發現,在5.06分左右(GC的時點),CPU占用顯著提升,而SWAP出現了釋放資源、memory資源增長出現拐點的情況(詳見下圖紅色框,橙色框中的變化是因修改配置導致,後面會介紹,暫且可忽略)

JVM用到了swap?是因為GC導致的CPU突然飆升,並且釋放了swap交換區這部分記憶體到memory?

為了驗證JVM是否用到swap,我們通過檢查proc下的進程記憶體資源占用情況

| for i in $( cd /proc;ls |grep "[1]"|awk ' $0 >100') ;do awk '/Swap:/{a=a+$2}END{print '"$i"',a/1024"M"}' /proc/$i/smaps 2>/dev/null ; done | sort -k2nr | head -10 # head -10 表示 取出 前10個記憶體占用高的進程 # 取出的第一列為進程的id 第二列進程占用swap大小 |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |

看到確實有用到305MB的swap

這裡簡單介紹下什麼是swap?

swap指的是一個交換分區或文件,主要是在記憶體使用存在壓力時,觸發記憶體回收,這時可能會將部分記憶體的數據交換到swap空間,以便讓系統不會因為記憶體不夠用而導致oom或者更致命的情況出現。

當某進程向OS請求記憶體發現不足時,OS會把記憶體中暫時不用的數據交換出去,放在swap分區中,這個過程稱為swap out。

當某進程又需要這些數據且OS發現還有空閑物理記憶體時,又會把swap分區中的數據交換回物理記憶體中,這個過程稱為swap in。

為了驗證GC耗時與swap操作有必然關係,我抽查了十幾台機器,重點關註耗時長的GC日誌,通過時間點確認到GC耗時的時間點與swap操作的時間點確實是一致的。

進一步查看虛擬機各實例 swappiness 參數,一個普遍現象是,凡是發生較長Full GC的實例都配置了參數 vm.swappiness = 30(值越大表示越傾向於使用swap);而GC時間相對正常的實例配置參數 vm.swappiness = 0(最大限度地降低使用swap)。

swappiness 可以設置為 0 到 100 之間的值,它是Linux的一個內核參數,控制系統在進 行swap時,記憶體使用的相對權重。

Ø swappiness=0: 表示最大限度使用物理記憶體,然後才是 swap空間

Ø swappiness=100: 表示積極的使用swap分區,並且把記憶體上的數據及時的交換到swap空間裡面

對應的物理記憶體使用率和swap使用情況如下

至此,矛頭似乎都指向了swap。

Ø 問題分析

當記憶體使用率達到水位線(vm.swappiness)時,linux會把一部分暫時不使用的記憶體數據放到磁碟swap去,以便騰出更多可用記憶體空間;

當需要使用位於swap區的數據時,再將其換回記憶體中,當JVM進行GC時,需要對相應堆分區的已用記憶體進行遍歷;

假如GC的時候,有堆的一部分內容被交換到swap空間中,遍歷到這部分的時候就需要將其交換回記憶體,由於需要訪問磁碟,所以相比物理記憶體,它的速度肯定慢的令人髮指,GC停頓的時間一定會非常非常恐怖;

進而導致Linux對swap分區的回收滯後(記憶體到磁碟換入換出操作十分占用CPU與系統IO),在高併發/QPS服務中,這種滯後帶來的結果是致命的(STW)。

Ø 問題解決

至此,答案似乎很清晰,我們只需嘗試把swap關閉或釋放掉,看看能否解決問題?

如何釋放swap?

  1. 設置vm.swappiness=0(重啟應用釋放swap後生效),表示儘可能不使用交換記憶體

a、 臨時設置方案,重啟後不生效

設置vm.swappiness為0

sysctl vm.swappiness=0

查看swappiness值

cat /proc/sys/vm/swappiness

b、 永久設置方案,重啟後仍然生效

vi /etc/sysctl.conf

添加

vm.swappiness=0

  1. 關閉交換分區swapoff –a

前提:首先要保證記憶體剩餘要大於等於swap使用量,否則會報Cannot allocate memory!swap分區一旦釋放,所有存放在swap分區的文件都會轉存到物理記憶體上,可能會引發系統IO或者其他問題。

a、 查看當前swap分區掛載在哪?

b、 關停分區

關閉swap交換區後的記憶體變化見下圖橙色框,此時swap分區的文件都轉存到了物理記憶體上

關閉Swap交換區後,於2.23再次發生Full GC,耗時190ms,問題得到解決。

Ø 疑惑

1、 是不是只要開啟了swap交換區的JVM,在GC的時候都會耗時較長呢?

2、 既然JVM對swap如此不待見,為何JVM不明令禁止使用呢?

3、 swap工作機制是怎樣的?這台物理記憶體為8g的server,使用了交換區記憶體(swap),說明物理記憶體不夠使用了,但是通過free命令查看記憶體使用情況,實際物理記憶體似乎並沒有占用那麼多,反而Swap已占近1G?

free:除了buff/cache剩餘了多少記憶體

shared:共用記憶體

buff/cache:緩衝、緩存區記憶體數(使用過高通常是程式頻繁存取文件)

available:真實剩餘的可用記憶體數


大家可以想想,關閉交換磁碟緩存意味著什麼?

其實大可不必如此激進,要知道這個世界永遠不是非0即1的,大家都會或多或少選擇走在中間,不過有些偏向0,有些偏向1而已。

很顯然,在swap這個問題上,JVM可以選擇偏向儘量少用,從而降低swap影響,要降低swap影響有必要弄清楚Linux記憶體回收是怎麼工作的,這樣才能不遺漏任何可能的疑點。

先來看看swap是如何觸發的?

Linux會在兩種場景下觸發記憶體回收,一種是在記憶體分配時發現沒有足夠空閑記憶體時會立刻觸發記憶體回收;另一種是開啟了一個守護進程(kswapd進程)周期性對系統記憶體進行檢查,在可用記憶體降低到特定閾值之後主動觸發記憶體回收。

通過如下圖示可以很容易理解,詳細信息參見:http://hbasefly.com/2017/05/24/hbase-linux/

解答是不是只要開啟了swap交換區的JVM,在GC的時候都會耗時較長

筆者去查了一下另外的一個應用,相關指標信息請見下圖。

實名服務的QPS是非常高的,同樣能看到應用了swap,GC平均耗時 576ms,這是為什麼呢?

通過把時間範圍聚焦到發生GC的某一時間段,從監控指標圖可以看到swapUsed沒有任何變化,也就是說沒有swap活動,進而沒有影響到垃級回收的總耗時。

通過如下命令列舉出各進程swap空間占用情況,很清楚的看到實名這個服務swap空間占用的較少(僅54.2MB)

另一個顯著的現象是實名服務Full GC間隔較短(幾個小時一次),而我的服務平均間隔2周一次Full GC

基於以上推測

1、 實名服務由於 GC 間隔較短,記憶體中的東西根本沒有機會置換到swap中就被回收了,GC的時候不需要將swap分區中的數據交換回物理記憶體中,完全基於記憶體計算,所以要快很多

2、 將哪些記憶體數據置換進swap交換區的篩選策略應該是類似於LRU演算法(最近最少使用原則)

為了證實上述猜測,我們只需跟蹤swap變更日誌,監控數據變化即可得到答案,這裡採用一段shell 腳本實現

#!/bin/bash 
echo -e `date +%y%m%d%H%M%S` 
echo -e "PID\t\tSwap\t\tProc_Name" 

#拿出/proc目錄下所有以數字為名的目錄(進程名是數字才是進程,其他如sys,net等存放的是其他信息) 
for pid in `ls -l /proc | grep ^d | awk '{ print $9 }'| grep -v [^0-9]` 
do 
    if [ $pid -eq 1 ];then continue;fi 
    grep -q "Swap" /proc/$pid/smaps 2>/dev/null 
    if [ $? -eq 0 ];then 
        swap=$(gawk '/Swap/{ sum+=$2;} END{ print sum }' /proc/$pid/smaps) #統計占用的swap分區的 大小 單位是KB 
        proc_name=$(ps aux | grep -w "$pid" | awk '!/grep/{ for(i=11;i<=NF;i++){ printf("%s ",$i); }}') #取出進程的名字 
        if [ $swap -gt 0 ];then #判斷是否占用swap 只有占用才會輸出 
            echo -e "${pid}\t${swap}\t${proc_name:0:100}" 
    fi 
   fi
done | sort -k2nr | head -10 | gawk -F'\t' '{ #排序取前 10 
    pid[NR]=$1; 
    size[NR]=$2; 
    name[NR]=$3; 
} 
END{ 
    for(id=1;id<=length(pid);id++) 
    { 
    if(size[id]<1024) 
        printf("%-10s\t%15sKB\t%s\n",pid[id],size[id],name[id]); 
    else if(size[id]<1048576) 
        printf("%-10s\t%15.2fMB\t%s\n",pid[id],size[id]/1024,name[id]);
    else 
    printf("%-10s\t%15.2fGB\t%s\n",pid[id],size[id]/1048576,name[id]); 
    } 
}'

由於上面圖中 2022.3.2 19:57:00 至 2022.3.2 19:58:00 發生了一次Full GC,我們重點關註下這一分鐘內swap交換區的變化即可,我這裡每10s做一次信息採集,可以看到在GC時點前後,swap確實沒有變化

通過上述分析,回歸本文核心問題上,現在看來我的處理方式過於激進了,其實也可以不用關閉swap,通過適當降低堆大小,也是能夠解決問題的。

這也側面的說明,部署Java服務的Linux系統,在記憶體分配上並不是無腦大而全,需要綜合考慮不同場景下JVM對Java永久代 、Java堆(新生代和老年代)、線程棧、Java NIO所使用記憶體的需求。

總結

綜上,我們得出結論,swap和GC同一時候發生會導致GC時間非常長,JVM嚴重卡頓,極端的情況下會導致服務崩潰。

主要原因是:JVM進行GC時,需要對對應堆分區的已用記憶體進行遍歷,假如GC的時候,有堆的一部分內容被交換到swap中,遍歷到這部分的時候就須要將其交換回記憶體;更極端情況同一時刻因為記憶體空間不足,就需要把記憶體中堆的另外一部分換到SWAP中去,於是在遍歷堆分區的過程中,會把整個堆分區輪流往SWAP寫一遍,導致GC時間超長。線上應該限制swap區的大小,如果swap占用比例較高應該進行排查和解決,適當的時候可以通過降低堆大小,或者添加物理記憶體。

因此,部署Java服務的Linux系統,在記憶體分配上要慎重。

以上內容希望可以起到拋轉引玉的作用,如有理解不到位的地方煩請指出。


  1. 0-9 ↩︎


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

-Advertisement-
Play Games
更多相關文章
  • 1高階函數 1.1 數學概念 回顧下數學知識: y=f(x) 這是最開始接觸的普通函數 y=g(f(x)) 這個就是我們接觸到的高階函數 在數學和電腦科學中,高階函數至少應當是滿足下麵一個條件的函數: 1)接受一個或者多個函數作為參數 2)輸出一個函數 程式中我們的高階函數也類似 示例計數器的函數 ...
  • Redis 數據結構-雙向鏈表 最是人間留不住,朱顏辭鏡花辭樹。 1、簡介 Redis 之所以快主要得益於它的數據結構、操作記憶體資料庫、單線程和多路 I/O 復用模型,進一步窺探下它常見的五種基本數據的底層數據結構。 Redis 常見數據類型對應的的底層數據結構。 String:簡單動態字元串。 L ...
  • 前言 今天給大家介紹的是Python爬蟲批量下載相親網站圖片數據,在這裡給需要的小伙伴們代碼,並且給出一點小心得。 首先是爬取之前應該儘可能偽裝成瀏覽器而不被識別出來是爬蟲,基本的是加請求頭,但是這樣的純文本數據爬取的人會很多,所以我們需要考慮更換代理IP和隨機更換請求頭的方式來對相親網站圖片數據進 ...
  • 一、MybatisPlusMax簡介 MybatisPlusMax是MybatisPlus的增強包,秉承只拓展不修改的理念,對MybatisPlus做增強。 正如MybatisPlus是對MyBatis的增強,MybatisPlusMax是對MybatisPlus的增強,拓展理念一脈相承。 Myba ...
  • 相較於最初的 Bourne shell,現代 bash 版本的最大改進之一體現在算術方面。早期的 shell 版本沒有內建的算術功能,哪怕是給變數加1,也得調用單獨的程式來完成。 1、算術方法一: $(( )) 只要都是整數運算,就可以在 $(( )) 的算術表達式內使用所有的標準運算符。還有一個額 ...
  • 大家好,我是三友~~ 這篇文章我準備來聊一聊RocketMQ消息的一生。 不知你是否跟我一樣,在使用RocketMQ的時候也有很多的疑惑: 消息是如何發送的,隊列是如何選擇的?消息是如何存儲的,是如何保證讀寫的高性能?RocketMQ是如何實現消息的快速查找的?RocketMQ是如何實現高可用的?消 ...
  • 原文:JavaFx 頁面和控制項設置快捷鍵 - Stars-One的雜貨小窩 之前說過一篇window系統全局快捷鍵的設置,本期主要是講解JavaFx應用程式的快捷鍵設置,還是有所區別的 這裡主要是Tornadofx為例進行講解,以Kotlin語言為例,由於比較簡單,就不貼截圖了,下麵例子都是自己測試 ...
  • 2023-01-10 一、Mybatis中獲取主鍵自增數據 要獲取自增數據時,需要在映射文件中的“<insert>”中添加兩個屬性,例如獲取自增的id ①EmployeeMapper.xml中的<mapper>標簽內部 <insert id="insertEmployee" useGenerated ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...