聽說你是高手?說說你的 JVM調優方法論 吧?(美團面試,問的賊細)

文章很長,且持續更新,建議收藏起來,慢慢讀!瘋狂創客圈總目錄 博客園版 爲您奉上珍貴的學習資源 :

免費贈送 :《尼恩Java面試寶典》 持續更新+ 史上最全 + 面試必備 2000頁+ 面試必備 + 大廠必備 +漲薪必備
免費贈送 :《尼恩技術聖經+高併發系列PDF》 ,幫你 實現技術自由,完成職業升級, 薪酬猛漲!加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷1)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷2)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷3)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領

免費贈送 資源寶庫: Java 必備 百度網盤資源大合集 價值>10000元 加尼恩領取


聽說你是高手?說說你的 JVM調優方法論 吧?(美團面試,問的賊細)

尼恩特別說明: 尼恩的文章,都會在 《技術自由圈》 公號 發佈, 並且維護最新版本。 如果發現圖片 不可見, 請去 《技術自由圈》 公號 查找

尼恩說在前面

在40歲老架構師 尼恩的讀者交流羣(50+)中,最近有小夥伴拿到了一線互聯網企業如得物、阿里、滴滴、極兔、有贊、希音、百度、網易、美團的面試資格,遇到很多很重要的面試題:

說說,你的JVM調優方法論?

說說,何時進行JVM調優?

說說,JVM調優的基本原則?

說說,JVM調優目標?

說說,JVM調優量化目標?

說說,JVM調優的步驟?

最近有小夥伴在面試 美團,又遇到了相關的面試題。小夥伴懵了,因爲沒有遇到過,所以支支吾吾的說了幾句,面試官不滿意,面試掛了。

所以,尼恩給大家做一下系統化、體系化的梳理,使得大家內力猛增,可以充分展示一下大家雄厚的 “技術肌肉”,讓面試官愛到 “不能自已、口水直流”,然後實現”offer直提”。

當然,這道面試題,以及參考答案,也會收入咱們的 《尼恩Java面試寶典PDF》V171版本,供後面的小夥伴參考,提升大家的 3高 架構、設計、開發水平。

最新《尼恩 架構筆記》《尼恩高併發三部曲》《尼恩Java面試寶典》的PDF,請關注本公衆號【技術自由圈】獲取,回覆:領電子書

本文的2個重量級作者:

  • 第一重量級作者 Owen(資深架構師,負責寫初稿 )
  • 第二重量級作者 尼恩 (40歲老架構師, 負責提升此文的 技術高度,讓大家有一種 俯視 技術的感覺)

《尼恩Java面試寶典》 是大家 面試的殺手鐗, 此文當最新PDF版本,可以找43歲老架構師尼恩獲取。

圖片

一:引言

在軟件開發和運維中,JVM作爲執行Java程序的核心引擎,扮演着至關重要的角色。

隨着應用程序的複雜性和負載不斷增加,對JVM的性能和穩定性要求也越來越高。

在此背景下,JVM調優變得至關重要。

JVM調優涉及到一系列的參數設置、垃圾收集器的選擇、內存分配策略等方面,對於提高Java應用程序的性能、減少內存泄漏、降低系統崩潰風險都有重要作用。

另外,在大廠面試中,JVM調優的知識也是備受關注的考察點,因爲它直接關係到系統的穩定性和性能優化。

候選人對JVM調優的理解和實踐能力,可以反映其在Java虛擬機運行機制方面的深度和廣度,

需要注意的是,調優並非首選方法,一般而言,解決性能問題的第一步是優化程序本身,只有在必要時才考慮進行JVM調優。

JVM調優,有什麼好處?

JVM調優目的是通過調整Java虛擬機的配置參數、垃圾回收策略和內存分配等手段,提升Java應用程序的性能、穩定性和可靠性。

隨着應用規模和用戶量的增長,原始的JVM配置可能無法滿足業務需求,因此必須進行調優以確保系統的正常運行。

然而,並不是所有異常情況都需要進行JVM調優。

在實際情況中,大多數問題可以通過分析JVM日誌文件和業務邏輯來定位,並通過業務層面的優化來解決。

儘管如此,深入瞭解各項參數和指標仍然至關重要,因爲它們有助於更快速地理解和解決問題,調優能帶來什麼好處?

  • 性能層面:

    通過調整JVM參數和優化垃圾回收機制,能夠提高Java應用程序的性能,減少延遲,提升系統響應速度和併發能力、和吞吐量。

  • 資源利用:

    合理配置JVM資源,包括內存、CPU等,能夠有效地利用硬件資源,提高系統的資源利用率,降低成本。

  • 穩定性: 通過調優JVM,可減少內存泄漏、OOM(Out of Memory)等問題的發生,提高系統的穩定性和可靠性,降低系統崩潰的風險。

二:JVM調優的關注哪些指標?

調優,到底調的是什麼?

調優之前,要搞清楚一個問題:怎樣纔算是“優”。

如何定性?

如何定量?

到底需要其實是需要關注幾個關鍵的指標,以全面評估系統的運行狀態和性能表現。

需要有一個具體的指標來衡量性能情況,而在JVM裏面衡量性能的兩個核心指標分別“吞吐量”和“停頓時間”。

核心指標1:吞吐量(throughput):

程序運行過程中執行兩種任務,分別是執行業務代碼的 任務 和進行垃圾回收的任務,

吞吐量大,意就是說程序運行業務代碼的時間越多, 換句話說,執行業務任務越多, 吞吐量就越高,

吞吐量計算公式 ,

吞吐量 = CPU在用戶應用程序運行的時間 / (CPU在用戶應用程序運行的時間 + CPU垃圾回收的時間),

在實踐中我們發現對於大多數的應用領域,評估一個垃圾收集(GC)算法如何,有如下一個核心標準:

  • 吞吐量越高越好

一般而言GC 的吞吐量不能低於 95%。

本質上,吞吐量是指應用程序線程用時佔程序總用時的比例。

例如,吞吐量99/100, 意味着100秒的程序執行時間,應用程序線程運行了99秒, 而在這一時間段內GC線程只運行了1秒。

核心指標2:停頓時間(pause times):

JVM在專門的線程(GC threads)中執行GC。

因爲JVM進行垃圾回收的時候,某些階段必須要停止業務線程專心進行垃圾收集, 只要GC線程是活動的,它們將與應用程序線程(application threads)爭用當前可用CPU的時鐘週期。

停頓時間(pause times) 是指一個時間段內應用程序線程讓與GC線程執行,而應用程序線程完全暫停。

例如,GC期間100毫秒的停頓時間, 意味着在這100毫秒期間內沒有應用程序線程是活動的。

如果說一個正在運行的應用程序有100毫秒的“平均停頓時間”,那麼就是說該應用程序所有的停頓時間平均長度爲100毫秒。

同樣,100毫秒的“最大停頓時間”是指:該應用程序所有的停頓時間最大不超過100毫秒。

注意,這裏說的JVM停頓時間,就是指JVM停止業務線程而去進行垃圾收集的這段時長,其實指的是每次GC造成用戶線程停頓的平均時間,不是總的垃圾回收時間。

停頓時間越長,就意味着GC場景下,用戶線程平均等待的時間越長,停頓時間會直接影響用戶使用系統的體驗。

除了吞吐量(throughput) 、停頓時間(pause times) 兩個核心指標,JVM調優還會關心下面的非核心指標:

核心指標3:堆內存佔用量:

細緻監控堆內存使用量、非堆內存使用量以及永久代(或元空間)使用量指標數據。

舉例來說,當堆內存使用量持續增加,而內存回收頻率較低時,可能暗示着潛在的內存泄漏問題,這可能導致系統性能下降或者最終的內存耗盡(OOM)。

非核指標1:垃圾回收次數

GC非常佔用CPU資源的,如果GC佔用的資源越多,那麼意味着其他事情所用的資源會減少,系統所能做的事情也會越少。

儘管垃圾回收過程會消耗大量的CPU資源,但是我們也不能單純地、一味的追求GC次數減少

爲啥? GC次數減少了,有可能單次GC的時間變長,那麼就可能會增加單次GC的“停頓時長”(核心指標2),

非核指標2:垃圾回收頻率

通常情況下,與垃圾回收次數相比,較低的垃圾回收頻率被認爲是更好的選擇。

垃圾回收的頻率,需要適中

  • 頻率過小,每次垃圾回收的時間會過長

  • 頻率過大,停頓時間長,延遲高

所以:通常來說垃圾回收頻率是越低越好。

詳細記錄GC頻率、GC停頓時間以及每次GC後的內存情況。

或者說:減少 GC次數可能會導致單次垃圾回收的時間變長,進而增加單次垃圾回收的“停頓時長”。

所以, 需要在這兩者之間做一些平衡。

吞吐量、暫停時間、堆內存佔用三者之間的關係

這三個指標不可能同時達到,因爲他們是一個不可能的關係

內存變大,要回收的東西變多,暫停時間自然增加.

吞吐量增加,必然要降低垃圾回收頻率,頻率降低,垃圾誰收停頓時間必然增大.

因此,目前gc的優化方向主要是吞吐量和暫停時間.

”高吞吐量”和”低停頓時間”是一對相互競爭的目標

高吞吐量最好因爲這會讓應用程序的最終用戶感覺只有應用程序線程在做“生產性”工作。

直覺上,吞吐量越高程序運行越快。

低停頓時間最好因爲從最終用戶的角度來看不管是GC還是其他原因導致一個應用被掛起始終是不好的。

這取決於應用程序的類型,有時候甚至短暫的200毫秒暫停都可能打斷終端用戶體驗。

因此,具有低的最大停頓時間是非常重要的,特別是對於一個交互式應用程序。

不幸的是”高吞吐量”和”低停頓時間”是一對相互競爭的目標(矛盾)。

GC需要一定的前提條件以便安全地運行。

例如,必須保證應用程序線程在GC線程試圖確定哪些對象仍然被引用和哪些沒有被引用的時候不修改對象的狀態。

爲此,應用程序在GC期間必須停止(或者僅在GC的特定階段,這取決於所使用的算法)。 然而這會增加額外的線程調度開銷:直接開銷是上下文切換,間接開銷是因爲緩存的影響。

加上JVM內部安全措施的開銷,這意味着GC及隨之而來的不可忽略的開銷,將增加GC線程執行實際工作的時間。

因此我們可以通過儘可能少運行GC,來最大化吞吐量,例如,只有在不可避免的時候進行GC,來節省所有與它相關的開銷。

然而,僅僅偶爾運行GC意味着每當GC運行時將有許多工作要做,因爲在此期間積累在堆中的對象數量很高。 單個GC需要花更多時間來完成, 從而導致更高的平均和最大停頓時間。

因此,考慮到低停頓時間,最好頻繁地運行GC以便更快速地完成。這反過來又增加了開銷並導致吞吐量下降,我們又回到了起點。

綜上所述,在設計(或使用)GC算法時,我們必須確定我們的目標:

一個GC算法只可能針對兩個目標之一(即只專注於最大吞吐量或最小停頓時間),或嘗試找到一個二者的折衷。

吞吐量和暫停時間是矛盾的,如何抉擇?

高吞吐量較好因爲這會讓應用程序的最終用戶感覺只有應用程序線程在做"生產性"工作。直覺上,吞吐量越高程序運行越快。

低暫停時間(低延遲)較好因爲從最終用戶的角度來看不管是GC還是其他原因導致一個應用被掛起始終是不好的。這取決於應用程序的類型, 有時候甚至短暫的200毫秒暫停都可能打斷終端用戶體驗 。因此,具有低的較大暫停時間是非常重要的,特別是對於一個 交互式應用程序 。

不幸的是"高吞吐量"和"低暫停時間"是一對相互競爭的目標(矛盾)。

  • 因爲如果選擇以吞吐量優先,那麼 必然需要降低內存回收的執行頻率 ,但是這樣會導致GC需要更長的暫停時間來執行內存回收。
  • 相反的,如果選擇以低延遲優先爲原則,那麼爲了降低每次執行的內存回收時的暫停時間,也 只能頻繁地執行內存回收 ,但這又引起了年輕代內存的縮減和導致程序吞吐量的下降。

在設計(或使用)GC算法時,我們必須確定我們的目標: 一個GC算法可能針對兩個目標之一(即只專注於較大吞吐量或最小暫停時間),或嘗試找到一個二者的折中。

現在標準: 在最大吞吐量優先的情況下,降低停頓時間

不同的垃圾回收器有不同的抉擇方向:

  • Parallel以吞吐量優先

  • cms以停頓時間優先

  • 而G1則取折中方案: 在保證用戶可接受的停頓時間的前提下,儘可能提高吞吐量.

JVM調優沒有萬能的公式和標準,因爲每個人所面對的場景是不一樣。

要想調整到最優的性能,其實首先要確認的是自己的需求目標是什麼(以吞吐量優先/停頓時間優先),

然後,根據這個目標去慢慢的調整各項指標,從而達到一個最佳的平衡點。

三:如何獲得JVM內存指標?

在項目啓動的時候 增加下列參數來收集GC日誌,然後通過第三方的日誌分析工具(比如GCesay:https://gceasy.io/

分析收集到的GC日誌來得到吞吐量、停頓時間相關的統計數據。

 java  
 -XX:+PrintGCDetails -XX:+PrintGCDateStamps 
 -XX:+UseGCLogFileRotation 
 -XX:+PrintHeapAtGC -XX:NumberOfGCLogFiles=5  
 -XX:GCLogFileSize=20M    
 -Xloggc:/opt/ard-user-gc-%t.log  
 -jar abg-user-1.0-SNAPSHOT.jar 
 -Xloggc:/opt/app/ard-user/ard-user-gc-%t.log   設置日誌目錄和日誌名稱
 -XX:+UseGCLogFileRotation           開啓滾動生成日誌
 -XX:NumberOfGCLogFiles=5            滾動GC日誌文件數,默認0,不滾動
 -XX:GCLogFileSize=20M               GC文件滾動大小,需開啓UseGCLogFileRotation
 -XX:+PrintGCDetails                 開啓記錄GC日誌詳細信息(包括GC類型、各個操作使用的時間),並且在程序運行結束打印出JVM的內存佔用情況
 -XX:+ PrintGCDateStamps             記錄系統的GC時間           
 -XX:+PrintGCCause                   產生GC的原因(默認開啓)

日誌分析工具有哪些?

我們看到日誌,尤其是CMS和G1的日誌,直接看日誌文檔都是很不方便的,密密麻麻的文字,其實市面上已經有一些日誌分析工具了。

進行系統調優時,首先需要對系統的各項指標進行檢測。爲了有效地進行監測和設置相應的閾值,我們通常會藉助監控工具,例如普羅米修斯等。在分析階段,以下工具是常用的:

  1. VisualVM:

    這是一個功能強大、可擴展的開源工具,用於深入分析Java應用程序。它提供了豐富的功能,包括性能監控、內存使用情況、垃圾回收情況等,並支持線程分析、堆快照等功能。

  2. Java Mission Control (JMC):

    由Oracle提供的專業Java性能監控和故障診斷工具。JMC集成了多種強大功能,包括垃圾回收分析、內存泄漏檢測、線程分析等。

  3. jvisualvm:

    這是JDK自帶的監控和調試工具,可用於監視本地和遠程Java應用程序的性能、內存使用情況等。它提供了直觀的圖形界面和豐富的監控指標。

  4. JConsole:

    JConsole是JDK自帶的監控工具,提供了基本的圖形界面,可用於監視Java應用程序的內存使用情況、線程信息、垃圾回收情況等。

  5. GCViewer:

    這是專門用於分析Java應用程序垃圾回收日誌的工具。

    GCViewer能將GC日誌解析成易於理解的圖表和統計信息,幫助用戶分析和優化垃圾回收行爲。

接下來,介紹使用 gceasy.io 進行 日誌分析

網址:https://gceasy.io/

注意:這款工具不需要我們下載軟件,他是在線的。

我們要做的就是兩步:

步驟一:導出GC日誌到本地磁盤

步驟二:將本地日誌上傳到gceasy.io上,進行分析

指標分析第一步:導出日誌

-Xloggc:/Users/lxl/Downloads/gc.log
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-XX:+PrintGCCause

  • ‐Xloggc參數:指定gc日誌的保存地址。這裏指定的是當前目錄,文件名以gc-+時間戳.log打印。%t表示時間戳
  • ‐XX:+PrintGCDetails:在日誌中打印GC詳情。
  • ‐XX:+PrintGCDateStamps:在日誌中打印GC的時間
  • ‐XX:+PrintGCTimeStamps:在日誌中打印GC耗時
  • ‐XX:+PrintGCCause : [這個參數沒查到]
  • ‐XX:+UseGCLogFileRotation:這個參數表示以滾動文件的形式打印日誌
  • ‐XX:NumberOfGCLogFiles:GC日誌文件的最大個數,這裏設置10個
  • ‐XX:GCLogFileSize:GC日誌每個文件的最大容量,這裏是100M

我們把日誌下載到Downloads文件夾下了。以下便是GC日誌的全部內容

Java HotSpot(TM) 64-Bit Server VM (25.202-b08) for bsd-amd64 JRE (1.8.0_202-b08), built on Dec 15 2018 20:16:16 by "java_re" with gcc 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00)
Memory: 4k page, physical 16777216k(1745536k free)
/proc/meminfo:
CommandLine flags: -XX:-BytecodeVerificationLocal -XX:-BytecodeVerificationRemote -XX:InitialHeapSize=268435456 -XX:+ManagementServer -XX:MaxHeapSize=4294967296 -XX:+PrintGC -XX:+PrintGCCause -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:TieredStopAtLevel=1 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC 
2022-01-12T15:02:37.044-0800: 0.839: [GC (Allocation Failure) [PSYoungGen: 65536K->4400K(76288K)] 65536K->4416K(251392K), 0.0043915 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
2022-01-12T15:02:37.308-0800: 1.103: [GC (Allocation Failure) [PSYoungGen: 69936K->4959K(76288K)] 69952K->5047K(251392K), 0.0046449 secs] [Times: user=0.02 sys=0.01, real=0.01 secs] 
2022-01-12T15:02:37.625-0800: 1.420: [GC (Allocation Failure) [PSYoungGen: 70495K->7467K(76288K)] 70583K->7563K(251392K), 0.0051392 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
2022-01-12T15:02:37.831-0800: 1.627: [GC (Allocation Failure) [PSYoungGen: 73003K->9356K(141824K)] 73099K->9460K(316928K), 0.0072596 secs] [Times: user=0.03 sys=0.01, real=0.00 secs] 
2022-01-12T15:02:37.869-0800: 1.664: [GC (Metadata GC Threshold) [PSYoungGen: 22322K->7049K(141824K)] 22426K->7161K(316928K), 0.0057809 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
2022-01-12T15:02:37.875-0800: 1.670: [Full GC (Metadata GC Threshold) [PSYoungGen: 7049K->0K(141824K)] [ParOldGen: 112K->6873K(87040K)] 7161K->6873K(228864K), [Metaspace: 20573K->20571K(1067008K)], 0.0237404 secs] [Times: user=0.09 sys=0.01, real=0.02 secs] 
2022-01-12T15:02:38.392-0800: 2.188: [GC (Allocation Failure) [PSYoungGen: 131072K->7194K(236032K)] 137945K->14075K(323072K), 0.0054542 secs] [Times: user=0.01 sys=0.01, real=0.00 secs] 
2022-01-12T15:02:39.850-0800: 3.646: [GC (Allocation Failure) [PSYoungGen: 235546K->9697K(270336K)] 242427K->20203K(357376K), 0.0092838 secs] [Times: user=0.02 sys=0.01, real=0.01 secs] 
2022-01-12T15:02:40.479-0800: 4.274: [GC (Metadata GC Threshold) [PSYoungGen: 179780K->12779K(397312K)] 190286K->25839K(484352K), 0.0117953 secs] [Times: user=0.04 sys=0.01, real=0.02 secs] 
2022-01-12T15:02:40.491-0800: 4.286: [Full GC (Metadata GC Threshold) [PSYoungGen: 12779K->0K(397312K)] [ParOldGen: 13059K->21448K(132096K)] 25839K->21448K(529408K), [Metaspace: 34068K->34068K(1079296K)], 0.0437361 secs] [Times: user=0.16 sys=0.01, real=0.04 secs] 
2022-01-12T15:02:42.177-0800: 5.972: [GC (Allocation Failure) [PSYoungGen: 384512K->13185K(399872K)] 405960K->34641K(531968K), 0.0115070 secs] [Times: user=0.04 sys=0.01, real=0.01 secs] 
2022-01-12T15:02:43.010-0800: 6.806: [GC (Allocation Failure) [PSYoungGen: 397697K->16864K(530432K)] 419153K->58461K(662528K), 0.0248406 secs] [Times: user=0.04 sys=0.02, real=0.02 secs] 
2022-01-12T15:02:44.338-0800: 8.133: [GC (Allocation Failure) [PSYoungGen: 530400K->26083K(539648K)] 571997K->86488K(671744K), 0.0302789 secs] [Times: user=0.06 sys=0.02, real=0.03 secs] 
2022-01-12T15:02:45.800-0800: 9.595: [GC (Allocation Failure) [PSYoungGen: 539619K->32647K(733696K)] 600024K->99769K(865792K), 0.0280332 secs] [Times: user=0.04 sys=0.02, real=0.02 secs] 
2022-01-12T15:02:47.765-0800: 11.560: [GC (Allocation Failure) [PSYoungGen: 729479K->41445K(738304K)] 796601K->124936K(870400K), 0.0370655 secs] [Times: user=0.04 sys=0.02, real=0.04 secs] 
2022-01-12T15:02:49.620-0800: 13.415: [GC (Allocation Failure) [PSYoungGen: 738277K->26677K(974848K)] 821768K->114930K(1106944K), 0.0270382 secs] [Times: user=0.05 sys=0.02, real=0.02 secs] 
2022-01-12T15:02:52.146-0800: 15.942: [GC (Allocation Failure) [PSYoungGen: 959541K->17569K(985600K)] 1047794K->110447K(1117696K), 0.0274985 secs] [Times: user=0.05 sys=0.01, real=0.03 secs] 
2022-01-12T15:02:54.110-0800: 17.905: [GC (Allocation Failure) [PSYoungGen: 950433K->10240K(1236480K)] 1043311K->109662K(1368576K), 0.0146713 secs] [Times: user=0.05 sys=0.01, real=0.01 secs] 
2022-01-12T15:02:54.692-0800: 18.487: [GC (Metadata GC Threshold) [PSYoungGen: 264005K->3360K(1259520K)] 363427K->109573K(1391616K), 0.0086901 secs] [Times: user=0.03 sys=0.01, real=0.01 secs] 
2022-01-12T15:02:54.701-0800: 18.496: [Full GC (Metadata GC Threshold) [PSYoungGen: 3360K->0K(1259520K)] [ParOldGen: 106213K->54092K(208384K)] 109573K->54092K(1467904K), [Metaspace: 56204K->56204K(1101824K)], 0.1487173 secs] [Times: user=0.69 sys=0.01, real=0.14 secs] 
2022-01-12T15:02:57.787-0800: 21.583: [GC (Allocation Failure) [PSYoungGen: 1209856K->49146K(1321984K)] 1263948K->116260K(1530368K), 0.0339265 secs] [Times: user=0.05 sys=0.01, real=0.04 secs] 
2022-01-12T15:03:16.198-0800: 39.994: [GC (Allocation Failure) [PSYoungGen: 1321978K->29589K(1335296K)] 1389092K->101049K(1543680K), 0.0214759 secs] [Times: user=0.06 sys=0.01, real=0.03 secs] 
2022-01-12T15:03:19.021-0800: 42.816: [GC (GCLocker Initiated GC) [PSYoungGen: 1302421K->60915K(1280512K)] 1373881K->180735K(1488896K), 0.0482886 secs] [Times: user=0.08 sys=0.01, real=0.05 secs] 
2022-01-12T15:03:21.847-0800: 45.642: [GC (Allocation Failure) [PSYoungGen: 1280499K->89087K(1308672K)] 1400321K->228379K(1517056K), 0.0336500 secs] [Times: user=0.10 sys=0.01, real=0.04 secs] 
2022-01-12T15:03:24.516-0800: 48.311: [GC (Allocation Failure) [PSYoungGen: 1308671K->67295K(1257472K)] 1447963K->225652K(1465856K), 0.0381420 secs] [Times: user=0.07 sys=0.02, real=0.04 secs]

指標分析第二步:導入分析工具,盡心分析

打開gceasy.io網站,並選擇本地的gc文件,然後點擊分析。

(分析的速度根據日誌的多少而定,可能會比較慢)

1187916-20220112150654646-1505388261.png

接下來看看分析結果:

JVM memory size (JVM內存大小)

GCEasy是一款非常好用的在線分析GC日誌的工具,打開官網,直接上傳gc日誌,也可以更加上門的要求進行壓縮上傳。
在這裏插入圖片描述

這裏的Allocated和Peak分別表示可分配空間和峯值

  • Allocated:可分配空間大小。

    具體含義如下:指示爲每一代分配的大小。此數據點是從GC日誌收集的,因此它可能與JVM系統屬性指定的大小相匹配,也可能不匹配。假設您已將總堆大小配置爲2gb,而在運行時,如果JVM只分配了1gb,那麼在本報告中,您將看到分配的大小僅爲1gb

  • Peak: 分配的峯值。

    具體含義如下:每一代的峯值內存利用率。通常它不會超過分配的大小。然而,在少數情況下,我們也看到峯值利用率超出了分配的大小,特別是在G1 GC中

JVM memory size ,GCEasy展示了年輕代、老年代、元空間。JVM給分配的大小和程序運行過程中使用的峯值大小。

從JVM memory size展示的信息,我們可以判斷是否需要做下面的幾件事情。

  • 是否需要修改JVM內存(-Xms、-Xmx、-Xmn…)相關配置,比如年輕代和老年代峯值遠遠小於分配的大小,這個時候我們可以適當的減小內存設置。
  • 是否需要調整年輕代和老年代的比例(-XX:NewSize(-Xns)、-XX:MaxNewSize(-Xmn)、-XX:SurvivorRatio=8)。比如老年大的峯值一直小於老年代申請的內存,這個時候我們可以稍微多分點空間給年輕代。
  • 是否需要修改元空間(XX:MetaspaceSize,-XX:MaxMetaspaceSize)相關設置。
    年輕代,老年代屬於堆區,元空間屬於非堆區(直接對接的是機器的內存)

Key Performance Indicatiors(關鍵指標)

img

  • Throughput:吞吐量。

    指的是處理實際事務花費的時間與GC花費的時間的百分比。這個值越高越好

  • Latency:

    延遲情況。這裏的延遲情況是指的GC過程花費的時間。具體含義如上圖

Throughput表示的是吞吐量
Latency表示響應時間
Avg Pause GC Time 平均GC時間
Max Pause GC TIme 最大GC時間

Key Performance Indicators 給我們展示了GC吞吐量(應用程序線程用時佔程序總用時的比例,越高越好),每次GC的平均耗時(建議控制在50ms以下),GC最長耗時,每個時間段的GC次數及佔比信息。

通過Key Performance Indicators顯示的信息裏面,我們需要關注下面幾個問題:

  • 吞吐量,應用花在非GC上的時間百分比(引用花在生產任務上的百分比)。所以吞吐量越高越好。
  • 每次GC的平均耗時。越小越好,建議50ms以下。
  • GC最長耗時。越小越好。如果你的應用是一個後臺程序,並且任何請求不超過10秒,那麼GC最長耗時就不能超過10秒。

Interactive Graphs(交互圖)

Interactive Graphs 展示了

Heap after GC:GC之後堆的使用情況
Heap before GC:GC之前堆的使用情況
GC Duration:GC持續時間
Reclaimed Bytes:GC回收掉的垃圾對象的內存大小
Young Gen:年輕代堆的使用情況
Old Gen:老年代堆的使用情況
Meta Space:元空間的使用情況
A & P:每次GC的時候堆內存分配和晉升情況。其中紅色的線表示每次GC的時候年輕代裏面有多少內存(對象)晉升到了老年代。

第一部分是Heap after GC,GC後堆的內存圖,堆是用來存儲對象的,從圖中可以看出,隨着GC的進行,垃圾回收器把對象都回收掉了,因此堆的大小逐漸增大。
在這裏插入圖片描述
第二部分是Heap before GC,這是GC前堆的使用率,可以看出隨着程序的運行,堆使用率越來越高,堆被對象佔用的內存越來越大。
在這裏插入圖片描述
第三部分是GC Duration Time,就是GC持續時間。一個GC事件的發生具有多個階段,而不同的垃圾回收器又有不同的階段,這裏展示不作細分。這些階段(例如併發標記,併發清除等)與程序線程一起併發運行,此時不會暫停程序線程。但是某些階段(例如初始標記,清除等)會暫停整個應用程序,所以此圖標描述的僅暫停階段所花費的時間。
在這裏插入圖片描述
第四部分表示的是GC回收掉的垃圾對象的內存大小。
在這裏插入圖片描述
第五部分表示的是Young Gen,年輕代的內存分配情況。對象都是朝生夕死,年輕代存放的就是剛剛產生的對象,每進行一次GC,都會GC掉很多垃圾對象,剩下的就是右GC Root關聯的對象,這些對象會年齡會逐漸增加,達到了一定閾值就會晉升爲老年代的對象。可以看到before GC表示的圖線隨着時間的進行逐漸增大,也就是年輕代中對象越來越多,而GC事件發生後,年輕代中對象就會減少,也就是after GC圖線表示的內存變化趨勢。

在這裏插入圖片描述
第六部分是Old Gen,表示的是老年代的內存分配情況。細心的讀者會發現,爲啥一開始before GC的內存大小比after GC的內存分配要少呢?這裏得先知道老年代存放的都是年齡大的對象,意思就是經過了多次GC都沒有被GC掉的對象,就會晉升爲老年代的對象。所以這就解釋了爲啥after GC內存要比before GC內存要大,因爲每次GC過後,都會有年輕代的對象晉升爲老年代對象。

在這裏插入圖片描述
第七部分是每次GC的時候堆內存分配和晉升情況。其中紅色的線表示每次GC的時候年輕代裏面有多少內存(對象)晉升到了老年代。
在這裏插入圖片描述

GC Statistics(GC統計信息)

在這裏插入圖片描述
GC Statistics顯示一些GC的統計信息。

每種GC總共回收了多少內存、總共用了多長時間、平均時間、以及每種GC的單獨統計信息啥的。

Object Stats(對象的一些統計信息)

在這裏插入圖片描述

GC Causes(GC的原因信息)

在這裏插入圖片描述

Memory Leak

由於記錄的程序沒有內存泄漏,所以這裏就沒有內存泄漏的日誌信息。

此處可以診斷8種OOM中的5種(Java堆內存溢出,超出GC開銷限制,請求數組大小超過JVM限制,Permgen空間,元空間)。

四:JVM 常用配置策略

垃圾回收器的選擇

選擇垃圾回收器時,應根據CPU核心數、關注點(吞吐量或用戶停頓時間)以及JDK版本等因素做出合適的選擇,以提高應用程序的性能和穩定性。

  • CPU單核:

    當系統僅有單核CPU時,Serial垃圾收集器是最佳選擇。

    由於單核系統的性能瓶頸主要集中在單一處理器上,使用Serial垃圾收集器能夠簡化垃圾回收的過程,提高系統的整體性能。

  • CPU多核:關注吞吐量

    對於多核CPU且關注系統吞吐量的情況,推薦選擇Parallel Scavenge(PS)加 Parallel Old(PO)的組合。

    這種組合利用了多核CPU的並行處理能力,通過並行處理新生代和老年代的垃圾收集,以提高系統的吞吐量和整體性能。

  • CPU多核,關注用戶停頓時間,JDK版本1.6或1.7:

    如果系統是多核CPU,並且更關注用戶停頓時間,特別是在JDK版本爲1.6或1.7的情況下,推薦選擇Concurrent Mark-Sweep(CMS)垃圾收集器。

    CMS垃圾收集器以減少應用程序停頓時間爲目標,通過與應用程序線程併發執行部分垃圾回收操作,從而降低了GC造成的停頓時間,提高了系統的響應速度和用戶體驗。

  • CPU多核,關注用戶停頓時間,JDK1.8及以上,JVM可用內存6G以上:

    對於JDK版本爲1.8及以上,並且系統具備充足的內存資源(6G及以上),且依然關注用戶停頓時間的情況,推薦選擇Garbage-First(G1)垃圾收集器。

    G1垃圾收集器是一種面向服務端應用的垃圾收集器,具有高效的垃圾回收、可預測的停頓時間和良好的內存整理能力,適用於對用戶停頓時間有較高要求的應用場景。

垃圾回收器的選擇 的切換配置:

 //設置Serial垃圾收集器(新生代)
 開啓:-XX:+UseSerialGC
 
 //設置PS+PO,新生代使用功能Parallel Scavenge 老年代將會使用Parallel Old收集器
 開啓 -XX:+UseParallelOldGC
 
 //CMS垃圾收集器(老年代)
 開啓 -XX:+UseConcMarkSweepGC
 
 //設置G1垃圾收集器
 開啓 -XX:+UseG1GC

JVM參數常用原則

  • 對於JVM堆的設置

    通常我們會使用 -Xms-Xmx 來設定最小和最大堆大小,將它們設置爲相同的值可以防止垃圾收集器在堆大小之間進行收縮,從而減少額外的時間消耗。

  • 年輕代和年老代的大小將根據默認比例(通常爲1:2)分配堆內存。

    我們可以通過調整 -XX:NewRatio 參數來調整它們之間的比例,

    也可以通過 -XX:NewSize-XX:MaxNewSize 來設置年輕代的絕對大小。

    爲了防止年輕代堆大小的調整,通常將 -XX:NewSize-XX:MaxNewSize 設置爲相同大小。

  • 年輕代和年老代大小的合理設置沒有標準答案,因此調優時需要觀察它們大小變化對系統的影響。

    更大的年輕代會延長普通GC週期但增加每次GC的時間,而更小的年老代會導致更頻繁的Full GC。

    選擇應根據應用程序對象生命週期的分佈情況,例如,如果應用存在大量的臨時對象,則應選擇更大的年輕代;如果存在大量的持久對象,則應適當增大年老代。

    觀察應用一段時間後,根據峯值時年老代所佔內存來調整年輕代的大小,但應保留年老代至少1/3的增長空間。

  • 在配置較好的機器上(如多核、大內存),可以爲年老代選擇並行收集算法,使用XX:+UseParallelOldGC,默認爲串行收集。

  • 線程堆棧的設置:每個線程默認會分配1M的堆棧空間,用於存放棧幀、調用參數、局部變量等。

    對於大多數應用來說,這個默認值過大,一般可以將其減小至256K。

    減小線程堆棧大小可以在內存不變的情況下創建更多線程,但這也受限於操作系統的支持。

五:常見調優策略

5.1 調整內存大小

  • 現象:垃圾收集頻率非常頻繁

  • 措施:考慮增加堆內存大小。

  • 說明:

頻繁的垃圾收集通常是由於內存過小,導致需要不斷進行垃圾收集以釋放空間來容納新對象。

因此,增加堆內存大小可以顯著降低垃圾收集的頻率。

需要注意的是,如果垃圾收集次數雖然頻繁但每次回收的對象卻很少,那麼問題可能不在於內存過小,而是由於內存泄漏導致的對象無法被正確回收,從而引發了頻繁的垃圾收集。

在這種情況下,調整內存大小可能無法解決問題,而需要對代碼進行進一步的分析和調試。

 //設置堆初始值
 指令1:-Xms2g
 指令2:-XX:InitialHeapSize=2048m
 
 //設置堆區最大值
 指令1:`-Xmx2g` 
 指令2: -XX:MaxHeapSize=2048m
 
 //新生代內存配置
 指令1:-Xmn512m
 指令2:-XX:MaxNewSize=512m

5.2 調整GC觸發時機

  • 現象:

    在CMS和G1垃圾回收器下,頻繁發生Full GC,導致程序嚴重卡頓。

  • 說明:

在G1和CMS的部分GC階段是併發進行的,即業務線程和垃圾收集線程同時運行。

這意味着在垃圾收集過程中,業務線程可能會生成新的對象。

因此,在進行垃圾收集時,需要預留一部分內存空間來容納新產生的對象。

如果此時內存空間不足以容納新對象,JVM會停止併發收集,暫停所有業務線程(STW),以確保垃圾收集正常進行。

可以通過調整GC的觸發時機(例如在老年代佔用60%時觸發GC)來預留足夠的空間給業務線程創建的對象。

 //使用多少比例的老年代後開始CMS收集,默認是68%,如果頻繁發生SerialOld卡頓,應該調小
 -XX:CMSInitiatingOccupancyFraction
 
 //G1混合垃圾回收週期中要包括的舊區域設置佔用率閾值。默認佔用率爲 65%
 -XX:G1MixedGCLiveThresholdPercent=65 

5.3 調整對象晉升到老年代年齡閾值

  • 現象:

    老年代發生頻繁的GC,每次清理回收大量對象。

  • 說明:

當對象的晉升年齡設定較低時,新生代中的對象很快就會被晉升到老年代。

這導致老年代中對象數量增多,其中很多對象實際上在短時間內就可能被回收。

通過調整對象的晉升年齡,可以減少過早進入老年代的對象數量,從而減少老年代的空間壓力和頻繁的GC。

注意:提高晉升年齡雖然可以減緩老年代的壓力,但同時可能會增加新生代的GC頻率,因爲對象在新生代的停留時間變長。

此外,新生代中頻繁複制這些對象可能會導致新生代的GC時間也相應增長。

在調整晉升年齡時,應綜合考慮新生代和老年代的GC性能,以達到最優的系統性能平衡。

 // 進入老年代最小的GC年齡,年輕代對象轉換爲老年代對象最小年齡值,默認值7
 -XX:InitialTenuringThreshol=7 

5.4 調整大對象進入老年代的標準

  • 現象:

    老年代經常發生頻繁的GC,每次回收大量對象,而這些對象的體積都相對較大。

  • 說明:

大量大對象直接分配到老年代會快速填滿老年代空間,導致老年代頻繁GC。

爲解決此問題,可調整大對象直接進入老年代的標準。

需要注意:將大對象調整爲直接進入老年代後,可能會增加新生代的GC頻率和時間。

 //新生代可容納的最大對象,大於則直接會分配到老年代,0代表沒有限制。
  -XX:PretenureSizeThreshold=1000000 

5.5 調整內存區域大小比率

  • 現象:

    某一內存區域頻繁發生GC,而其他區域的GC表現正常。

  • 說明:

頻繁的GC可能是由於對應區域的空間不足所致,需要不斷進行GC以釋放空間。

在JVM堆內存無法增加的情況下,可以考慮調整對應區域的大小比率。

注意:儘管頻繁的GC可能是由於空間不足造成的,但也有可能是因爲內存泄漏導致內存無法回收,進而引發GC頻繁。

因此,在調整內存區域大小比率之前,需要仔細分析是否存在內存泄漏問題。

 // survivor區和Eden區大小比率
 指令:-XX:SurvivorRatio=6  //S區和Eden區佔新生代比率爲1:6,兩個S區2:6
 
 // 新生代和老年代的佔比
 -XX:NewRatio=4  //表示新生代:老年代 = 1:4 即老年代佔整個堆的4/5;默認值=2

5.6 調整對象晉升至老年代的年齡閾值

  • 現象:

    老年代頻繁進行GC,每次回收大量對象。

  • 說明:

如果對象的晉升年齡較小,新生代中的對象很快就會晉升至老年代,導致老年代中對象數量增多。

然而,這些對象在接下來的短時間內可能會被回收。爲解決老年代空間不足導致的頻繁GC問題,可調整對象晉升至老年代的年齡閾值,使對象不那麼容易晉升至老年代。

注意:增加對象晉升年齡可能會導致新生代中對象的停留時間增加,從而增加新生代的GC頻率,並且複製大對象可能導致新生代GC的時間延長。

在調整晉升年齡時,需綜合考慮新生代和老年代的GC性能,以獲得最優的系統性能平衡。

5.7 調整垃圾回收的觸發時機

  • 現象:

    G1和CMS垃圾收集器在執行垃圾回收時與應用程序的業務線程併發工作。

    在垃圾回收過程中,業務線程可能生成新對象,需預留內存空間以容納這些新產生的對象。

    若內存空間不足,JVM會暫停所有業務線程(STW)以確保垃圾回收正常進行。

  • 說明:

在進行垃圾回收時,若未預留足夠的內存空間供新對象使用,可能導致內存壓力過大,從而觸發STW。

通過調整垃圾回收的觸發時機來預留足夠的內存空間,如可設定在老年代佔用達到一定比例時觸發垃圾回收。

這有助於提前釋放內存空間,爲新對象分配留出足夠的空間,從而減少因內存不足而導致的STW情況。

注意:

提早觸發垃圾回收會增加老年代垃圾回收的頻率,這可能導致一些性能開銷,如額外的CPU使用和系統停頓時間。

因此,在調整垃圾回收的觸發時機時,需要在性能與內存利用率之間找到恰當的平衡。

5.8 設置符合預期的停頓時間

現象:程序間接性的卡頓
原因:如果沒有確切的停頓時間設定,垃圾收集器以吞吐量爲主,那麼垃圾收集時間就會不穩定。
注意:不要設置不切實際的停頓時間,單次時間越短也意味着需要更多的GC次數才能回收完原有數量的垃圾.

參數配置:l

 //GC停頓時間,垃圾收集器會嘗試用各種手段達到這個時間
      -XX:MaxGCPauseMillis 

六: JVM調優案例和實踐

案例1:網站流量增加後,網頁響應速度變慢

問題描述
在測試環境中,網站速度較快,但一到生產環境就顯著變慢。
問題分析

  1. 初步診斷: 通過使用 jstat -gc 指令監控線上JVM的GC活動,發現GC頻率和所佔時間異常高。這表明頻繁的GC正影響業務線程的執行,從而導致頁面響應緩慢。
  2. 內存調整後的問題: 增加JVM的堆內存從2GB到16GB後,雖然常規請求的處理速度提高,但出現了間歇性的更長時間卡頓。進一步監控發現,雖然Full GC(FGC)的次數不多,但每次的持續時間過長,有時達到幾十秒。
  3. 原因推斷: 增加堆內存後,雖然減少了頻繁的垃圾回收,但因爲PS+PO垃圾收集器(Parallel Scavenge + Parallel Old)在垃圾標記和收集階段都需要停止所有工作線程(STW),所以每次GC時業務線程的停頓時間顯著增長。

解決方案

  1. 調整垃圾收集器: 服務不穩定的根本問題是垃圾回收過程中的停頓時間過長,由於默認的PS+PO組合垃圾收集器導致。爲了解決這一問題,可更換爲併發類的收集器,如CMS垃圾收集器。
  2. CMS配置優化: 根據系統運行的實際情況,調整CMS的啓動閾值,預設了合理的停頓時間,以確保不會因爲內存回收而影響用戶的使用體驗。

案例2:CPU飆升和GC頻繁的調優實踐

問題描述:
隨着在線遊戲玩家數量的增加,系統出現CPU飆升和GC頻繁的情況,導致遊戲體驗下降。
問題分析:
使用監控工具檢查系統的CPU使用情況和GC情況,發現系統在高負載情況下CPU佔用過高,且GC頻率過於頻繁。
解決方案:

  1. 代碼優化:進行代碼審查和性能分析,發現並優化存在不必要的循環操作和資源競爭問題,以減少CPU佔用。
  2. 堆內存調整:增加堆內存大小,減少GC的頻率,提高系統的吞吐量和穩定性,確保系統能夠應對增加的玩家數量。
  3. GC算法調優:根據系統負載情況和硬件環境,選擇合適的GC算法,並調整相應的參數,以減少GC造成的性能損耗。例如,針對大堆內存和高併發情況,可以考慮使用並行GC或G1收集器,並根據具體情況調整相關參數以提升性能。

案例3:數據分析平臺系統頻繁 Full GC

問題描述:
數據分析平臺對用戶在App中的行爲進行定時分析統計,但系統頻繁發生Full GC,導致頁面打開卡頓,影響用戶體驗。
問題分析:

  1. CMS GC算法使用:系統使用CMS(Concurrent Mark-Sweep)GC算法,但頻繁的Full GC表明GC調優方面存在問題。
  2. Young GC後存活對象進入老年代:使用jstat命令監控發現,每次Young GC後大約有10%的存活對象進入老年代,這意味着Survivor區空間可能設置過小,導致存活對象在Survivor區放不下而提前進入老年代。

解決方案:

  1. 調整Survivor區大小:增大Survivor區大小,確保其能容納Young GC後的存活對象,使存活對象能在Survivor區經歷多次Young GC達到年齡閾值後才進入老年代。
  2. 優化存活對象進入老年代的大小:調整Survivor區大小後,每次Young GC後進入老年代的存活對象穩定在幾百KB左右,大大降低了Full GC的頻率,提升了系統的穩定性和性能。

案例4:內存飆高問題定位

**問題描述: **
在Java進程中,內存飆高,可能是由於大量對象創建或內存泄漏導致的。

持續的內存飆高可能表明垃圾回收跟不上對象創建速度,或存在內存泄漏導致對象無法回收。
問題分析:

  1. 觀察垃圾回收情況:
    • 使用 jstat -gc PID 1000 命令觀察GC次數、時間等信息,每隔一秒打印一次。
    • 使用 jmap -histo PID | head -20 命令查看堆內存佔用空間最大的前20個對象類型。
    • 如果GC頻率高且每次回收的內存空間正常,可能是對象創建速度過快導致內存佔用高;如果每次回收的內存很少,可能是內存泄漏。
  2. 導出堆內存文件快照:
    • 使用 jmap -dump:live,format=b,file=/home/myheapdump.hprof PID 命令將堆內存信息導出到文件,以進一步分析內存佔用情況。

解決方案:
通過使用VisualVM對dump文件進行離線分析,識別內存佔用較高的對象,並進一步定位到創建這些對象的業務代碼位置,以便從代碼和業務場景中精確定位具體問題。

案例5:Major GC和Minor GC頻繁

這個案例,來自美團技術官網

確定目標

服務情況:Minor GC每分鐘100次 ,Major GC每4分鐘一次,單次Minor GC耗時25ms,單次Major GC耗時200ms,接口響應時間50ms。

由於這個服務要求低延時高可用,結合上文中提到的GC對服務響應時間的影響,計算可知由於Minor GC的發生,12.5%的請求響應時間會增加,其中8.3%的請求響應時間會增加25ms,可見當前GC情況對響應時間影響較大。

(50ms+25ms)× 100次/60000ms = 12.5%,50ms × 100次/60000ms = 8.3%

優化目標:降低TP99、TP90時間。

優化

首先優化Minor GC頻繁問題。通常情況下,由於新生代空間較小,Eden區很快被填滿,就會導致頻繁Minor GC,因此可以通過增大新生代空間來降低Minor GC的頻率。例如在相同的內存分配率的前提下,新生代中的Eden區增加一倍,Minor GC的次數就會減少一半。

這時很多人有這樣的疑問,擴容Eden區雖然可以減少Minor GC的次數,但會增加單次Minor GC時間麼?根據上面公式,如果單次Minor GC時間也增加,很難保證最後的優化效果。

結合下面情況來分析,單次Minor GC時間主要受哪些因素影響?是否和新生代大小存在線性關係?

首先,單次Minor GC時間由以下兩部分組成:T1(掃描新生代)和 T2(複製存活對象到Survivor區)如下圖。(注:這裏爲了簡化問題,我們認爲T1只掃描新生代判斷對象是否存活的時間,其實該階段還需要掃描部分老年代,後面案例中有詳細描述。)

img

  • 擴容前:新生代容量爲R ,假設對象A的存活時間爲750ms,Minor GC間隔500ms,那麼本次Minor GC時間= T1(掃描新生代R)+T2(複製對象A到S)。
  • 擴容後:新生代容量爲2R ,對象A的生命週期爲750ms,那麼Minor GC間隔增加爲1000ms,此時Minor GC對象A已不再存活,不需要把它複製到Survivor區,那麼本次GC時間 = 2 × T1(掃描新生代R),沒有T2複製時間。

可見,擴容後,Minor GC時增加了T1(掃描時間),但省去T2(複製對象)的時間,更重要的是對於虛擬機來說,複製對象的成本要遠高於掃描成本,所以,單次Minor GC時間更多取決於GC後存活對象的數量,而非Eden區的大小。因此如果堆中短期對象很多,那麼擴容新生代,單次Minor GC時間不會顯著增加。下面需要確認下服務中對象的生命週期分佈情況:

img

通過上圖GC日誌中兩處紅色框標記內容可知:

  1. new threshold = 2(動態年齡判斷,對象的晉升年齡閾值爲2),對象僅經歷2次Minor GC後就晉升到老年代,這樣老年代會迅速被填滿,直接導致了頻繁的Major GC。

  2. Major GC後老年代使用空間爲300M+,意味着此時絕大多數(86% = 2G/2.3G)的對象已經不再存活,也就是說生命週期長的對象佔比很小。

由此可見,服務中存在大量短期臨時對象,擴容新生代空間後,Minor GC頻率降低,對象在新生代得到充分回收,只有生命週期長的對象才進入老年代。這樣老年代增速變慢,Major GC頻率自然也會降低。

優化結果

通過擴容新生代爲爲原來的三倍,單次Minor GC時間增加小於5ms,頻率下降了60%,服務響應時間TP90,TP99都下降了10ms+,服務可用性得到提升。

調整前:img

調整後:img

小結

如何選擇各分區大小應該依賴應用程序中對象生命週期的分佈情況:如果應用存在大量的短期對象,應該選擇較大的年輕代;如果存在相對較多的持久對象,老年代應該適當增大。

更多思考

關於上文中提到晉升年齡閾值爲2,很多同學有疑問,爲什麼設置了MaxTenuringThreshold=15,對象仍然僅經歷2次Minor GC,就晉升到老年代?這裏涉及到“動態年齡計算”的概念。

動態年齡計算

Hotspot遍歷所有對象時,按照年齡從小到大對其所佔用的大小進行累積,當累積的某個年齡大小超過了survivor區的一半時,取這個年齡和MaxTenuringThreshold中更小的一個值,作爲新的晉升年齡閾值。在本案例中,調優前:Survivor區 = 64M,desired survivor = 32M,此時Survivor區中age<=2的對象累計大小爲41M,41M大於32M,所以晉升年齡閾值被設置爲2,下次Minor GC時將年齡超過2的對象被晉升到老年代。

JVM引入動態年齡計算,主要基於如下兩點考慮:

  1. 如果固定按照MaxTenuringThreshold設定的閾值作爲晉升條件:

    a)MaxTenuringThreshold設置的過大,原本應該晉升的對象一直停留在Survivor區,直到Survivor區溢出,一旦溢出發生,Eden+Svuvivor中對象將不再依據年齡全部提升到老年代,這樣對象老化的機制就失效了。

    b)MaxTenuringThreshold設置的過小,“過早晉升”即對象不能在新生代充分被回收,大量短期對象被晉升到老年代,老年代空間迅速增長,引起頻繁的Major GC。分代回收失去了意義,嚴重影響GC性能。

  2. 相同應用在不同時間的表現不同:特殊任務的執行或者流量成分的變化,都會導致對象的生命週期分佈發生波動,那麼固定的閾值設定,因爲無法動態適應變化,會造成和上面相同的問題。

總結來說,爲了更好的適應不同程序的內存情況,虛擬機並不總是要求對象年齡必須達到Maxtenuringthreshhold再晉級老年代。

美團案例6: 請求高峯期發生GC,導致服務可用性下降

這個案例,來自美團技術官網

確定目標

GC日誌顯示,高峯期CMS在重標記(Remark)階段耗時1.39s。

Remark階段是Stop-The-World(以下簡稱爲STW)的,即在執行垃圾回收時,Java應用程序中除了垃圾回收器線程之外其他所有線程都被掛起,意味着在此期間,用戶正常工作的線程全部被暫停下來,這是低延時服務不能接受的。本次優化目標是降低Remark時間。

img

優化

解決問題前,先回顧一下CMS的四個主要階段,以及各個階段的工作內容。下圖展示了CMS各個階段可以標記的對象,用不同顏色區分。

  1. Init-mark初始標記(STW) ,該階段進行可達性分析,標記GC ROOT能直接關聯到的對象,所以很快。

  2. Concurrent-mark併發標記,由前階段標記過的綠色對象出發,所有可到達的對象都在本階段中標記。

  3. Remark重標記(STW) ,暫停所有用戶線程,重新掃描堆中的對象,進行可達性分析,標記活着的對象。因爲併發標記階段是和用戶線程併發執行的過程,所以該過程中可能有用戶線程修改某些活躍對象的字段,指向了一個未標記過的對象,如下圖中紅色對象在併發標記開始時不可達,但是並行期間引用發生變化,變爲對象可達,這個階段需要重新標記出此類對象,防止在下一階段被清理掉,這個過程也是需要STW的。特別需要注意一點,這個階段是以新生代中對象爲根來判斷對象是否存活的。

  4. 併發清理,進行併發的垃圾清理。

img

可見,Remark階段主要是通過掃描堆來判斷對象是否存活。那麼準確判斷對象是否存活,需要掃描哪些對象?CMS對老年代做回收,Remark階段僅掃描老年代是否可行?結論是不可行,原因如下:img

如果僅掃描老年代中對象,即以老年代中對象爲根,判斷對象是否存在引用,上圖中,對象A因爲引用存在新生代中,它在Remark階段就不會被修正標記爲可達,GC時會被錯誤回收。

新生代對象持有老年代中對象的引用,這種情況稱爲“跨代引用”。因它的存在,Remark階段必須掃描整個堆來判斷對象是否存活,包括圖中灰色的不可達對象。

灰色對象已經不可達,但仍然需要掃描的原因:新生代GC和老年代的GC是各自分開獨立進行的,只有Minor GC時纔會使用根搜索算法,標記新生代對象是否可達,也就是說雖然一些對象已經不可達,但在Minor GC發生前不會被標記爲不可達,CMS也無法辨認哪些對象存活,只能全堆掃描(新生代+老年代)。

由此可見堆中對象的數目影響了Remark階段耗時。 分析GC日誌可以得出同樣的規律,Remark耗時>500ms時,新生代使用率都在75%以上。這樣降低Remark階段耗時問題轉換成如何減少新生代對象數量。

新生代中對象的特點是“朝生夕滅”,這樣如果Remark前執行一次Minor GC,大部分對象就會被回收。

CMS就採用了這樣的方式,在Remark前增加了一個可中斷的併發預清理(CMS-concurrent-abortable-preclean),該階段主要工作仍然是併發標記對象是否存活,只是這個過程可被中斷。

此階段在Eden區使用超過2M時啓動,當然2M是默認的閾值,可以通過參數修改。如果此階段執行時等到了Minor GC,那麼上述灰色對象將被回收,Reamark階段需要掃描的對象就少了。

除此之外CMS爲了避免這個階段沒有等到Minor GC而陷入無限等待,提供了參數CMSMaxAbortablePrecleanTime ,默認爲5s,含義是如果可中斷的預清理執行超過5s,不管發沒發生Minor GC,都會中止此階段,進入Remark。

根據GC日誌紅色標記2處顯示,可中斷的併發預清理執行了5.35s,超過了設置的5s被中斷,期間沒有等到Minor GC ,所以Remark時新生代中仍然有很多對象。

對於這種情況,CMS提供CMSScavengeBeforeRemark參數,用來保證Remark前強制進行一次Minor GC。

優化結果

經過增加CMSScavengeBeforeRemark參數,單次執行時間>200ms的GC停頓消失,從監控上觀察,GCtime和業務波動保持一致,不再有明顯的毛刺。img

小結

通過案例分析瞭解到,由於跨代引用的存在,CMS在Remark階段必須掃描整個堆,同時爲了避免掃描時新生代有很多對象,增加了可中斷的預清理階段用來等待Minor GC的發生。只是該階段有時間限制,如果超時等不到Minor GC,Remark時新生代仍然有很多對象,我們的調優策略是,通過參數強制Remark前進行一次Minor GC,從而降低Remark階段的時間。

更多思考

案例中只涉及老年代GC,其實新生代GC存在同樣的問題,即老年代可能持有新生代對象引用,所以Minor GC時也必須掃描老年代。

JVM是如何避免Minor GC時掃描全堆的? 經過統計信息顯示,老年代持有新生代對象引用的情況不足1%,根據這一特性JVM引入了卡表(card table)來實現這一目的。如下圖所示:

img

卡表的具體策略是將老年代的空間分成大小爲512B的若干張卡(card)。卡表本身是單字節數組,數組中的每個元素對應着一張卡,當發生老年代引用新生代時,虛擬機將該卡對應的卡表元素設置爲適當的值。如上圖所示,卡表3被標記爲髒(卡表還有另外的作用,標識併發標記階段哪些塊被修改過),之後Minor GC時通過掃描卡表就可以很快的識別哪些卡中存在老年代指向新生代的引用。這樣虛擬機通過空間換時間的方式,避免了全堆掃描。

總結來說,CMS的設計聚焦在獲取最短的時延,爲此它“不遺餘力”地做了很多工作,包括儘量讓應用程序和GC線程併發、增加可中斷的併發預清理階段、引入卡表等,雖然這些操作犧牲了一定吞吐量但獲得了更短的回收停頓時間。

美團案例7:發生Stop-The-World的GC

這個案例,來自美團技術官網

確定目標

GC日誌如下圖(在GC日誌中,Full GC是用來說明這次垃圾回收的停頓類型,代表STW類型的GC,並不特指老年代GC),根據GC日誌可知本次Full GC耗時1.23s。這個在線服務同樣要求低時延高可用。

本次優化目標是降低單次STW回收停頓時間,提高可用性。

img

優化

首先,什麼時候可能會觸發STW的Full GC呢?

  1. Perm空間不足;
  2. CMS GC時出現promotion failed和concurrent mode failure(concurrent mode failure發生的原因一般是CMS正在進行,但是由於老年代空間不足,需要儘快回收老年代裏面的不再被使用的對象,這時停止所有的線程,同時終止CMS,直接進行Serial Old GC);
  3. 統計得到的Young GC晉升到老年代的平均大小大於老年代的剩餘空間;
  4. 主動觸發Full GC(執行jmap -histo:live [pid])來避免碎片問題。

然後,我們來逐一分析一下:

  • 排除原因2:如果是原因2中兩種情況,日誌中會有特殊標識,目前沒有。

  • 排除原因3:根據GC日誌,當時老年代使用量僅爲20%,也不存在大於2G的大對象產生。

  • 排除原因4:因爲當時沒有相關命令執行。

  • 鎖定原因1:根據日誌發現Full GC後,Perm區變大了,推斷是由於永久代空間不足容量擴展導致的。

找到原因後解決方法有兩種:

  1. 通過把-XX:PermSize參數和-XX:MaxPermSize設置成一樣,強制虛擬機在啓動的時候就把永久代的容量固定下來,避免運行時自動擴容。
  2. CMS默認情況下不會回收Perm區,通過參數CMSPermGenSweepingEnabled、CMSClassUnloadingEnabled ,可以讓CMS在Perm區容量不足時對其回收。

由於該服務沒有生成大量動態類,回收Perm區收益不大,所以我們採用方案1,啓動時將Perm區大小固定,避免進行動態擴容。

優化結果

調整參數後,服務不再有Perm區擴容導致的STW GC發生。

小結

對於性能要求很高的服務,建議將MaxPermSize和MinPermSize設置成一致(JDK8開始,Perm區完全消失,轉而使用元空間。而元空間是直接存在內存中,不在JVM中),Xms和Xmx也設置爲相同,這樣可以減少內存自動擴容和收縮帶來的性能損失。虛擬機啓動的時候就會把參數中所設定的內存全部化爲私有,即使擴容前有一部分內存不會被用戶代碼用到,這部分內存在虛擬機中被標識爲虛擬內存,也不會交給其他進程使用。

八:JVM調優常見面試題的精簡答案

8. 1、調優包括哪些維度?

​ 架構調優、代碼調優、JVM調優、數據庫調優、操作系統調優等

​ 架構調優和代碼調優是JVM調優的基礎,其中架構調優是對系統影響最大的

8.2、何時進行JVM調優

  • Heap內存(老年代)持續上漲達到設置的最大內存值;
  • Full GC 次數頻繁;
  • GC 停頓時間過長(超過1秒);
  • 應用出現OutOfMemory等內存異常;
  • 應用中有使用本地緩存且佔用大量內存空間;
  • 系統吞吐量與響應性能不高或不降;

8.3、JVM調優的基本原則

  • 大多數的Java應用不需要進行JVM優化;
  • 大多數導致GC問題的原因是代碼層面的問題導致的(代碼層面);
  • 上線之前,應先考慮將機器的JVM參數設置到最優;
  • 減少創建對象的數量(代碼層面);
  • 減少使用全局變量和大對象(代碼層面);
  • 優先架構調優和代碼調優,JVM優化是不得已的手段(代碼、架構層面);
  • 分析GC情況優化代碼比優化JVM參數更好(代碼層面)

其實最有效的優化手段是架構和代碼層面的優化,而JVM優化則是最後不得已的手段,也可以說是對服務器配置的最後一次“壓榨”

8.4、JVM調優目標

目的都是爲了令應用程序使用最小的硬件消耗來承載更大的吞吐。JVM調優主要是針對垃圾收集器的收集性能優化,令運行在虛擬機上的應用能夠使用更少的內存以及延遲獲取更大的吞吐量,總結以下:

  • 延遲:GC低停頓和GC低頻率;
  • 低內存佔用;
  • 高吞吐量。

8.5、JVM調優量化目標

  • Heap 內存使用率 <= 70%;
  • Old generation 內存使用率 <= 70%;
  • avgpause <= 1秒;
  • Full GC 次數 0 或 avg pause interval >= 24小時。

8.6、JVM調優的步驟

  • 分析GC日誌及dump文件,判斷是否需要優化,確定瓶頸問題點;
  • 確定JVM調優量化目標;
  • 確定JVM調優參數(根據歷史JVM參數來調整);
  • 依次調優內存、延遲、吞吐量等指標;
  • 對比觀察調優前後的差異;
  • 不斷的分析和調整,直到找到合適的JVM參數配置;
  • 找到最合適的參數,將這些參數應用到所有服務器,並進行後續跟蹤。

8.7、VM參數解析及調優

-Xmx4g 
–Xms4g 
–Xmn1200m 
–Xss512k 
-XX:NewRatio=4 
-XX:SurvivorRatio=8 
-XX:PermSize=100m 
-XX:MaxPermSize=256m 
-XX:MaxTenuringThreshold=15
  • -Xmx4g:堆內存最大值爲4GB。
  • -Xms4g:初始化堆內存大小爲4GB。
  • -Xmn1200m:設置年輕代大小爲1200MB。增大年輕代後,將會減小年老代大小。此值對系統性能影響較大,Sun官方推薦配置爲整個堆的3/8。
  • -Xss512k:設置每個線程的堆棧大小。JDK5.0以後每個線程堆棧大小爲1MB,以前每個線程堆棧大小爲256K。應根據應用線程所需內存大小進行調整。在相同物理內存下,減小這個值能生成更多的線程。但是操作系統對一個進程內的線程數還是有限制的,不能無限生成,經驗值在3000~5000左右。
  • -XX:NewRatio=4:設置年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代)。設置爲4,則年輕代與年老代所佔比值爲1:4,年輕代佔整個堆棧的1/5
  • -XX:SurvivorRatio=8:設置年輕代中Eden區與Survivor區的大小比值。設置爲8,則兩個Survivor區與一個Eden區的比值爲2:8,一個Survivor區佔整個年輕代的1/10
  • -XX:PermSize=100m:初始化永久代大小爲100MB。
  • -XX:MaxPermSize=256m:設置持久代大小爲256MB。
  • -XX:MaxTenuringThreshold=15:設置垃圾最大年齡。如果設置爲0的話,則年輕代對象不經過Survivor區,直接進入年老代。對於年老代比較多的應用,可以提高效率。如果將此值設置爲一個較大值,則年輕代對象會在Survivor區進行多次複製,這樣可以增加對象再年輕代的存活時間,增加在年輕代即被回收的概論。

可調優參數:

  • -Xms:初始化堆內存大小,默認爲物理內存的1/64(小於1GB)。
  • -Xmx:堆內存最大值。默認(MaxHeapFreeRatio參數可以調整)空餘堆內存大於70%時,JVM會減少堆直到-Xms的最小限制。
  • -Xmn:新生代大小,包括Eden區與2個Survivor區。
  • -XX:SurvivorRatio=1:Eden區與一個Survivor區比值爲1:1。
  • -XX:MaxDirectMemorySize=1G:直接內存。報java.lang.OutOfMemoryError: Direct buffer memory異常可以上調這個值。
  • -XX:+DisableExplicitGC:禁止運行期顯式地調用System.gc()來觸發fulll GC。
  • 注意: Java RMI的定時GC觸發機制可通過配置-Dsun.rmi.dgc.server.gcInterval=86400來控制觸發的時間。
  • -XX:CMSInitiatingOccupancyFraction=60:老年代內存回收閾值,默認值爲68。
  • -XX:ConcGCThreads=4:CMS垃圾回收器並行線程線,推薦值爲CPU核心數。
  • -XX:ParallelGCThreads=8:新生代並行收集器的線程數。
  • -XX:MaxTenuringThreshold=10:設置垃圾最大年齡。如果設置爲0的話,則年輕代對象不經過Survivor區,直接進入年老代。對於年老代比較多的應用,可以提高效率。如果將此值設置爲一個較大值,則年輕代對象會在Survivor區進行多次複製,這樣可以增加對象再年輕代的存活時間,增加在年輕代即被回收的概論。
  • -XX:CMSFullGCsBeforeCompaction=4:指定進行多少次fullGC之後,進行tenured區 內存空間壓縮。
  • -XX:CMSMaxAbortablePrecleanTime=500:當abortable-preclean預清理階段執行達到這個時間時就會結束。

8.8、內存調優示例

-XX:+PrintGC   輸出GC日誌
-XX:+PrintGCDetails 輸出GC的詳細日誌
-XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準時間的形式)
-XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC   在進行GC的前後打印出堆的信息
-Xloggc:../logs/gc.log 日誌文件的輸出路徑

image

  • java heap:參數-Xms和-Xmx,建議擴大至3-4倍FullGC後的老年代空間佔用。

  • 永久代:-XX:PermSize和-XX:MaxPermSize,建議擴大至1.2-1.5倍FullGc後的永久代空間佔用。

  • 新生代:-Xmn,建議擴大至1-1.5倍FullGC之後的老年代空間佔用。

  • 老年代:2-3倍FullGC後的老年代空間佔用。

    -Xms373m -Xmx373m //4*93=372
    -Xmn140m //1.5*93=139.5
    -XX:PermSize=5m -XX:MaxPermSize=5m //1.5*3=4.5
    

九 、結語

在JVM調優中,關鍵在於準確識別系統的性能瓶頸和優化方向,選擇適合的調優策略和參數。

實施調優方案後,必須驗證效果,並持續監控系統性能,及時調整優化策略和參數以保持系統高性能和穩定性。

同時,需要及時發現和解決各種潛在的性能問題,如內存泄漏、CPU飆升、頻繁的垃圾回收等,以確保系統在高負載和複雜環境下能夠保持卓越的性能表現。

總之,JVM調優是一個持續改進的過程,通過對系統性能的深入分析和優化,確保Java應用程序在各種情況下都能夠保持高效穩定的運行狀態。

隨着硬件技術的迅速發展,JVM調優也將面臨新的挑戰和機遇。新一代的處理器、存儲技術以及分佈式系統架構等將對JVM調優提出更高的要求,需要更智能、更高效的優化方案來適應日益複雜的應用場景和巨大的數據處理需求。

未來,JVM調優將持續創新和進步,以滿足不斷變化的業務需求和技術挑戰,爲Java應用程序提供更穩定、更高效的運行環境,推動Java生態系統的蓬勃發展和壯大。

與開篇所述保持一致,我們強調在JVM調優中,真正的參數調整是較少的,更多的是通過分析日誌和結合系統業務進行代碼層面的優化。

這可能是調優工作中佔據更大比重的內容。我們不應迷失方向,只爲了調優而調優,只爲了調整參數而調整參數。最終,我們需要回歸到業務本質,這纔是最核心的內容。我們也需要更深入地瞭解JVM的相關參數,以更好地支撐業務需求的實現。

說在最後:有問題找老架構取經

JVM 調優方法論、JVM調優 相關的面試題,是非常常見的面試題。也是核心面試題。

以上的內容,如果大家能對答如流,如數家珍,基本上 面試官會被你 震驚到、吸引到。

最終,讓面試官愛到 “不能自已、口水直流”。offer, 也就來了。

在面試之前,建議大家系統化的刷一波 5000頁《尼恩Java面試寶典》V174,在刷題過程中,如果有啥問題,大家可以來 找 40歲老架構師尼恩交流。

另外,如果沒有面試機會,可以找尼恩來幫扶、領路。

  • 大齡男的最佳出路是 架構+ 管理
  • 大齡女的最佳出路是 DPM,

圖片

女程序員如何成爲DPM,請參見:

DPM (雙棲)陪跑,助力小白一步登天,升格 產品經理+研發經理

領跑模式,尼恩已經指導了大量的就業困難的小夥伴上岸。

前段時間,領跑一個40歲+就業困難小夥伴拿到了一個年薪100W的offer,小夥伴實現了 逆天改命

技術自由的實現路徑:

實現你的 架構自由:

喫透8圖1模板,人人可以做架構

10Wqps評論中臺,如何架構?B站是這麼做的!!!

阿里二面:千萬級、億級數據,如何性能優化? 教科書級 答案來了

峯值21WQps、億級DAU,小遊戲《羊了個羊》是怎麼架構的?

100億級訂單怎麼調度,來一個大廠的極品方案

2個大廠 100億級 超大流量 紅包 架構方案

… 更多架構文章,正在添加中

實現你的 響應式 自由:

響應式聖經:10W字,實現Spring響應式編程自由

這是老版本 《Flux、Mono、Reactor 實戰(史上最全)

實現你的 spring cloud 自由:

Spring cloud Alibaba 學習聖經》 PDF

分庫分表 Sharding-JDBC 底層原理、核心實戰(史上最全)

一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之間混亂關係(史上最全)

實現你的 linux 自由:

Linux命令大全:2W多字,一次實現Linux自由

實現你的 網絡 自由:

TCP協議詳解 (史上最全)

網絡三張表:ARP表, MAC表, 路由表,實現你的網絡自由!!

實現你的 分佈式鎖 自由:

Redis分佈式鎖(圖解 - 秒懂 - 史上最全)

Zookeeper 分佈式鎖 - 圖解 - 秒懂

實現你的 王者組件 自由:

隊列之王: Disruptor 原理、架構、源碼 一文穿透

緩存之王:Caffeine 源碼、架構、原理(史上最全,10W字 超級長文)

緩存之王:Caffeine 的使用(史上最全)

Java Agent 探針、字節碼增強 ByteBuddy(史上最全)

實現你的 面試題 自由:

4800頁《尼恩Java面試寶典 》 40個專題

免費獲取11個技術聖經PDF:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章