前言
之前或多或少分享過一些記憶體模型、物件建立之類的內容,其實大部分人看完都是懵懵懂懂,也不知道這些的實際意義。
直到有一天你會碰到線上奇奇怪怪的問題,如:
- 執行緒執行一個任務遲遲沒有返回,應用假死。
- 介面響應緩慢,甚至請求超時。
- CPU 高負載執行。
這類問題並不像一個空指標、陣列越界這樣明顯好查,這時就需要剛才提到的記憶體模型、物件建立、執行緒等相關知識結合在一起來排查問題了。
正好這次藉助之前的一次生產問題來聊聊如何排查和解決問題。
生產現象
首先看看問題的背景吧:
我這其實是一個定時任務,在固定的時間會開啟 N 個執行緒併發的從 Redis 中獲取資料進行運算。
業務邏輯非常簡單,但應用一般涉及到多執行緒之後再簡單的事情都要小心對待。
果不其然這次就出問題了。
現象:原本只需要執行幾分鐘的任務執行了幾個小時都沒退出。翻遍了所有的日誌都沒找到異常。
於是便開始定位問題之路。
定位問題
既然沒辦法直接從日誌中發現異常,那就只能看看應用到底在幹嘛了。
最常見的工具就是 JDK 自帶的那一套。
這次我使用了 jstack
來檢視執行緒的執行情況,它的作用其實就是 dump 當前的執行緒堆疊。
當然在 dump 之前是需要知道我應用的 pid 的,可以使用 jps -v
這樣的方式列出所有的 Java 程式。
當然如果知道關鍵字的話直接使用 ps aux|grep java
也是可以的。
拿到 pid=1523
了之後就可以利用 jstack 1523 > 1523.log
這樣的方式將 dump 檔案輸出到日誌檔案中。
如果應用簡單不復雜,執行緒這些也比較少其實可以直接開啟檢視。
但複雜的應用匯出來的日誌檔案也比較大還是建議用專業的分析工具。
我這裡的日誌比較少直接開啟就可以了。
因為我清楚知道應用中開啟的執行緒名稱,所以直接根據執行緒名就可以在日誌中找到相關的堆疊:
所以通常建議大家執行緒名字給的有意義,在排查問題時很有必要。
其實其他幾個執行緒都和這裡的堆疊類似,很明顯的看出都是在做 Redis 連線。
於是我登入 Redis 檢視了當前的連線數,發現已經非常高了。
這樣 Redis 的響應自然也就變慢了。
接著利用 jps -v
列出了當前所以在跑的 Java 程式,果不其然有好幾個應用都在查詢 Redis,而且都是併發連線,問題自然就找到了。
解決辦法
所以問題的主要原因是:大量的應用併發查詢 Redis,導致 Redis 的效能降低。
既然找到了問題,那如何解決呢?
- 減少同時查詢 Redis 的應用,分開時段降低 Redis 的壓力。
- 將 Redis 複製幾個叢集,各個應用分開查詢。但是這樣會涉及到資料的同步等運維操作,或者由程式了進行同步也會增加複雜度。
目前我們選擇的是第一個方案,效果很明顯。
本地模擬
上文介紹的是執行緒相關問題,現在來分析下記憶體的問題。
以這個類為例:
public class HeapOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<>(10) ;
while (true){
list.add("1") ;
}
}
}
複製程式碼
啟動引數如下:
-Xms20m
-Xmx20m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/Users/xx/Documents
複製程式碼
為了更快的突出記憶體問題將堆的最大記憶體固定在 20M,同時在 JVM 出現 OOM 的時候自動 dump 記憶體到 /Users/xx/Documents
(不配路徑則會生成在當前目錄)。
執行之後果不其然出現了異常:
同時對應的記憶體 dump 檔案也生成了。
記憶體分析
這時就需要相應的工具進行分析了,最常用的自然就是 MAT 了。
我試了一個線上工具也不錯(檔案大了就不適合了):
上傳剛才生成的記憶體檔案之後:
因為是記憶體溢位,所以主要觀察下大物件:
也有相應提示,這個很有可能就是記憶體溢位的物件,點進去之後:
看到這個堆疊其實就很明顯了:
在向 ArrayList 中不停的寫入資料時,會導致頻繁的擴容也就是陣列複製這些過程,最終達到 20M 的上限導致記憶體溢位了。
更多建議
上文說過,一旦使用了多執行緒,那就要格外小心。
以下是一些日常建議:
- 儘量不要線上程中做大量耗時的網路操作,如查詢資料庫(可以的話在一開始就將資料從從 DB 中查出準備好)。
- 儘可能的減少多執行緒競爭鎖。可以將資料分段,各個執行緒分別讀取。
- 多利用
CAS+自旋
的方式更新資料,減少鎖的使用。 - 應用中加上
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp
引數,在記憶體溢位時至少可以拿到記憶體日誌。 - 執行緒池監控。如執行緒池大小、佇列大小、最大執行緒數等資料,可提前做好預估。
- JVM 監控,可以看到堆記憶體的漲幅趨勢,GC 曲線等資料,也可以提前做好準備。
總結
線上問題定位需要綜合技能,所以是需要一些基礎技能。如執行緒、記憶體模型、Linux 等。
當然這些問題沒有實操過都是紙上談兵;如果第一次碰到線上問題,不要慌張,反而應該慶幸解決之後你又會習得一項技能。
號外
最近在總結一些 Java 相關的知識點,感興趣的朋友可以一起維護。
歡迎關注公眾號一起交流: