《JVM故障診斷指南》之3 —— Java 線程: JVM持有內存的分析
前麵我們已經討論過JVM裏不同的堆空間,這節我們會給你提供教程,是關於如何從你的活動的應用Java線程中確定它持有多少堆空間,以及在哪裏占用。這裏有個來自Oracle Weblogic 10.0生產環境的真實案例,它能使你更好的理解分析過程。
我們也會演示這種情況,過多的垃圾收集或者堆空間內存占用問題並不總是由於真實的內存泄露引起,也可能是由於線程執行模型問題和太多的短生命對象引起。
後台
Java線程是JVM基礎的一部分。你的Java堆空間內存占用不僅僅是由於靜態的和長生命的對象導致,還有可能因為短生命對象。
OutOfMemoryError 問題經常被誤認為是內存泄露引起。我們經常忽略錯誤的線程執行模型和它們持有的JVM裏的短生命對象,直到它們的執行完成我們才發現。在這種問題情形下:
• 你“預期”的程序中短生命/無狀態對象(XML,JSON數據負載等)被線程持有的時間會變得很長(線程鎖爭用,大量數據負載,遠程係統的慢響應時間等)。
• 最後,這種短生命對象會因為垃圾收集而晉升到長生命空間,比如老年代空間。
• 副作用是會導致老年代空間很快被占滿,增加了Full GC(major 收集)的頻率。
• 由於這種嚴重的情況,它將導致更多的GC 垃圾收集,增加JVM暫停時間和最終的“OutOfMemoryError: Java 堆空間”。
• 你的應用此時被停掉,你很疑惑到底怎麼回事。
• 最後,你考慮增加Java堆空間或者尋找哪裏有內存泄露,你真的找對路了麼?
上麵這種情況,你應該找到線程執行模型並確定在給定的時間內每個線程需要持有多少內存。
OK我找到了這張圖片,但是線程棧大小到底是多少呢?
避免在線程棧大小和Java堆內存占用之間產生混淆是非常重要的。線程棧大小是一種特殊的內存空間,它被JVM用於存儲每個方法調用。當一個線程調用方法A,它將這個調用入棧。如果方法A調用方法B,同樣也會入棧。一旦方法執行完畢,這個調用便從棧裏出棧。
這種線程方法調用會導致Java對象產生,並分配在Java堆裏。增加線程棧的大小是沒有任何效果的。而調整線程棧大小通常是要處理java.lang.stackoverflowerror錯誤或者“OutOfMemoryError: unable to create new native thread”錯誤的時候才會需要。
案例研究和問題環境
下麵的分析是基於我們最近調查的一個真實的生產線問題。
1. 改變了用戶web接口(使用Google Web Toolkit 和 JSON作為數據負載)後,發現Weblogic 10.0生產環境上出現了某些性能下降。
2. 初始分析發現出現了“OutOfMemoryError: Java heap space”問題並伴有過多的垃圾收集。在OOM出現後自動(XX:+HeapDumpOnOutOfMemoryError)生成了Java堆轉儲文件。
3. 通過verbose:gc 日誌分析確認32-bit HotSpot JVM 老年代空間(1 GB 容量)被完全消耗。
4. 問題發生前和發生時自動產生了線程轉儲快照。
5. 此時唯一可能減輕問題的方法是在問題發生時重啟受影響的Weblogic 服務器。
6. 最終解決了這個問題是將所有的改變回滾。
7.
團隊首先懷疑新代碼引起了內存泄露。
線程轉儲分析:尋找嫌疑
第一步我們要做的是對產生的線程轉儲數據進行分析。這種數據通常會告訴你在JVM堆裏麵內存分配的罪魁禍首線程。同樣的它也會顯示任何一個嚐試從遠程係統發送和接受數據的貪婪或者阻塞的線程。
我們注意的第一個樣例是在Weblogic 控製服務器(JVM線程)裏觀察到的OOM事件和阻塞線程之間有很近的關聯關係。下麵是找到的原始線程模式:
000337> < [STUCK] ExecuteThread: '22' for queue: 'weblogic.kernel.Default (self-tuning)' has been busy for "672" seconds working on the request which is more than the configured time of "600" seconds.
如你所見,上麵的線程出現了阻塞並且花費了很長時間從遠程係統裏讀和接收JSON響應。一旦我們找到這個樣例,接下來是找出它和JVM堆轉儲分析之間的關聯,並且確定這個阻塞線程從堆裏占用了多少內存。
堆轉儲分析:暴露留存的對象!
使用MAT工具來做Java堆轉儲分析。我們會列出不同的分析步驟,它會允許我們精確查明持有的內存大小和源頭。
1.加載HotSpotJVM堆轉儲文件
2.選擇HISTOGRAM 查看並通過ExecuteThread過濾。
* ExecuteThread 是一種Java Class,它被Weblogic內核用於對象的創建和執行*
如你所見,這個圖非常有啟迪作用。我們可以看到總共210個Weblogic線程被創建。
這些線程總共持有的內存占用是806M。這對於帶有1G 老年代的32位的JVM進程來說是非常值得注意的。這個圖也告訴了我們這個問題的核心和源於線程自己的內存占有。
3.深入分析線程內存占用
接下來是深入分析線程內存持有。右鍵點擊ExecuteThread 類並且選擇“列出所有外部引用的對象”。
如你所見,我們通過線程堆轉儲分析可以發現“STUCK(阻塞)”線程和大量內存占用有很大關係。這個發現非常意外。
4.線程局部變量鑒別Thread Java Local variables identification
分析的最後一步是需要我們展開幾個線程示例並了解內存占用的原始來源。
如你所見,最後一步分析發現根源在於大量的JSON數據響應。通過對轉儲分析,這個問題可以早點暴露出來,我們發現少量的線程花費太多時間去讀取和接收JSON響應,這是大量數據負載的一個明顯的症狀。
很重要一點,通過方法局部變量創建的短生命對象也會出現在堆轉儲分析中。然而,其中的一些僅僅能被他們的父線程看到,這是由於他們沒有被其他對象引用,比如這個例子。為了找出真正的調用者你或許也需要分析這個線程棧,隨後通過代碼審查確定最終的根源。
通過這些發現,我們的交付團隊能確定最近的JSON錯誤代碼變化的產生,在某些情形下,大量的JSON數據可以達到45M以上。如果環境使用了32位JVM而且僅僅隻有1G的老年代,基於這個事實,你就能理解為什麼隻需要幾個線程就足夠觸發一些性能下降。
這個案例說明,合適的容量預計和堆分析,包括你活動的應用程序的內存占有和Java EE容器線程內存占有都是非常重要的。
譯者介紹:
梅小西
Java工程師,關注JVM,並發編程,喜歡研究Python,Scala,Golang等。
譯者相關譯文:
JVM內部原理
《JVM故障診斷指南》之1 ——JVM概覽與介紹
《JVM故障診斷指南》之2 ——調整合適的Java堆大小的技巧
《JVM故障診斷指南》之3 ——Java 線程: JVM持有內存的分析
《JVM故障診斷指南》之4 ——Java 8:從持久代到metaspace
最後更新:2017-05-22 15:34:44