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