某線上應用在進行查詢結果導出Excel時,大機率出現持續的FullGC。解決這個問題時,記錄了一下整個的流程,也能夠做爲通常性的FullGC問題排查指導。html
爲了定位FullGC的緣由,首先須要獲取heap dump文件,看下發生FullGC時堆內存的分配狀況,定位可能出現問題的地方。java
能夠在JVM參數中設置-XX:+ HeapDumpBeforeFullGC
參數。
建議動態增長這個參數,直接在線上鏡像中增長一方面是要從新打包發佈,另外一方面風險比較高linux
sudo -u admin /opt/taobao/java/bin/jinfo -flag +HeapDumpBeforeFullGC pid
sudo -u admin /opt/taobao/java/bin/jinfo -flag +HeapDumpAfterFullGC pidapache
也能夠用HeapDumpOnOutOfMemoryError這個參數,只在outOfMemoryError發生時才dump。實測只有在fullgc完成時纔會產生該文件,fullgc期間看不到。
此外還須要-XX:HeapDumpPath=/home/admin/logs/java.hprof
這個參數來指定dump文件存放路徑。json
先獲取java進程ID,再使用jmap進行dump。
注意,虛擬機上的jmap可能沒有作路徑映射,須要手動選擇jdk路徑下來執行api
ps -aux | grep java jmap -dump:file=test.hprof,format=b XXXX
JDK7後新增的多功能命令,其中jcmd pid GC.heap_dump FILE_NAME
的效果和jmap -dump:file=test.hprof,format=b pid
同樣。瀏覽器
能夠生成本機或遠程JVM的dump。還有一些其餘工具就不詳細介紹了。服務器
因爲使用的是阿里雲的服務器,能夠直接將dump文件上傳到OSS上經過公司內部工具來分析,或經過OSS再下載到本地。
設置OSSCMD:
操做命令 osscmd config --host=oss-cn-hangzhou-am101.aliyuncs.com --id=** --key=**
建立bucke:osscmd cb 000001
上傳文件:osscmd put 1.txt oss://000001/
下載文件:osscmd get oss://000001/1.txt 1.txtapp
其餘類型的Linux主機可使用SCP命令,參考:Linux scp命令框架
經過dump文件來分析fullGC的緣由,須要關注哪些類佔用內存空間較多、不可到達類等。
因爲使用的是公司內部工具Zprofiler和grace,詳細的使用過程這裏就不截圖了。一些其餘可用的工具和命令(參考Java內存泄漏分析系列之六:JVM Heap Dump(堆轉儲文件)的生成和MAT的使用):
jhat <heap-dump-file>
生成網頁,經過瀏覽器訪問``查看須要注意的是,只看dump文件有時還不能獲得結論,由於佔用空間大頭的有多是String、ArrayBlockingList這樣的對象,並且內容多是null或null對象的集合,無從排查。此時還要結合發生fullgc先後業務系統發生了什麼動做來肯定。若是有條件的話能夠在平常環境或預發環境重現一下。
固然,若是內存中的空間消耗對象是特殊的類,就比較好排查了。
具體狀況具體分析。
查詢DB中數據->在異步線程中經過poi轉換成Excel->上傳到OSS。
示例代碼:
// 導出代碼中將變量直接做爲lambda表達式的值傳入 List<XXData> data = queryData(request); SheetDownloadProperty property = sheetDownloadProperties.get(0); property.setTotalCount(request.getQueryRequest().getPageSize()); property.setPageSize(request.getQueryRequest().getPageSize()); property.setQueryFunction((currentPage, pageSize) -> data); // 該組件會在線程池異步調用poi組件轉換爲excel、上傳OSS、下載 asyncDownloadService.downloadFile(downloadTask);
private List<XXData> queryData(ExportRequest request) { //查詢DB,略 }
// 查詢方法 @FunctionalInterface public interface PageFunction<T> { /** * 方法執行 */ List<T> apply(Integer currentPage,Integer pageSize); }
經過內部工具可見,fullGC前有三個佔據內存較高的ArrayBlockingList,裏面有大量的內容爲null的Object。
這三個ArrayBlockingList所屬的中間件,雖然自己和業務流程沒有關係,可是仍不能排除嫌疑。
因爲依賴了二方庫poi,這個庫的usermodel模式很容易引發fullGC,同時也懷疑是由於lambda表達式直接傳了變量。
把poi的usermodel改成事件模式(https://my.oschina.net/OutOfMemory/blog/1068972)能夠避免這個問題。
可是該功能是一個二次封裝的三方包中的,同時其餘引用該組件的應用fullgc頻率並不高,沒有采用這個方案。
持有大量null對象的中間件版本較低,且新版目前已再也不維護,老版本的releas note雖然沒有提到這條bug fix,有必定嫌疑。
該中間件初始化時會建立三個容量爲810241024的ArrayBlockingList,和dump文件相符合。
一樣是由於這個中間件是在三方包中封裝,不方便直接該版本,一樣沒有采用這個方案。
能夠調整metaspace參數來實現,本次想找到代碼中相關的線索來解決,未採用該方案。
仔細觀察了這段代碼在其餘系統的的實現,發現其餘系統的lambda表達式是匿名方法,而不是直接傳值,即:
property.setQueryFunction((currentPage, pageSize) -> { // 查詢邏輯, 略 );
懷疑是直接傳變量進去致使的垃圾回收問題。更改到這種模式後,觸發下載功能時,連續長時間的fullGC仍然時有發生,沒有解決問題。
暫時能肯定的緣由是,公司中間件自己佔用堆內存較多,運行poi增長了GC的頻率。可是因爲它們都在二方庫的緣由,不方便修改。
此時搜索到stackoverflow有關於poi反覆GC的一個問題,和個人狀況相似,也是反覆GC可是仍然不能釋放內存。有回覆建議將GC回收器替換爲G1GC,將默認的UseConcMarkSweepGC替換後效果明顯,一次FullGC就能夠完成回收釋放,不會反覆FullGC,以下圖,20:30前的fullGC是CMS,持續時間長且反覆進行;20:30後是替換後第一次觸發excel轉換下載,進行了屢次下載,即便發生FullGC也只有1次,大大緩解了以前的問題:
本次暫定只採用方案5。
G1GC在JDK9已替代CMS成爲了正式的垃圾回收器,低版本JDK須要手動設置。具體須要設置的JVM參數:
-Xms32m -Xmx1g -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC -XX:MaxHeapFreeRatio=15 -XX:MinHeapFreeRatio=5
注意前兩行通常應用都會設置,不要覆蓋掉。最後兩行須要視狀況調整。另外,默認的-XX:+UseConcMarkSweepGC
須要去掉。
使用G1GC時須要確認工做線程數是否和預期一致,不要太多,通常來講和CPU核數一致便可。出現非預期數目的緣由多是,鏡像腳本指定核數時,直接按照物理機而不是虛擬機核數來生成。
查看方式是看gc日誌:
虛擬機設置核數的dokcker腳本示例:
export CPU_COUNT="$(grep -c 'cpu[0-9][0-9]*' /proc/stat)"
core dump是針對線程某一時刻的運行狀況的,能夠看到執行到哪一個類哪一個方法哪一行以及執行棧的;heap dump是針對內存某一時刻的分配狀況的。
簡單摘譯了一些,能夠直接看原文。
關於G1GC,會在後續文章中研究。