一、內存調優
1.內存溢出和內存泄漏
- 內存泄漏(memory leak):在Java中如果不再使用一個對象,但是該對象依然在GC ROOT的引用鏈上,這個對象就不會被垃圾回收器回收,這種情況就稱之為內存泄漏。
- 內存泄漏絕大多數情況都是由堆內存泄漏引起的,所以后續沒有特別說明則討論的都是堆內存泄漏。
2.內存泄漏的常見場景?
- 內存泄漏導致溢出的常見場景是大型的Java后端應用中,在處理用戶的請求之后,沒有及時將用戶的數據刪除。隨著用戶請求數量越來越多,內存泄漏的對象占滿了堆內存最終導致內存溢出。
- 這種產生的內存溢出會直接導致用戶請求無法處理,影響用戶的正常使用。重啟可以恢復應用使用,但是在運行一段時間之后依然會出現內存溢出。
- 第二種常見場景是分布式任務調度系統如Elastic-job、Quartz等進行任務調度時,被調度的Java應用在調度任務結束中出現了內存泄漏,最終導致多次調度之后內存溢出。
- 這種產生的內存溢出會導致應用執行下次的調度任務執行。同樣重啟可以恢復應用使用,但是在調度執行一段時間之后依然會出現內存溢出。
3.解決內存溢出的思路
檢測問題工具
Top命令
- top命令是linux下用來查看系統信息的一個命令,它提供給我們去實時地去查看系統的資源,比如執行時的進程、線程和系統參數等信息。
- 進程使用的內存為RES(常駐內存)- SHR(共享內存)
VisualVM
- VisualVM是多功能合一的Java故障排除工具并且他是一款可視化工具,整合了命令行 JDK 工具和輕量級分析功能,功能非常強大。
- 這款軟件在Oracle JDK 6~8 中發布,但是在 Oracle JDK 9 之后不在JDK安裝目錄下需要單獨下載。下載地址:https://visualvm.github.io/
Arthas
Arthas 是一款線上監控診斷產品,通過全局視角實時查看應用 load、內存、gc、線程的狀態信息,并能在不修改應用代碼的情況下,對業務問題進行診斷,包括查看方法調用的出入參、異常,監測方法執行耗時,類加載信息等,大大提升線上問題排查效率。
Prometheus + Grafana
Prometheus+Grafana是企業中運維常用的監控方案,其中Prometheus用來采集系統或者應用的相關數據,同時具備告警功能。Grafana可以將Prometheus采集到的數據以可視化的方式進行展示。
堆內存狀況的對比
產生內存溢出原因一 :代碼中的內存泄漏
1、equals()和hashCode()導致的內存泄漏
2、非靜態的內部類和匿名內部類的錯誤使用導致內存泄漏
3、由于線程池中的線程不被回收導致的ThreadLocal內存泄漏
4、由于JDK6中的字符串常量池位于永久代,intern被大量調用并保存產生的內存泄漏
5、大量的數據在靜態變量中被引用,但是不再使用,成為了內存泄漏
產生內存溢出原因二 : 并發請求問題
- 并發請求問題指的是用戶通過發送請求向Java應用獲取數據,正常情況下Java應用將數據返回之后,這部分數據就可以在內存中被釋放掉。
- 并發請求問題指的是用戶通過發送請求向Java應用獲取數據,正常情況下Java應用將數據返回之后,這部分數據就可以在內存中被釋放掉。但是由于用戶的并發請求量有可能很大,同時處理數據的時間很長,導致大量的數據存在于內存中,最終超過了內存的上限,導致內存溢出。這類問題的處理思路和內存泄漏類似,首先要定位到對象產生的根源。
診斷 – 內存快照
- 當堆內存溢出時,需要在堆內存溢出時將整個堆內存保存下來,生成內存快照(Heap Profile )文件。
- 生成內存快照的Java虛擬機參數:
- ??? -XX:+HeapDumpOnOutOfMemoryError:發生OutOfMemoryError錯誤時,自動生成hprof內存快照文件。
- -XX:HeapDumpPath=<path>:指定hprof文件的輸出路徑。
- 生成內存快照的Java虛擬機參數:
- 使用MAT打開hprof文件,并選擇內存泄漏檢測功能,MAT會自行根據內存快照中保存的數據分析內存泄漏的根源。
MAT內存泄漏檢測的原理 – 支配樹
MAT提供了稱為支配樹(Dominator Tree)的對象圖。支配樹展示的是對象實例間的支配關系。在對象引用圖中,所有指向對象B的路徑都經過對象A,則認為對象A支配對象B。
MAT內存泄漏檢測的原理 – 深堆和淺堆
支配樹中對象本身占用的空間稱之為淺堆(Shallow Heap)。
支配樹中對象的子樹就是所有被該對象支配的內容,這些內容組成了對象的深堆(Retained Heap),也稱之為保留集( Retained Set )?。深堆的大小表示該對象如果可以被回收,能釋放多大的內存空間。
解決內存溢出的思路
修復問題
并發引起內存溢出 – 設計不當
- 系統的方案設計不當,比如:
- 從數據庫獲取超大數據量的數據
- 線程池設計不當,生產者-消費者模型,消費者消費性能問題
解決方案:優化設計方案
并發引起內存溢出 - 參數不當
- 由于參數設置不當,比如堆內存設置過小,導致并發量增加之后超過堆內存的上限。
解決方案:調整參數,
二、GC調優
GC調優
- GC調優指的是對垃圾回收(Garbage Collection)進行調優。GC調優的主要目標是避免由垃圾回收引起程序性能下降。
GC調優的核心分成三部分:
1、通用Jvm參數的設置。
2、特定垃圾回收器的Jvm參數的設置。
3、解決由頻繁的FULLGC引起的程序性能問題。
GC調優沒有沒有唯一的標準答案,如何調優與硬件、程序本身、使用情況均有關系,重點學習調優的工具和方法。
GC調優的核心指標
所以判斷GC是否需要調優,需要從三方面來考慮,與GC算法的評判標準類似:
1.吞吐量(Throughput) 吞吐量分為業務吞吐量和垃圾回收吞吐量
業務吞吐量指的在一段時間內,程序需要完成的業務數量。比如企業中對于吞吐量的要求可能會是這樣的:
- 支持用戶每天生成10000筆訂單
- 在晚上8點到10點,支持用戶查詢50000條商品信息
保證高吞吐量的常規手段有兩條:
1、優化業務執行性能,減少單次業務的執行時間
2、優化垃圾回收吞吐量
垃圾回收吞吐量
垃圾回收吞吐量指的是 CPU 用于執行用戶代碼的時間與 CPU 總執行時間的比值,即吞吐量 = 執行用戶代碼時間 /(執行用戶代碼時間 + GC時間)。吞吐量數值越高,垃圾回收的效率就越高,允許更多的CPU時間去處理用戶的業務,相應的業務吞吐量也就越高。
2. 延遲(Latency)
延遲指的是從用戶發起一個請求到收到響應這其中經歷的時間。比如企業中對于延遲的要求可能會是這樣的:
所有的請求必須在5秒內返回給用戶結果
延遲 = GC延遲 + 業務執行時間,所以如果GC時間過長,會影響到用戶的使用。
3. 內存使用量
內存使用量指的是Java應用占用系統內存的最大值,一般通過Jvm參數調整,在滿足上述兩個指標的前提下,這個值越小越好。
發現問題工具
jstat工具
- Jstat工具是JDK自帶的一款監控工具,可以提供各種垃圾回收、類加載、編譯信息等不同的數據。
- 使用方法為:jstat -gc 進程ID 每次統計的間隔(毫秒) 統計次數
visualvm插件
VisualVm中提供了一款Visual Tool插件,實時監控Java進程的堆內存結構、堆內存變化趨勢以及垃圾回收時間的變化趨勢。同時還可以監控對象晉升的直方圖。
Prometheus + Grafana
Prometheus+Grafana是企業中運維常用的監控方案,其中Prometheus用來采集系統或者應用的相關數據,同時具備告警功能。Grafana可以將Prometheus采集到的數據以可視化的方式進行展示。
GC日志
- 通過GC日志,可以更好的看到垃圾回收細節上的數據,同時也可以根據每款垃圾回收器的不同特點更好地發現存在的問題。
- 使用方法(JDK 8及以下):-XX:+PrintGCDetails -Xloggc:文件名
- 使用方法(JDK 9+):-Xlog:gc*:file=文件名
GC Viewer
GCViewer是一個將GC日志轉換成可視化圖表的小工具,github地址:
https://github.com/chewiebug/GCViewer
使用方法:java -jar gcviewer_1.3.4.jar 日志文件.log
GCeasy
GCeasy是業界首款使用AI機器學習技術在線進行GC分析和診斷的工具。定位內存泄漏、GC延遲高的問題,提供JVM參數優化建議,支持在線的可視化工具圖表展示。
官方網站:https://gceasy.io/
常見的GC模式
特點:呈現鋸齒狀,對象創建之后內存上升,一旦發生垃圾回收之后下降到底部,并且每次下降之后的內存大小接近,存留的對象較少。
一、正常情況
特點:呈現鋸齒狀,對象創建之后內存上升,一旦發生垃圾回收之后下降到底部,并且每次下降之后的內存大小接近,存留的對象較少。
二、緩存對象過多
特點:呈現鋸齒狀,對象創建之后內存上升,一旦發生垃圾回收之后下降到底部,并且每次下降之后的內存大小接近,處于比較高的位置。
問題產生原因: 程序中保存了大量的緩存對象,導致GC之后無法釋放,可以使用MAT或者HeapHero等工具進行分析內存占用的原因。
三、內存泄漏
特點:呈現鋸齒狀,每次垃圾回收之后下降到的內存位置越來越高,最后由于垃圾回收無法釋放空間導致對象無法分配產生OutOfMemory的錯誤。
問題產生原因: 程序中保存了大量的內存泄漏對象,導致GC之后無法釋放,可以使用MAT或者HeapHero等工具進行分析是哪些對象產生了內存泄漏。
四、持續的FullGC
特點:在某個時間點產生多次Full GC,CPU使用率同時飆高,用戶請求基本無法處理。一段時間之后恢復正常。
問題產生原因: 在該時間范圍請求量激增,程序開始生成更多對象,同時垃圾收集無法跟上對象創建速率,導致·持續地在進行FULL GC。GC分析報告
五、元空間不足導致的FULLGC
特點:堆內存的大小并不是特別大,但是持續發生FULLGC。
問題產生原因: 元空間大小不足,導致持續FULLGC回收元空間的數據。GC分析報告
解決GC問題的手段
優化基礎JVM參數
參數1 : -Xmx 和 –Xms
-Xmx參數設置的是最大堆內存,但是由于程序是運行在服務器或者容器上,計算可用內存時,要將元空間、操作系統、其它軟件占用的內存排除掉。
優化基礎JVM參數
參數1 : -Xmx 和 –Xms
-Xms用來設置初始堆大小,建議將-Xms設置的和-Xmx一樣大,有以下幾點好處:
- 運行時性能更好,堆的擴容是需要向操作系統申請內存的,這樣會導致程序性能短期下降。
- 可用性問題,如果在擴容時其他程序正在使用大量內存,很容易因為操作系統內存不足分配失敗。
- 啟動速度更快,Oracle官方文檔的原話:如果初始堆太小,Java 應用程序啟動會變得很慢,因為 JVM 被迫頻繁執行垃圾收集,直到堆增長到更合理的大小。為了獲得最佳啟動性能,請將初始堆大小設置為與最大堆大小相同。
參數2 : -XX:MaxMetaspaceSize 和 –XX:MetaspaceSize
-XX:MaxMetaspaceSize=值 參數指的是最大元空間大小,默認值比較大,如果出現元空間內存泄漏會讓操作系統可用內存不可控,建議根據測試情況設置最大值,一般設置為256m。
-XX:MetaspaceSize=值 參數指的是到達這個值之后會觸發FULLGC(網上很多文章的初始元空間大小是錯誤的),后續什么時候再觸發JVM會自行計算。如果設置為和MaxMetaspaceSize一樣大,就不會FULLGC,但是對象也無法回收。
參數3 : -Xss虛擬機棧大小
如果我們不指定棧的大小,JVM 將創建一個具有默認大小的棧。大小取決于操作系統和計算機的體系結構。
比如Linux x86 64位 : 1MB,如果不需要用到這么大的棧內存,完全可以將此值調小節省內存空間,合理值為256k – 1m之間。
使用:-Xss256k
參數4 : 不建議手動設置的參數
由于JVM底層設計極為復雜,一個參數的調整也許讓某個接口得益,但同樣有可能影響其他更多接口。
‐XX:SurvivorRatio 伊甸園區和幸存者區的大小比例,默認值為8。
‐XX:MaxTenuringThreshold 最大晉升閾值,年齡大于此值之后,會進入老年代。另外JVM有動態年齡判斷機制:將年齡從小到大的對象占據的空間加起來,如果大于survivor區域的50%,然后把等于或大于該年齡的對象,放入到老年代。
其他參數 :
- -XX:+DisableExplicitGC
禁止在代碼中使用System.gc(), System.gc()可能會引起FULLGC,在代碼中盡量不要使用。使用
DisableExplicitGC參數可以禁止使用System.gc()方法調用。
- -XX:+HeapDumpOnOutOfMemoryError:發生OutOfMemoryError錯誤時,自動生成hprof內存快照文件。
-XX:HeapDumpPath=<path>:指定hprof文件的輸出路徑。
- 打印GC日志
JDK8及之前 : -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:文件路徑
JDK9及之后 : -Xlog:gc*:file=文件路徑
JVM參數模板:
-Xms1g
-Xmx1g
-Xss256k
-XX:MaxMetaspaceSize=512m
-XX:+DisableExplicitGC
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/logs/my-service.hprof
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:文件路徑
注意:
JDK9及之后gc日志輸出修改為 -Xlog:gc*:file=文件名
堆內存大小和棧內存大小根據實際情況靈活調整。
垃圾回收器的選擇
垃圾回收器的組合關系
垃圾回收器是垃圾回收算法的具體實現。
性能調優
應用程序在運行過程中經常會出現性能問題,比較常見的性能問題現象是:
1、通過top命令查看CPU占用率高,接近100甚至多核CPU下超過100都是有可能的。
2、請求單個服務處理時間特別長,多服務使用skywalking等監控系統來判斷是哪一個環節性能低下。
3、程序啟動之后運行正常,但是在運行一段時間之后無法處理任何的請求(內存和GC正常)。
線程轉儲的查看方式
線程轉儲(Thread Dump)提供了對所有運行中的線程當前狀態的快照。線程轉儲可以通過jstack、visualvm等工具獲取。其中包含了線程名、優先級、線程ID、線程狀態、線程棧信息等等內容,可以用來解決CPU占用率高、死鎖等問題。
線程轉儲(Thread Dump)中的幾個核心內容:
- 名稱: 線程名稱,通過給線程設置合適的名稱更容易“見名知意”
- 優先級(prio):線程的優先級
- Java ID(tid):JVM中線程的唯一ID
- 本地 ID (nid):操作系統分配給線程的唯一ID
- 狀態:線程的狀態,分為:
- NEW – 新創建的線程,尚未開始執行
- RUNNABLE –正在運行或準備執行
- BLOCKED – 等待獲取監視器鎖以進入或重新進入同步塊/方法
- WAITING – 等待其他線程執行特定操作,沒有時間限制
- TIMED_WAITING – 等待其他線程在指定時間內執行特定操作
- TERMINATED – 已完成執行
- TERMINATED – 已完成執行
更精細化的性能測試
JIT對程序性能的影響
Java程序在運行過程中,JIT即時編譯器會實時對代碼進行性能優化,所以僅憑少量的測試是無法真實反應運行系統最終給用戶提供的性能。如下圖,隨著執行次數的增加,程序性能會逐漸優化。
正確地測試代碼性能
OpenJDK中提供了一款叫JMH(Java Microbenchmark Harness)的工具,可以準確地對Java代碼進行基準測試,量化方法的執行性能。
官網地址:https://github.com/openjdk/jmh
JMH會首先執行預熱過程,確保JIT對代碼進行優化之后再進行真正的迭代測試,最后輸出測試的結果。