如何判斷內存是否泄露及何處泄露
內存泄露為什么不容易判斷
在性能問題里面,內存有沒有泄露?如果有泄露,是哪里泄露了?這兩個問題是非常難判斷和定位的。甚至有廠商對內存是否健康的評判就是:下次系統重啟之前,應用能正常運行。換句話說,有內存泄露找不出來也沒關系,定期重啟一下服務器,而且在生產環境,的確有些企業是這么干的。
CPU如果利用率異常,可以查哪個進程中的哪個函數占用CPU多,相應的,內存也可以查哪個進程占內存多。為什么內存問題不像CPU問題那么容易定位?
內存泄露是指使用內存完成后沒有釋放,內存增長并不能分辨增長出來的內存是進程真正要用的,還是進程泄露出來的。而CPU的占用是瞬時的、確定的,不存在某個進程申請了CPU占著不用的情況。
在一個討論組里,有人提問:對一個基于Java的Web系統進行壓力測試,如果虛擬用戶數從峰值下滑的同時,內存占用率卻保持在峰值不變,是否能得出Java程序存在內存泄露的問題?
回答是否定的,原因如下:
首先,內存占用率指的是什么內存的占用率?
在不同的OS上有不同的內存管理機制,比如AIX上,我們最關注是計算內存,但如果內存利用率指的是計算內存+非計算內存的話,即使內存占用率上升也說明不了太多問題。再比如Linux上,我們最關注是active內存,如果內存利用率指的是active+buffer+cache,即使內存占用率上升也說明不了太多問題。
第二,其他進程的干擾
操作系統上運行的進程千千萬,內存不下降,可能是其他應用/系統進程對內存的使用,應具體分辨是哪個進程占據了內存。因此考察是否有內存泄露應關注的是指定進程有沒有內存增長,這樣比較容易排除干擾。不過,查看進程的Data Segment也只能查看這個進程使用的一部分內存,而這個進程使用的Shared Memory Segment則不在這個指標中,但同樣需要關注(內存是分段的(Segment),每個段都是獨立的,有各自的度量讀數)
第三,進程池的原因
假如只關注計算內存(AIX),如果服務端的應用是一個100個進程的進程池,應用剛啟動的時候沒有客戶端的連接進來,因此沒有啟動任何進程,隨著客戶連接的增多,100個進程統統啟動,并常駐內存;再假如這些進程使用的內存是分配好不變的,那么內存占用率保持在峰值不變,是很正常的表現。
CICS里面也有進程常駐內存的概念。常駐內存后,進程不會掉下去,因此沒有創建、銷毀進程的開銷。
第四,JVM內存管理的原因
如果不是Java程序,內存不下降甚至內存上升,也有上述的多種原因,何況這是Java程序,存在一個JVM內存管理機制的原因。
我們曾經遇到這樣一個案例。對某Linux服務器上的某應用進行壓力測試,在一周的測試過程中,發現內存不斷增加。盡管服務器上每天定時清理內存(如下),但總趨勢仍然是內存增長。
備注:drop_caches是清理無用的cache,對于dirty狀態的是不清理的,直到dirty的內存被寫入磁盤。但如果用sync操作把dirty的內存flush到磁盤中,后續的drop_caches將釋放更多的內存。
后續我們就發現,這是JVM內存管理機制造成的內存泄露假象。該系統在測試過程中Java full GC(全量垃圾回收)沒有被調起,老年代的內存沒法被釋放。雖然應用使用的內存并沒有超過JVM設定的heap大小,但從Linux內存監控的指標上看,active內存是不斷增加的。
為什么full GC沒有被調起呢?這個場景下,老年代內存的增長會非常緩慢,幾天內都不會達到觸發full GC的標準,以致出現內存使用量不斷增長不回收的現象。并且,這個Java應用是一個獨立的Java程序,并沒有運行在應用中間件上,因此沒有中間件幫它做合理的GC策略,而應用本身也沒有去調起full GC。
后經調整應用,主動調起full GC,內存增長問題得到解決。
第五,本應用其他邏輯的干擾
也許這個應用是個接收客戶端數據報送并進行ETL處理的程序,服務端的應用在收到客戶端的數據后,開始啟動其他進程/線程/模塊去做后續處理,后續處理需要分配內存。
繼續最初的問題,回到那個基于Java的Web系統,如果虛擬用戶數從峰值下滑到0,內存占用率卻繼續上揚,是否能得出Java程序存在內存泄露的問題?
回答仍然是否,可能的原因還是上面那幾條。
總而言之,內存泄露是非常難判斷的事,需要長時間的測試才能得到猜測性結論。
誰占用的內存多
首先找到哪個應用或哪個進程占用的內存多。
1、物理內存占用
Nmon sheet
根據經驗,nmon的top sheet- Memory by command最直觀,也最容易直接看出哪個進程消耗的內存資源多。
然后采用類似ps –ef| grep java這樣的命令查看這個進程具體是什么內容。
另外,有不少命令也可以看哪個進程占用物理內存多,但講真,經常用命令行去看,但經常看不出來什么結果。例如下圖,每個進程消耗的物理內存似乎差距不大,雖然這個例子中服務器上跑oracle這樣的系統軟件,內存都是oracle占的,但即使不跑oracle,這些進程的內存占用往往也差距不大。
Svmon
列出消耗物理內存前十的進程
svmon -Pt10 | perl -e 'while(<>){print if($.==2||$&&&!$s++);$.=0 if(/^-+$/)}'
Svmon里面的inuse指的是這個進程對物理內存的消耗,包括計算內存+非計算內存。其實非計算內存,我們一般是不做過多關注的,及時占用的多,也沒什么問題。
ps
- ps aux | head -1 ; ps aux | sort -rn +4 | head -10
按照占用物理內存的百分比排序,列出前十個進程。
Nmon command
- nmon --> t (top processes) --> 4 (order in process size)
2、Paging Space占用
用到Paging Space不一定說明這個進程占用內存多,很有很能是它被其他進程擠出來的。查出誰在用Paging Space,大概率是查出誰是受害者。
按照占用Paging Space的進程排序
- svmon -P -O sortseg=pgsp
檢查哪個進程引起的Paging到Paging Space(IBM script)。腳本發現po這個指標大于50的時候保存進程相關信息退出
Paging Space一旦為這個分頁分配了磁盤空間,就不會因為這個分頁換回物理內存而釋放,因此經常可以看到Paging Space的利用率不為0,但此時物理內存占用也不多。Paging Space的利用率不為0只能說明歷史上有物理內存不足的情況。
進一步關注指定進程是否有泄漏
在穩定性測試(也叫持久測試或疲勞測試)中,需要觀察內存是否有泄露。然而使用內存的進程千千萬,整個服務器的內存增長似乎也不能判斷某個進程的內存有泄露。因此在穩定性測試過程中往往需要全程關注指定進程的內存消耗,比如運行3天、7天。
查看內存使用情況的命令有ps、sar、svmon、vmstat等等,但本文并不從工具使用的角度來介紹,而是從性能測試中關注指標的角度來介紹。如果采用其他命令查看內存,需注意,相似的名字在不同命令當中的含義是不一樣的,一定要搞清楚這個字段的真正含義。
例1:Virtual這個詞,有時候在內存里面指Paging Space(換頁空間),有時指進程空間里面占用的所有分頁(包括物理內存和Paging Space中的分頁)。
例2:Nmon中的PgIn/PgOut、topas中的PageIn/PageOut是指對文件系統的換頁,而vmstat中的pi/po是對Paging Space的換頁,而topas P中進程的PAGE SPACE是指進程的Data Segment。
進程使用的數據段
1. 獲取來源
ps gv 進程號:SIZE(單位為KB)
svmon –P 進程號:work process private、work shared library data、text data BSS heap、USLA heap、application stack、private load data等segment之和(單位為4KB或64KB)
topas P中進程的PAGE SPACE(單位為4KB)
2. 指標說明
內存泄露指進程自己申請分配、使用了內存但沒有在使用完畢后釋放,大量的泄露會導致物理內存用滿,降低系統效率。
如何判斷一個進程有沒有內存泄露?AIX中使用ps gv命令觀察特定進程的SIZE指標,如果SIZE經過長時間測試后,不斷增長,則可能有內存泄露的嫌疑,這里說的是嫌疑,而不是一定。況且,查看進程的SIZE值也只能查看這個進程使用的一部分內存,而這個進程使用的Shared Memory Segment則不在這個指標中。如果是JAVA程序由于涉及到JVM的內存管理,問題就更難判斷的,我們先放下JAVA程序不表,單說C的程序。
SIZE在ps命令當中的解釋是The virtual size of the data section of the process (in 1KB units)。為什么看這個指標,則需要從進程空間開始說。
進程空間的內存可以分為三種類型:
1)數據段:Data Segment (Data + BSS + Heap)
2)棧:Stack
3)代碼段:Code Segment
棧(Stack):包括返回地址、自動分配的變量,都是一會兒有一會兒沒的,系統自動回收,不會造成內存泄露。
代碼段:進程跑起來肯定要有代碼,代碼基本上可以說是固定大小的,不會造成內存泄露。當然如果代碼太大,裝不進內存,那就另當別論了。
剩下的數據段就是進程自己分配、使用的分頁,數據段包括Data + BSS + Heap。
Data是已經初始化的全局和靜態變量。
BSS是未初始化的全局和靜態變量,比如static int i。
Heap(堆)是進程中malloc, realloc等函數申請的,需要free等函數釋放。
3. 舉例
ps gv命令中SIZE就是該進程數據段的virtual size(1KB為單位),這些分頁可能在物理內存中也可能在Paging Space中。
檢查SIZE列在長期的測試過程中是否有明顯的持續增長,如有,說明可能有內存泄露。
長期的抓取和后期的圖形化處理,可以寫腳本或代碼來實現
其他命令也可以看到這個值,以下圖為例“svmon –P 進程號”可以看work process private的virtual大小+work shared library data的virtual大小。如果有text data BSS heap、USLA heap、application stack、private load data等segment,還需把這些segment也加上。由于svmon中統計的segment較多,因此不推薦采用svmon統計Data Segment。
二者的單位是4K的分頁。(169+85)4=2544=1016,與ps v得到的SIZE值相同。
解釋一下為什么單位是4K,svmon的輸出結果中,work process private和work shared library data的PSize(Page Size)類型是sm。而這臺機器上命令svmon顯示,只有s和m兩個類型,分別對應4KB和64KB。而sm這個類型是什么呢?
AIX上面進程空間的虛擬內存分頁默認的頁大小是4K,但POWER5+以上的處理器支持4種頁大小,分別是4KB(small),64KB(medium),16MB(large)和16GB(huge),POWER6處理器開始支持4K和64K的混合形式,即一個segment里面既有4K分頁,也有64K分頁,當需要大塊內存、需要提升性能的時候用64K分頁,當64K分頁可能會浪費內存的時候則用4K分頁。svmon命令需要告訴用戶這個segment有多少內存,為了統計時的方便,就只用一個單位來度量,這個單位就是sm中的***個字母s(small),對應的度量單位是4K,這個條記錄后面的列中的數據都依據這個度量單位出具。
另外解釋一下inuse和virtual。Svmon里面的inuse指的是這個進程對物理內存的消耗,包括計算內存+非計算內存。而virtual指的是進程空間里面的分頁,這個分頁也許在物理內存,也許在Paging Space。假如說一個進程使用的分頁都在物理內存的話,inuse>=virtual,因為此時inuse里面有文件緩存,而virtual里面沒有文件緩存,文件緩存是操作系統給緩存的,和進程空間沒關系。
也可以Topas,敲擊P,看指定進程的PAGE SPACE,也是254,254*4=1016,與ps v得到的SIZE值相同。
這里的PAGE SPACE的單位是4KB,比較好查,只要man topas就可以找到這一段:PAGE SPACE:The virtual working set size used by process (4 KB pages)
有人會問,為什么不從nmon里面取值?nmon的TOP Sheet里面的進程也有SIZE等內存的指標,但nmon的TOP Sheet中只列出占CPU比較多的N個進程,如果被監控的進程占CPU很低,就不會出現在TOP Sheet中。或者一開始被監控進程占CPU較多,后來由于它占用的CPU減少而從TOP sheet中移除了,那么我們并不知道這個進程是銷毀了還是CPU利用率太低了。
進程使用的共享內存段
除了Data Segment可以造成內存泄露,如果進程分配共享內存,也可能造成內存泄露。
共享內存是某個進程分配,其他進程可以訪問的內存段,共享內存會映射到每個進程的地址空間。
那么如何查看指定進程消耗的共享內存呢?
從進程空間的角度看,共享內存段在這里
首先,還是要介紹概念,共享內存在AIX上可能有兩種內存段:shared memory segment和memory mapped segment。為什么有兩種段呢?這是由于程序調用了不同的實現接口導致的(System V Shared Memory services (shmat) 和BSD Memory Mapped Services)。BSD Memory Mapped Services可以將文件直接映射到內存而省去中間的buffer,但它也可以用來創建共享內存,它創建出來的共享內存段就是memory mapped segment。
查看指定進程消耗的共享內存的具體方法如下:
1. 獲取來源
32位程序
svmon -P進程號| egrep "Vsid|shared memory|mmap maps"
64位程序
svmon -P進程號| egrep "Vsid|shmat/mmap"
查看virtual字段
2. 指標說明
如果查出來某個內存段是memory mapped segment類型的,這個segment里面是不是共享內存,需要用ipcs –mS輔助判斷(因為memory mapped segment也可能是文件直接映射內存)。ipcs查到的都是共享內存,因此可以通過svmon –P中這個段的Vsid( virtual segment ID)在ipcs中查找有沒有這個段號。
這里沒有現成的例子,只是截個示意圖
ipcs –mS看到的是所有共享內存段,而沒有段的大小。如果看段的大小可以加-b選項。需注意,-b選項看到的是可分配的***值,而不是已分配的。
SEGSZ的單位是Byte。-b列出共享內存段以及消息、信號量的***值,也就是可分配的***值,而不是已分配的(數據庫程序除外)。-m列出活動的共享內存段。