生產環境Java應用服務內存泄漏分析與解決

有個生產環境CRM業務應用服務,情況有些奇怪,監控數據顯示內存異常。內存使用率99.%多。通過生產監控看板發現,CRM內存超配或內存泄漏的現象,下面分析一下這個問題過程記錄。

1、服務器硬件配置部署情況

生產服務器採用阿里雲ECS機器,配置是4HZ、8GB,單個應用服務獨佔,CRM應用獨立部署,即單臺服務器僅部署一個java應用服務。

用了4個節點4臺機器,每臺機器都差不多情況。

監控看板如下:

 

2、應用啓動參數配置

應用啓動配置參數:

 /usr/bin/java

-javaagent:/home/agent/skywalking-agent.jar

-Dskywalking.agent.service_name=xx-crm

 -XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=/tmp/xx-crm.hprof

-Dspring.profiles.active=prod

-server -Xms4884m -Xmx4884m -Xmn3584m

-XX:MetaspaceSize=512m

-XX:MaxMetaspaceSize=512m

-XX:CompressedClassSpaceSize=128m

-jar /home/xxs-crm.jar

 

堆內:最大最小堆內存4884m約4.8G左右,其中新生代-Xmn3584m 約3.5G左右,

非堆: 元數據區配置 512M,類壓縮空間 128M, Code Cache代碼緩存區240M(沒有配置參數,通過監控看板看到的)。

 

3、內存分佈統計

 

從監控看板的數據來看,我們簡單統計一下內存分配數據情況。

通過JVM配置參數和監控看板數據可知:

堆內存:4.8G

非堆內存:(Metaspace)512M+(CompressedClassSpace)128M+(Code Cache)240M約等1GB左右。

堆內存(heap)+非堆內存(nonHeap)=5.8G

8GB物理內存除去操作系統本身佔用大概500M。即除了操作系統本身佔用之外,還有7.5G可用內存。

但是 7.5-5.8=1.7GB,起碼至少還有1~2GB空閒才合理呀!怎麼內存佔用率99%多,就意味着有1~2G不知道誰佔去了,有點詭異!

 

4、問題分析

先看一下JVM內存模型,環境是使用JDK8

JVM內存數據分區:

 

堆heap結構:

堆大家都比較容易理解的,也是java程序接觸得最多的一塊,不存在什麼數據上統計錯誤,或佔用不算之類的。

那說明額外佔用也非堆裏面,只不過沒有統計到非堆裏面去,曾經一度懷疑監控prometheus展示的數據有誤

先看一下dump文件數據,這裏使用MAT工具(一個開源免費的內存分析工具,個人認爲比較好用,推薦大家使用。下載地址:https://www.eclipse.org/mat/downloads.php)。

 

通過下載內存dump鏡像觀察到

 

有個offHeapStore,這個東西堆外內存,可以初步判斷是 ehcahe引起的。

通過ehcahe源碼分析,發現ehcache裏面也使用了netty的NIO方法內存,ehcache磁盤緩存寫數據時會用到DirectByteBuffer。

DirectByteBuffer是使用非堆內存,不受GC影響。

 

當有文件需要暫存到ehcache的磁盤緩存時,使用到了NIO中的FileChannel來讀取文件,默認ehcache使用了堆內的HeapByteBuffer來給FileChannel作爲讀取文件的緩衝,FileChannel讀取文件使用的IOUtil的read方法,針對HeapByteBuffer底層還用到一個臨時的DirectByteBuffer來和操作系統進行直接的交互。

 

ehcache使用HeapByteBuffer作爲讀文件緩衝:

 

IOUtil對於HeapByteBuffer實際會用到一個臨時的DirectByteBuffer來和操作系統進行交互。

 

 

DirectByteBuffer泄漏根因分析

默認情況下這個臨時的DirectByteBuffer會被緩存在一個ThreadLocal的bufferCache裏不會釋放,每一個bufferCache有一個DirectByteBuffer的數組,每次當前線程需要使用到臨時DirectByteBuffer時會取出自己bufferCache裏的DirectByteBuffer數據,選取一個不小於所需size的,如果bufferCache爲空或者沒有符合的,就會調用Bits重新創建一個,使用完之後再緩存到bufferCache裏。

這裏的問題在於 :這個bufferCache是ThreadLocal的,意味着極端情況下有N個調用線程就會有N組 bufferCache,就會有N組DirectByteBuffer被緩存起來不被釋放,而且不同於在IO時直接使用DirectByteBuffer,這N組DirectByteBuffer連GC時都不會回收。我們的文件服務在讀寫ehcache的磁盤緩存時直接使用的tomcat的worker線程池,

這個worker線程池的配置上限是2000,我們的配置中心上的配置的參數:

 

所以,這種隱藏的問題影響所有使用到HeapByteBuffer的地方而且很隱祕,由於在CRM服務中大量使用了ehcache存在較大的sizeIO且調用線程比較多的場景下容易暴露出來。

 

獲取臨時DirectByteBuffer的邏輯:


bufferCache從ByteBuffer數組裏選取合適的ByteBuffer:

 

將ByteBuffer回種到bufferCache:

 

NIO中的FileChannelSocketChannelChannel默認在通過IOUtil進行IO讀寫操作時,除了會使用HeapByteBuffer作爲和應用程序的對接緩衝,但在底層還會使用一個臨時的DirectByteBuffer來和系統進行真正的IO交互,爲提高性能,當使用完後這個臨時的DirectByteBuffer會被存放到ThreadLocal的緩存中不會釋放,當直接使用HeapByteBuffer的線程數較多或者IO操作的size較大時,會導致這些臨時的DirectByteBuffer佔用大量堆外直接內存造成泄漏。

那麼除了減少直接調用ehcache讀寫的線程數有沒有其他辦法能解決這個問題?併發比較高的場景下意味着減少業務線程數並不是一個好辦法。

在Java1.8_102版本開始,官方提供一個參數jdk.nio.maxCachedBufferSize,這個參數用於限制可以被緩存的DirectByteBuffer的大小,對於超過這個限制的DirectByteBuffer不會被緩存到ThreadLocal的bufferCache中,這樣就能被GC正常回收掉。唯一的缺點是讀寫的性能會稍差一些,畢竟創建一個新的DirectByteBuffer的代價也不小,當然通過測試驗證對比分析,性能也沒有數量級的差別。

增加參數:

-XX:MaxDirectMemorySize=1600m
-Djdk.nio.maxCachedBufferSize=500000    ---注意不能帶單位

就是調整了-Djdk.nio.maxCachedBufferSize=500000(注意這裏是字節數,不能用mkg等單位

增加調整參數之後,運行一段時間,持續觀察整體DirectByteBuffer穩定控制在1.5G左右,性能也幾乎沒有衰減。一切恢復正常,再看監控看板沒有看到佔滿內存告警。

  

5、調整應用啓動參數配置

 

業務系統調整後的啓動命令參數如下:

 java

-javaagent:/home/agent/skywalking-agent.jar

-Dskywalking.agent.service_name=xx-crm

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=/tmp/xx-crm.hprof

-Dspring.profiles.active=prod

-server -Xms4608m -Xmx4608m -Xmn3072m

-XX:MetaspaceSize=300m

-XX:MaxMetaspaceSize=512m

-XX:CompressedClassSpaceSize=64m

-XX:MaxDirectMemorySize=1600m

-Djdk.nio.maxCachedBufferSize=500000

-jar /home/xx-crm.jar

 

6、總結

碰到這類非堆內存問題有兩種解決辦法:

1、如果允許的話減少IO線程數。

2、調整配置應用啓動參數,-Djdk.nio.maxCachedBufferSize=xxx 記住 -XX:MaxDirectMemorySize 參數也要配置上。

如果不配置 -XX:MaxDirectMemorySize 這個參數(最大直接內存),JVM就默認取-Xmx的值當作它的最大值(可能就會像我一樣遇到超配的情況)。

 

參考文章Troubleshooting Problems With Native (Off-Heap) Memory in Java Applications

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章