進程物理內存遠大於Xmx的問題分析

問題描述
最近經常被問到一個問題,”為什麼我們係統進程占用的物理內存(Res/Rss)會遠遠大於設置的Xmx值”,比如Xmx設置1.7G,但是top看到的Res的值卻達到了3.0G,隨著進程的運行,Res的值還在遞增,直到達到某個值,被OS當做bad process直接被kill掉了。
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可能嗎
先說下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
一般來說連續分配的內存塊還是有一定關係的,當然也不能完全肯定這種關係,此次為了排查這個問題,我特地寫了個簡單的分析工具來分析這個問題,得到的效果大致如下:
當然這隻是一個簡單的分析,後麵我們會挖掘更多的點出來,比如每個內存塊是屬於哪塊memory pool,到底是什麼地方分配的等(注:上麵的第一條,其實就是new+old+perm對應的虛擬內存及其物理內存映射情況
)。
進程滿足什麼條件會被os因為oom而被kill
當一個進程無故消失的時候,我們一般看/var/log/message
裏是否有Out of memory: Kill process
關鍵字(如果是java進程我們先看是否有crash日誌),如果有就說明是被os因為oom而被kill了:
從上麵我們看到了一個堆棧,也就是內核裏選擇被kill進程的過程,這個過程會對進程進行一係列的計算,每個進程都會給它們計算一個score,這個分數會記錄在/proc/<pid>/oom_score
裏,通常這個分數越高,就越危險,被kill的可能性就越大,下麵將內核相關的代碼貼出來,有興趣的可以看看,其中代碼注釋上也寫了挺多相關的東西了:
物理內存到底去哪了?
DirectByteBuffer冰山對象?
這是我們查這個問題首先要想到的一個地方,是否是因為什麼地方不斷創建DirectByteBuffer對象,但是由於沒有被回收導致了內存泄露呢,之前有篇文章已經詳細介紹了這種特殊對象,可以看我之前發的文章《JVM源碼分析之堆外內存完全解讀》,對阿裏內部的童鞋,可以直接使用zprofiler的heap視圖裏的堆外內存分析功能拿到統計結果,知道後台到底綁定了多少堆外內存還沒有被回收:
某個動態庫裏頻繁分配?
對於動態庫裏頻繁分配的問題,主要得使用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,處理一個放進一個,以至於不會被活活撐死。
最後更新:2017-04-11 19:32:01