最近對系統進行壓測時發現Server日誌報以下錯誤:java.lang.OutOfMemoryError: GC overhead limit exceeded。html
查看Oracle OOM文檔,在文檔中找到如下內容:java
Exception in thread thread_name: java.lang.OutOfMemoryError:GC Overhead limit exceeded Cause: The detail message "GC overhead limitexceeded" indicates that the garbage collector is running all the time andJava program is making very slow progress. After a garbage collection, if the Java process is spending more than approximately 98% of its time doing garbage collection and if it is recovering less than 2% of the heap and has been doing so far the last 5 (compile time constant) consecutive garbage collections, then a java.lang.OutOfMemoryError is thrown.This exception is typically thrown because the amount of live data barely fitsinto the Java heap having little free space for new allocations. Action:Increase the heap size.The java.lang.OutOfMemoryError exception for GC Overhead limit exceeded can be turned off with thecommand line flag -XX:-UseGCOverheadLimit. 原文連接以下: docs.oracle.com/javase/8/do… 上面一段文字大概意思是JVM用98%的時間回收了不到2%的堆內存,所以預測到內存即將耗盡,拋出OOM異常來提示內存不足,至關於給用戶一個警告信息。Oracle給的解決方法是提升堆內存,也能夠用-XX:-UseGCOverheadLimit參數來關閉這個異常警告。bash
這種方式其實只是治標不治本,讓用戶提升堆內存容量,雖然解決了燃眉之急,但實際上並無解決內存消耗過大的根本問題。若是堆內存一直增加,最終還會提示「GC overhead limit exceeded」。即便用XX:-UseGCOverheadLimit參數關閉OOM預測提示,當內存佔用增加到極限,最後也會直接報 java.lang.OutOfMemoryError: Java heap space,形成進程中止的嚴重影響。因此Oracle給的解決方案並不靠譜,只能做爲參考。服務器
經過以上描述,能夠推測進程中要麼有任務一直在申請內存,要麼發生了內存泄露。具體哪一種緣由還得對內存使用狀況深刻分析後判斷,如下是分析的整個過程。oracle
登上服務器,用 Java自帶的jmap生成dump 文件,命令以下:app
jmap -dump:live,format=b,file= heap.hprof less
將dump文件下載到本身電腦,用 MAT打開。 ide
從overview視圖看到java.util.concurrent.ThreadPoolExecutor @ 0x744ce0aa 共佔用了1.6G內存,比例佔到了75%,能夠初步判斷有一個ThreadPoolExecutor的實例佔據了大量內存,致使OOM。函數
點擊overview視圖下的Leak Suspects 按鈕,查看分析結果。 ui
點擊菜單欄的Hitstorgam按鈕,生成instances報告,並用Retained Heap進行排序。
從圖中看出佔用內存最多的是BufferedImages對象,QRCodeTask對象排到了第四,這個對象功能後面會提到。
選中BufferedImage右鍵查show object by classby incoming class,看到確實是QRCodeTask引用了BufferedImage。
選中BufferedImage,點擊Dominator Tree菜單。從圖中能夠看到確實是java.util.concurrent.LinkedBlockingQueue$Node引用了BufferImage,與Leak Suspects報告一致。
MAT分析工做基本到此結束,下一步找到相應代碼查看業務邏輯。
1.當有請求過來時會執行如下函數,把QRCodeBatchTask添加到線程池中。
public void applyQRcode() {
exec.execute(new QRCodeBatchTask());
}
複製代碼
2.QRCodeBatchTask會不斷往線程池提交QRCodeTask任務,並把生成的圖片賦值給QRCodeTask。
public void run() {
backImage=ImageIO.read(backGroundImageFile);
logoImage=ImageIO.read(logoFile);
for (List list : lists) {
exec.execute(new QRCodeTask(backImage,logoImage));
)}}
複製代碼
3.QRCodeTask保存backGroundImage,logoImage對象,並在任務執行中用到。
public class QRCodeTask implements Runnable {
private BufferedImage backGroundImage, logoImage;
public QRCodeTask(String backGroundImage, logoImage ) {
this.backGroundImage = backImage;
this.logoImage = logoImage;
}
}
複製代碼
以上業務代碼處理邏輯是每次請求過來時都會新建一個QRCodeBatchTask任務並提交到線程池中。QRCodeBatchTask會從磁盤讀取兩張圖片,賦值給新建的QRCodeTask任務,全部新建的任務都會提交到線程池中去執行。
ThreadPoolExecutor原理是把提交的全部任務添加到LinkedBlockingQueue隊列中,這也就是圖中java.util.concurrent.LinkedBlockingQueue$Node對象引用QRCodeTask對象的緣由。實際壓測時候隨着請求的持續增長,新建的QRCodeBatchTask愈來愈多,生成的圖片也愈來愈多,最終致使佔用的內存持續增加直至OOM發生。
緣由已經知道,解決問題就相對容易了。由於每一個任務用到的backGroundImage,logoImage實際上是同樣的,所以能夠用單例模式獲取這兩個圖片,以此來保證進程內只保留一個圖片對象,這樣就減小了內存的佔用。
經過重構代碼,再次運行程序並用VisualVM進行監控能夠觀察到內存佔用趨於平穩,沒有再發生OOM,問題獲得解決。
想要了解更多,關注公衆號:七分熟pizza