【JAVA】JVM 調優

【JAVA】JVM 調優

翻譯文章:JVM Tuning: How to Prepare Your Environment for Performance Tuning

當涉及到Java應用程序時,要確保它們以最高性能運行,至關重要的是縮小代碼與運行它的虛擬機之間的資源差距(如果有的話)。做到這一點的方法是深入並微調Java虛擬機

一、介紹

1. 什麼是JVM調優?

  • 成本 –考慮到您的環境,您可以通過添加硬件而不是花費時間來調整JVM來獲得更多收益
  • 所需的結果 –從長遠來看,調整穩定性比提高性能更有效
  • 持續存在的問題 –在開始調整之前,您應該對系統進行徹底檢查,以查看是否存在潛在的問題,因爲調整可能會暫時延遲或隱藏它。如果不監視JVM,則無法進行JVM調優或調試。找出要監視的關鍵JVM性能指標是什麼,以及哪些是當今可用的最佳監視工具
  • 內存泄漏 –無論如何調整,它們始終會導致垃圾回收問題

2. 應用優化考慮了所有性能層

儘管很關鍵,但是調整JVM不足以確保最佳性能。例如,如果應用程序的架構設計不佳或代碼編寫不佳,那麼僅通過調整JVM就無法期望性能飛速增長

做得很好的調優將研究整個系統以及可能影響性能的所有層,包括數據庫和操作系統。就是說,當您處於執行JVM調整的階段時,請假定項目的體系結構和代碼是最佳的或已調整。但是,在深入研究它之前,您必須設置性能優化目標並確定當前的性能問題。這些目標將作爲基準,以便在優化後將其與應用進行比較,並確定是否需要進一步的干預

二、背景知識

1. JVM主要參數

JVM參數是特定於Java的值,這些值會更改Java虛擬機的行爲

a. 堆內存

就JVM性能而言,一般都需要初始化堆內存

# 用於指定最小和最大堆大小的參數,unit 是初始化內存的單位, 可以是g (GB), m(MB), or k(KB)
-Xms<heap size>[unit]
-Xmx<heap size>[unit]

在設置JVM內存的最小和最大堆大小時,您可能需要考慮將它們設置爲相同的值。這樣,您就不必調整堆大小,從而節省了寶貴的CPU週期。如果您正在使用較大的堆,則可能還需要預觸摸所有頁面方法是將-XX:+AlwaysPreTouch 設置爲啓動項

b. 元空間

從Java 8開始,Metaspace已取代了舊的PermGen內存空間。不再有java.lang.OutOfMemoryError:PermGen錯誤,現在我們可以開始監視應用程序日誌中的java.lang.OutOfMemoryError: Metadata space 此內存區域中的大量垃圾回收工作可能表明類或類加載器中的內存泄漏

默認情況下,元數據分配受可用本機內存量的限制,但JVM公開的以下屬性使我們可以控制元空間:

# 設置可分配給元空間的最大本機內存量, 默認情況下爲無限制
-XX:MaxMetaspaceSize
# 垃圾蒐集器內部是根據變量_capacity_until_GC來判斷metaspace區域是否達到閾值的
# 該參數用於設置首次使用不夠而觸發FGC的閾值, GC收集器會對metaspace的回收, 同時計算新的_capacity_until_GC值
# 以後發生FGC就跟MetaspaceSize沒有關係了
-XX:MetaspaceSize
# 垃圾回收後需要可用的元空間內存區域的最小百分比。 如果剩餘的內存量低於閾值,則將調整元空間區域的大小
-XX:MinMetaspaceFreeRatio
# 垃圾回收後需要可用的元空間內存區域的最大百分比。 如果剩餘的內存量大於閾值,則將調整元空間區域的大小
-XX:MaxMetaspaceFreeRatio

2. 垃圾收集器

我們將在JDK中使用默認的parallel垃圾收集器(或throughput收集器), 可以通過XX:+UseParallelGC開啓。 這個標誌啓用了新生代和老年代收集器的並行版本。也可以通過XX:+UseSerialGC 來進行串行垃圾收集器。串行收集器是單線程收集器,而並行收集器是多線程收集器

通過使用-XX:+PrintCommandLineFlags -version 檢查Java版本的默認值

三、內存分配

深入研究,重要的是要注意內存分配參數

# 設置新生代空間的初始大小
-XX:NewSize
# 設置新生代空間的最大大小
-XXMaxNewSize
# 指定整個年輕代空間的大小,即eden和兩個survivor空間
-Xmn

您將使用以下參數來計算有關老年代的空間大小:根據新生代空間的大小自動設置老年代的大小

# 初始的老年代空間等於
(-Xmx) - (-XX:NewSize)
# 老年代的最小大小爲
(-Xmx) - (-XXMaxNewSize)

1. 內存不足

OutOfMemoryError可能是每個開發者的噩夢。 可能面臨着難以複製和診斷的應用崩潰。 不幸的是,在大型應用程序中這種情況很常見。 幸運的是,JVM具有將堆內存寫入文件的參數,您可以使用該參數進行故障排除

# 在拋出java.lang.OutOfMemoryError時命令JVM將堆轉儲到物理文件中
-XX:+HeapDumpOnOutOfMemoryError
# 指定目錄或文件名的路徑
-XX:HeapDumpPath=./java_pid<pid>.hprof
# 第一次發生OutOfMemoryError時,用於運行緊急用戶定義的命令
-XX:OnOutOfMemoryError="<cmd args>;<cmd args>"
# 用於限制在拋出OutOfMemoryError之前,VM在GC中花費的時間比例
-XX:+UseGCOverheadLimit

四、垃圾收集

JVM具有四個垃圾收集器實現:

-XX:+UseSerialGC			# 串行垃圾收集器
-XX:+UseParallelGC			# 並行垃圾收集器
-XX:+UseConcMarkSweepGC		# CMS垃圾收集器
-XX:+UseG1GC				# G1垃圾收集器

1.GC日誌分析

垃圾收集性能與JVM和應用程序性能密切相關。 當垃圾收集器無法清除內存時,它會越來越多地工作,最終導致stop-the-world事件, 甚至出現內存不足的情況。 我們希望儘可能避免這種情況。 爲了做到這一點,我們需要能夠觀察JVM垃圾收集器在做什麼。 監視GC性能的最佳方法之一是查看GC日誌。 您可以使用以下命令記錄GC活動:

-XX:+UseGCLogFileRotation						# 指定日誌文件輪換策略
-XX:NumberOfGCLogFiles=<number of log files>	# 說明輪換日誌時要使用的最大日誌文件數
-XX:GCLogFileSize=<file size>[unit]				# 指日誌文件的最大大小
-Xloggc:/path/to/gc.log							# 指定文件路徑

對於GC日誌記錄,還有其他重要的JVM參數。 例如:

-XX:+PrintGC 				# 輸出GC日誌
-XX:+PrintGCDetails 		# 輸出GC的詳細日誌
-XX:+PrintGCTimeStamps 		# 輸出GC的時間戳(以基準時間的形式)
-XX:+PrintGCDateStamps 		# 輸出GC的時間戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 			# 在進行GC的前後打印出堆的信息
-XX:+PrintTenuringDistribution		# 在日誌中添加有關對象年齡信息
-XX:+PrintGCApplicationStoppedTime	# 包含有關應用程序在安全點停止的時間的信息,通常是由於stop the world垃圾收集導致的

示例: 使用以下命令行打開完整GC日誌

-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:<filename>

但是,如果沒有可用的完整GC日誌,則可以使用監視工具來調用它們,或者使用以下命令來啓用它們:

jmap -histo:live pid

無論哪種方式,您都會獲得類似於以下內容的信息:我們可以看到由JVM執行的一些操作, 單個垃圾收集器日誌行可以使我們深入瞭解發生了什麼,釋放了多少內存以及整個操作花費了多長時間

0.134: [GC (Allocation Failure) [PSYoungGen: 65536K->10720K(76288K)] 65536K->40488K(251392K), 0.0190287 secs] [Times: user=0.13 sys=0.04, real=0.02 secs]
0.193: [GC (Allocation Failure) [PSYoungGen: 71912K->10752K(141824K)] 101680K->101012K(316928K), 0.0357512 secs] [Times: user=0.27 sys=0.06, real=0.04 secs]
1.235: [Full GC (System.gc()) [PSYoungGen: 10752K->0K(190464K)] [ParOldGen: 358209K->368152K(459264K)] 368961K->368152K(649728K), [Metaspace: 2652K->2652K(1056768K)], 1.1751101 secs] [Times: user=10.64 sys=0.05, real=1.18 secs]
2.612: [Full GC (Ergonomics) [PSYoungGen: 179712K->0K(190464K)] [ParOldGen: 368152K->166769K(477184K)] 547864K->166769K(667648K), [Metaspace: 2659K->2659K(1056768K)], 0.2662589 secs] [Times: user=2.14 sys=0.00, real=0.27 secs]

讓我們看一下其中的一行,該行描述了使用System.gc()方法從測試代碼中有意執行的Full GC事件.

1.235: [Full GC (System.gc()) [PSYoungGen: 10752K->0K(190464K)] [ParOldGen: 358209K->368152K(459264K)] 368961K->368152K(649728K), [Metaspace: 2652K->2652K(1056768K)], 1.1751101 secs] [Times: user=10.64 sys=0.05, real=1.18 secs]

**分析:**除了事件類型之外,我們還看到了新生代中發生的事情,老年代中發生的事情,元空間區域中,,最後是

  • 新生代垃圾收集器: 在新生代GC之後,[PSYoungGen:10752K-> 0K(190464K)],空間從10752K減少到0K,分配的新生代空間總數爲190464K
  • 老年代垃圾收集器:[ParOldGen:358209K-> 368152K(459264K)],其內存使用量爲358209K,而回收完成時爲368152K。分配給老年代的總內存爲459264K。這意味着這個GC週期最終並沒有釋放太多的老年代空間
  • 元空間區域: [Metaspace:2652K-> 2652K (1056768K)],以相同的內存使用量2652K開始和結束,整個區域佔用1056768K
  • 總內存差異:368961K-> 368152K (649728K),在完成整個垃圾收集之後,我們從最初的368961K開始獲得了368152K的內存,並且佔用的整個內存空間爲649728K
  • 整個操作花費的時間:[Times: user=10.64 sys=0.05, real=1.18 secs],JVM完成整個垃圾收集操作所需的時間爲1.18秒。 user = 10.64告訴我們在操作系統內核之外的用戶模式代碼中花費的CPU時間。 sys = 0.05部分是進程本身在內核內部花費的CPU時間,這意味着CPU花在執行與系統相關的調用上的時間

五、性能目標

設定您的JVM性能目標,開始調整JVM的性能之前,首先必須設置性能目標

  • 延遲: 運行垃圾回收事件所需的時間
  • 吞吐量:VM花在執行應用程序上的時間與花在執行垃圾收集上的時間的百分比
  • 佔用空間:是垃圾收集器正常運行所需的內存量

您不能一次專注於所有三個目標,因爲任何三個目標的任何性能提升都會導致另一個或兩個目標的性能下降

  • 高吞吐量和低延遲導致更高的內存使用率
  • 高吞吐量和低內存使用量會導致更高的延遲
  • 低延遲和低內存使用率會導致較低的吞吐量

在考慮業務需求時,您必須決定哪兩個與您的應用程序最相關。 無論哪種方式,JVM調整的目標都是優化垃圾收集器,以使您擁有高吞吐量,更少的內存消耗和低延遲。 但是,較少的內存/低延遲並不意味着內存或延遲越少或越低,性能就越好。 這取決於您選擇關注的指標

1. 調優原則

執行性能調整時,請牢記以下原則,因爲它們使垃圾收集更加容易

  • Minor GC collection: 意味着MinorGC應該收集儘可能多的死對象以減少Full GC的頻率
  • GC內存最大化: 它表示GC在一個週期內可以訪問的內存越多,清理效率越高,收集頻率越低
  • 2/3: 您需要從三個績效目標中選擇兩個

首先,您需要記住的是Java VM調整不能解決所有性能問題。因此,應僅在必要時進行。也就是說,調整是一個漫長的過程,在此過程中,您很可能會根據壓力測試和基準測試結果執行正在進行的配置優化和多次迭代。在達到所需指標之前,您可能還需要多次調整參數,從而重新運行測試

通常,調優應該首先滿足內存使用要求延遲,最後滿足吞吐量

2. 衡量內存佔用量

要確定內存使用情況,首先需要知道活動數據的大小。

活動數據的大小是自應用程序進入穩定狀態以來活動數據佔用的Java堆的數量。必須以穩定狀態而不是啓動階段來測量活動數據。

  • 在啓動階段,JVM加載並啓動應用程序的主要模塊和數據; 因此,JVM參數還不穩定。

  • 穩定階段意味着應用程序已經運行了一段時間並進行了壓力測試。 更具體地說,當應用程序達到在生產環境中滿足業務高峯期要求的工作負載並在達到高峯後保持穩定時,它就處於穩定階段。 只有這樣,每個JVM性能參數纔會處於穩定狀態

如何確定內存佔用量

確保使用默認的JVM參數執行測試,因爲它可以讓您查看穩定階段應用程序需要多少內存。一旦應用以穩定狀態運行,您就必須根據平均老年代和永久代佔用率來估算內存佔用量,在穩定狀態期間查看Full GC日誌,您也可以使用最長的Full GC進行估算。

GC日誌是收集有意義且豐富的數據以幫助進行調整的最佳方法之一。 啓用GC日誌不會影響性能。 因此,即使在生產環境中也可以使用它們來檢測問題。可通過上文GC LOG 分析確定內存佔用量

2. 調整延遲

一旦確定了內存佔用量,下一步就是延遲調整。 在此階段,堆內存大小和延遲不滿足應用程序要求。 因此,需要根據應用程序的實際需求進行新的調試。 您可能必須再次調整堆大小,確定GC的持續時間和頻率,並確定是否需要切換到另一個垃圾收集器

確定系統延遲要求

我們提到了性能目標,但我們沒有爲它們設定值。 這些目標是調整後需要滿足的系統延遲要求。 有助於實現目標的指標包括

  • 可接受的Minor GC的平均頻率,您可以將其與Minor GC的數量進行比較
  • 可接受的最大Full GC暫停時間,您可以將其與最長的Full GC週期進行比較
  • 最高Full GC暫停的可接受頻率,您將其與Full GC的最高頻率進行比較
  • 可接受的Minor GC平均暫停時間,您可以將其與Minor GC持續時間進行比較

3. 吞吐量調整

在JVM性能調整的最後一步,我們對到目前爲止得到的結果進行吞吐量測試,然後根據需要進行一些調整。根據測試和整體應用程序要求,應用程序應具有設置的吞吐量指標。 當達到或超過此目標時,您可以停止調整。

但是,如果經過優化後,您仍然無法達到吞吐量目標,則需要重新實現它,並評估吞吐量需求與當前的吞吐量之間的差距。 如果差距大約爲20%,則可以更改參數,增加內存並再次調試應用程序。 但是,如果差距大於20%,則需要將吞吐量目標作爲吞吐量目標進行審查,並且設計可能無法滿足整個Java應用程序的要求

對於垃圾回收,吞吐量調整有兩個目的: 這些會導致低吞吐量

  • 最小化傳遞到老年代的對象數量
  • 減少FULL GC執行時間或stop-the-world事件
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章