本文來自: PerfMa技術社區PerfMa(笨馬網絡)官網java
最近常常被問到一個問題,」爲何咱們系統進程佔用的物理內存(Res/Rss)會遠遠大於設置的Xmx值」,好比Xmx設置1.7G,可是top看到的Res的值卻達到了3.0G,隨着進程的運行,Res的值還在遞增,直到達到某個值,被OS當作bad process直接被kill掉了。linux
top - 16:57:47 up 73 days, 4:12, 8 users, load average: 6.78, 9.68, 13.31 Tasks: 130 total, 1 running, 123 sleeping, 6 stopped, 0 zombie Cpu(s): 89.9%us, 5.6%sy, 0.0%ni, 2.0%id, 0.7%wa, 0.7%hi, 1.2%si, 0.0%st ... PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 22753 admin 20 0 4252m 3.0g 17m S 192.8 52.7 151:47.59 /opt/app/java/bin/java -server -Xms1700m -Xmx1700m -Xmn680m -Xss256k -XX:PermSize=128m -XX:MaxPermSize=128m -XX:+UseStringCache -XX:+ 40 root 20 0 0 0 0 D 0.3 0.0 5:53.07 [kswapd0]
先說下Xmx,這個vm配置只包括咱們熟悉的新生代和老生代的最大值,不包括持久代,也不包括CodeCache,還有咱們常據說的堆外內存從名字上一看也知道沒有包括在內,固然還有其餘內存也不會算在內等,所以理論上咱們看到物理內存大於Xmx也是可能的,不過超過太多估計就可能有問題了。網絡
物理內存和虛擬內存間的映射關係
咱們知道os在內存上面的設計是花了心思的,爲了讓資源獲得最大合理利用,在物理內存之上搞一層虛擬地址,同一臺機器上每一個進程可訪問的虛擬地址空間大小都是同樣的,爲了屏蔽掉複雜的到物理內存的映射,該工做os直接作了,當須要物理內存的時候,當前虛擬地址又沒有映射到物理內存上的時候,就會發生缺頁中斷,由內核去爲之準備一塊物理內存,因此即便咱們分配了一塊1G的虛擬內存,物理內存上不必定有一塊1G的空間與之對應,那到底這塊虛擬內存塊到底映射了多少物理內存呢,這個咱們在linux下能夠經過/proc/<pid>/smaps這個文件看到,其中的Size表示虛擬內存大小,而Rss表示的是物理內存,因此從這層意義上來講和虛擬內存塊對應的物理內存塊不該該超過此虛擬內存塊的空間範圍併發
8dc00000-100000000 rwxp 00000000 00:00 0 Size: 1871872 kB Rss: 1798444 kB Pss: 1798444 kB Shared_Clean: 0 kB Shared_Dirty: 0 kB Private_Clean: 0 kB Private_Dirty: 1798444 kB Referenced: 1798392 kB Anonymous: 1798444 kB AnonHugePages: 0 kB Swap: 0 kB KernelPageSize: 4 kB MMUPageSize: 4 kB
通常來講連續分配的內存塊仍是有必定關係的,固然也不能徹底確定這種關係,這次爲了排查這個問題,我特意寫了個簡單的分析工具來分析這個問題,獲得的效果大體以下:app
固然這只是一個簡單的分析,後面咱們會挖掘更多的點出來,好比每一個內存塊是屬於哪塊memory pool,究竟是什麼地方分配的等(注:上面的第一條,其實就是new+old+perm對應的虛擬內存及其物理內存映射狀況
)。函數
當一個進程無端消失的時候,咱們通常看/var/log/message
裏是否有Out of memory: Kill process
關鍵字(若是是java進程咱們先看是否有crash日誌),若是有就說明是被os由於oom而被kill了:高併發
從上面咱們看到了一個堆棧,也就是內核裏選擇被kill進程的過程,這個過程會對進程進行一系列的計算,每一個進程都會給它們計算一個score,這個分數會記錄在/proc/<pid>/oom_score
裏,一般這個分數越高,就越危險,被kill的可能性就越大,下面將內核相關的代碼貼出來,有興趣的能夠看看,其中代碼註釋上也寫了挺多相關的東西了:工具
這是咱們查這個問題首先要想到的一個地方,是不是由於什麼地方不斷建立DirectByteBuffer對象,可是因爲沒有被回收致使了內存泄露呢,以前有篇文章已經詳細介紹了這種特殊對象,能夠看我以前發的文章《JVM源碼分析之堆外內存徹底解讀》,知道後臺到底綁定了多少堆外內存尚未被回收:源碼分析
對於動態庫裏頻繁分配的問題,主要得使用google的perftools工具了,該工具網上介紹挺多的,就不對其用法作詳細介紹了,經過該工具咱們能獲得native方法分配內存的狀況,該工具主要利用了unix的一個環境變量LD_PRELOAD,它容許你要加載的動態庫優先加載起來,至關於一個Hook了,因而能夠針對同一個函數能夠選擇不一樣的動態庫裏的實現了,好比googleperftools就是將malloc方法替換成了tcmalloc的實現,這樣就能夠跟蹤內存分配路徑了,獲得的效果相似以下:性能
從上面的輸出中咱們看到了zcalloc
函數總共分配了1616.3M的內存,還有Java_java_util_zip_Deflater_init
分配了1591.0M內存,deflateInit2_
分配了1590.5M,然而總共才分配了1670.0M內存,因此這幾個函數確定是調用者和被調用者的關係:
上述代碼也驗證了他們這種關係。
那如今的問題就是找出哪裏調用Java_java_util_zip_Deflater_init
了,從這方法的命名上知道它是一個java的native方法實現,對應的是java.util.zip.Deflater
這個類的init
方法,因此要知道init方法哪裏被調用了,跟蹤調用棧咱們會想到btrace工具,可是btrace是經過插樁的方式來實現的,對於native方法是沒法插樁的,因而咱們看調用它的地方,找到對應的方法,而後進行btrace腳本編寫:
import com.sun.btrace.annotations.*; import static com.sun.btrace.BTraceUtils.*; @BTrace public class Test { @OnMethod( clazz="java.util.zip.Deflater", method="<init>" ) public static void onnewThread(int i,boolean b) { jstack(); } }
因而跟蹤對應的進程,咱們能抓到調用Deflater構造函數的堆棧
從上面的堆棧咱們找出了調用java.util.zip.Deflate.init()
的地方
上面已經定位了具體的代碼了,因而再細緻跟蹤了下對應的代碼,其實並非代碼實現上的問題,而是代碼設計上沒有考慮到流量很大的場景,當流量很大的時候,無論本身系統是否能承受這麼大的壓力,都來者不拒,拿到數據就作deflate,而這個過程是須要分配堆外內存的,當量達到必定程度的時候此時會發生oom killer,另外咱們在分析過程當中發現其實物理內存是有降低的
這也就說明了其實代碼使用上並無錯,所以建議將deflate放到隊列裏去作,好比限制隊列大小是100,每次最多100個數據能夠被deflate,處理一個放進一個,以致於不會被活活撐死。
一塊兒來學習吧: