剛剛做完了一個(gè)項(xiàng)目的性能測(cè)試,“有幸”也遇到了內(nèi)存泄露的案例,所以在此和大家分享一下。
主要從以下幾部分來(lái)說(shuō)明,關(guān)于內(nèi)存和內(nèi)存泄露、溢出的概念,區(qū)分內(nèi)存泄露和內(nèi)存溢出;內(nèi)存的區(qū)域劃分,了解GC回收機(jī)制;重點(diǎn)關(guān)注如何去監(jiān)控和發(fā)現(xiàn)內(nèi)存問(wèn)題;此外分析出問(wèn)題還要如何解決內(nèi)存問(wèn)題。
下面就開(kāi)始本篇的內(nèi)容:
第一部分 概念
眾所周知,java中的內(nèi)存java虛擬機(jī)自己去管理的,他不想C++需要自己去釋放?;\統(tǒng)地去講,java的內(nèi)存分配分為兩個(gè)部分,一個(gè)是數(shù)據(jù)堆,一個(gè)是棧。程序在運(yùn)行的時(shí)候一般分配數(shù)據(jù)堆,把局部的臨時(shí)的變量都放進(jìn)去,生命周期和進(jìn)程有關(guān)系。但是如果程序員聲明了static的變量,就直接在棧中運(yùn)行的,進(jìn)程銷(xiāo)毀了,不一定會(huì)銷(xiāo)毀static變量。
另外為了保證java內(nèi)存不會(huì)溢出,java中有垃圾回收機(jī)制。 System.gc()即垃圾收集機(jī)制是指jvm用于釋放那些不再使用的對(duì)象所占用的內(nèi)存。java語(yǔ)言并不要求jvm有g(shù)c,也沒(méi)有規(guī)定gc如何工作。垃圾收集的目的在于清除不再使用的對(duì)象。gc通過(guò)確定對(duì)象是否被活動(dòng)對(duì)象引用來(lái)確定是否收集該對(duì)象。
而其中,內(nèi)存溢出就是你要求分配的java虛擬機(jī)內(nèi)存超出了系統(tǒng)能給你的,系統(tǒng)不能滿(mǎn)足需求,于是產(chǎn)生溢出。
內(nèi)存泄漏是指你向系統(tǒng)申請(qǐng)分配內(nèi)存進(jìn)行使用(new),可是使用完了以后卻不歸還(delete),結(jié)果你申請(qǐng)到的那塊內(nèi)存你自己也不能再訪問(wèn),該塊已分配出來(lái)的內(nèi)存也無(wú)法再使用,隨著服務(wù)器內(nèi)存的不斷消耗,而無(wú)法使用的內(nèi)存越來(lái)越多,系統(tǒng)也不能再次將它分配給需要的程序,產(chǎn)生泄露。一直下去,程序也逐漸無(wú)內(nèi)存使用,就會(huì)溢出。
第二部分 原理
JAVA垃圾回收及對(duì)內(nèi)存區(qū)劃分
在Java虛擬機(jī)規(guī)范中,提及了如下幾種類(lèi)型的內(nèi)存空間:
◇ 棧內(nèi)存(Stack):每個(gè)線程私有的。
◇ 堆內(nèi)存(Heap):所有線程公用的。
◇ 方法區(qū)(Method Area):有點(diǎn)像以前常說(shuō)的“進(jìn)程代碼段”,這里面存放了每個(gè)加載類(lèi)的反射信息、類(lèi)函數(shù)的代碼、編譯時(shí)常量等信息。
◇ 原生方法棧(Native Method Stack):主要用于JNI中的原生代碼,平時(shí)很少涉及。
而Java的使用的是堆內(nèi)存,java堆是一個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū),類(lèi)的實(shí)例(對(duì)象)從中分配空間。Java虛擬機(jī)(JVM)的堆中儲(chǔ)存著正在運(yùn)行的應(yīng)用程序所建立的所有對(duì)象,“垃圾回收”也是主要是和堆內(nèi)存(Heap)有關(guān)。
垃圾回收的概念就是JAVA虛擬機(jī)(JVM)回收那些不再被引用的對(duì)象內(nèi)存的過(guò)程。一般我們認(rèn)為正在被引用的對(duì)象狀態(tài)為“alive”,而沒(méi)有被應(yīng)用或者取不到引用屬性的對(duì)象狀態(tài)為“dead”。垃圾回收是一個(gè)釋放處于”dead”狀態(tài)的對(duì)象的內(nèi)存的過(guò)程。而垃圾回收的規(guī)則和算法被動(dòng)態(tài)的作用于應(yīng)用運(yùn)行當(dāng)中,自動(dòng)回收。
JVM的垃圾回收器采用的是一種分代(generational )回收策略,用較高的頻率對(duì)年輕的對(duì)象(young generation)進(jìn)行掃描和回收,這種叫做minor collection,而對(duì)老對(duì)象(old generation)的檢查回收頻率要低很多,稱(chēng)為major collection。這樣就不需要每次GC都將內(nèi)存中所有對(duì)象都檢查一遍,這種策略有利于實(shí)時(shí)觀察和回收。
(Sun JVM 1.3 有兩種最基本的內(nèi)存收集方式:一種稱(chēng)為copying或scavenge,將所有仍然生存的對(duì)象搬到另外一塊內(nèi)存后,整塊內(nèi)存就可回收。這種方法有效率,但需要有一定的空閑內(nèi)存,拷貝也有開(kāi)銷(xiāo)。這種方法用于minor collection。另外一種稱(chēng)為mark-compact,將活著的對(duì)象標(biāo)記出來(lái),然后搬遷到一起連成大塊的內(nèi)存,其他內(nèi)存就可以回收了。這種方法不需要占用額外的空間,但速度相對(duì)慢一些。這種方法用于major collection. )
一些對(duì)象被創(chuàng)建出來(lái)只是擁有短暫的生命周期,比如 iterators 和本地變量。
另外一些對(duì)象被創(chuàng)建是擁有很長(zhǎng)的生命周期,比如 高持久化對(duì)象等。
垃圾回收器的分代策略是把內(nèi)存區(qū)劃分為幾個(gè)代,然后為每個(gè)代分配一到多個(gè)內(nèi)存區(qū)塊。當(dāng)其中一個(gè)代用完了分配給他的內(nèi)存后,JVM會(huì)在分配的內(nèi)存區(qū)內(nèi)執(zhí)行一個(gè)局部的GC(也可以叫minor collection)操作,為了回收處于“dead”狀態(tài)的對(duì)象所占用的內(nèi)存。局部GC通常要不Full GC要快很多。
JVM定義了兩個(gè)代,年輕代(yong generation)(有時(shí)稱(chēng)為“nursery”托兒所)和老年代(old generation)。年輕代包括 “Eden space(伊甸園)”和兩個(gè)“survivor spaces”。虛擬內(nèi)存初始化的時(shí)候會(huì)把所有對(duì)象都分配到 Eden space,并且大部分對(duì)象也會(huì)在該區(qū)域被釋放。 當(dāng)進(jìn)行 minor GC的時(shí)候,VM會(huì)把剩下的沒(méi)有釋放的對(duì)象從Eden space移動(dòng)到其中一個(gè)survivor spaces當(dāng)中。此外,VM也會(huì)把那些長(zhǎng)期存活在survivor spaces 里的對(duì)象移動(dòng)到 老生代的“tenured” space中。當(dāng) tenured generation 被填滿(mǎn)后,就會(huì)產(chǎn)生Full GC,F(xiàn)ull GC會(huì)相對(duì)比較慢因?yàn)榛厥盏膬?nèi)容包括了所有的 live狀態(tài)的對(duì)象。pemanet generation這個(gè)代包括了所有java虛擬機(jī)自身使用的相對(duì)比較穩(wěn)定的數(shù)據(jù)對(duì)象,比如類(lèi)和對(duì)象方法等。
關(guān)于代的劃分,可以從下圖中獲得一個(gè)概況:
如果垃圾回收器影響了系統(tǒng)的性能,或者成為系統(tǒng)的瓶頸,你可以通過(guò)自定義各個(gè)代的大小來(lái)優(yōu)化它的性能。使用JConsole,可以方便的查看到當(dāng)前應(yīng)用所配置的垃圾回收器的各個(gè)參數(shù)。想要獲得更詳細(xì)的參數(shù),可以參考以下調(diào)優(yōu)介紹:
Tuning Garbage collection with the 5.0 HotSpot VM
http://java.sun.com/docs/hotspot/gc/index.html
最后,總結(jié)一下各區(qū)內(nèi)存:
Eden Space (heap): 內(nèi)存最初從這個(gè)線程池分配給大部分對(duì)象。
Survivor Space (heap):用于保存在eden space內(nèi)存池中經(jīng)過(guò)垃圾回收后沒(méi)有被回收的對(duì)象。
Tenured Generation (heap):用于保持已經(jīng)在 survivor space內(nèi)存池中存在了一段時(shí)間的對(duì)象。
Permanent Generation (non-heap): 保存虛擬機(jī)自己的靜態(tài)(refective)數(shù)據(jù),例如類(lèi)(class)和方法(method)對(duì)象。Java虛擬機(jī)共享這些類(lèi)數(shù)據(jù)。這個(gè)區(qū)域被分割為只讀的和只寫(xiě)的,
Code Cache (non-heap):HotSpot Java虛擬機(jī)包括一個(gè)用于編譯和保存本地代碼(native code)的內(nèi)存,叫做“代碼緩存區(qū)”(code cache)
第三部分 監(jiān)控(工具發(fā)現(xiàn)問(wèn)題)
談到內(nèi)存監(jiān)控工具,JConsole是必須要介紹的,它是一個(gè)用JAVA寫(xiě)的GUI程序,用來(lái)監(jiān)控VM,并可監(jiān)控遠(yuǎn)程的VM,易用且功能強(qiáng)大。具體可監(jiān)控JAVA內(nèi)存、JAVA CPU使用率、線程執(zhí)行情況、加載類(lèi)概況等,Jconsole需要在JVM參數(shù)中配置端口才能使用。
由于是GUI程序,界面可視化,這里就不做詳細(xì)介紹,
具體幫助支持文檔請(qǐng)參閱性能測(cè)試JConsole使用方法總結(jié):
http://www.taobao.ali.com/chanpin/km/test/DocLib/性能測(cè)試輔助工具-JConsole的使用方法.aspx
或者參考SUN官網(wǎng)的技術(shù)文檔:
http://Java.sun.com/j2se/1.5.0/docs/guide/management/jconsole.html
http://Java.sun.com/javase/6/docs/technotes/tools/share/jconsole.html
在實(shí)際測(cè)試某一個(gè)項(xiàng)目時(shí),內(nèi)存出現(xiàn)泄露現(xiàn)象。起初在性能測(cè)試的1個(gè)小時(shí)中,并不明顯,而在穩(wěn)定性測(cè)試的時(shí)候才發(fā)現(xiàn),應(yīng)用的HSF調(diào)用在經(jīng)過(guò)幾個(gè)小時(shí)運(yùn)行后,就出現(xiàn)性能明顯下降的情況。在服務(wù)日志中報(bào)大量HSF超時(shí),但所調(diào)用系統(tǒng)沒(méi)有任何超時(shí)日志,并且壓力應(yīng)用的load都很低。經(jīng)過(guò)查看日志后,認(rèn)為應(yīng)用可能存在內(nèi)存泄漏。通過(guò)jconsole 以及 jmap 工具進(jìn)行分析發(fā)現(xiàn),確實(shí)存在內(nèi)存泄漏問(wèn)題,其中PS Old Gen最終達(dá)到占用 100%的占用。如圖所示:
從上圖可以看到,雖然每次Full GC,JVM內(nèi)存會(huì)有部分回收,但回收并不徹底,不可回收的內(nèi)存對(duì)象會(huì)越來(lái)越多,這樣便會(huì)出現(xiàn)以上的一個(gè)趨勢(shì)。在Full GC無(wú)法回收的對(duì)象越來(lái)越多時(shí),最終已使用內(nèi)存達(dá)到系統(tǒng)分配的內(nèi)存最大值,系統(tǒng)最后無(wú)內(nèi)存可分配,最終down機(jī)。
第四部分 分析
經(jīng)過(guò)開(kāi)發(fā)和架構(gòu)師對(duì)應(yīng)用的分析,查看此時(shí)內(nèi)存隊(duì)列,看哪個(gè)對(duì)象占用數(shù)據(jù)最多,再利用jmap命令,對(duì)線程數(shù)據(jù)分析,如下所示:
num #instances #bytes class name
———————————————-
1: 9248056 665860032 com.taobao.matrix.mc.domain.**
2: 9248031 295936992 com.taobao.matrix.**
3: 9248068 147969088 java.util.**
4: 1542111 37010664 java.util.Date
前三個(gè)instances不斷增加,指代的是同一個(gè)代碼邏輯,異步分發(fā)的問(wèn)題,堵塞消息,回收多次都無(wú)法回收成功。導(dǎo)致內(nèi)存溢出。
此外,對(duì)應(yīng)用的性能單獨(dú)做了壓測(cè),他的性能只能支撐到一半左右,故發(fā)送消息的TPS,應(yīng)用肯定無(wú)法處理過(guò)來(lái),導(dǎo)致消息堆積,而JAVA垃圾回收期認(rèn)為這些都是有用的對(duì)象,導(dǎo)致內(nèi)存堆積,直至系統(tǒng)崩潰。
調(diào)優(yōu)方法
由于具體調(diào)優(yōu)方法涉及到應(yīng)用的配置信息,故在此暫不列出,可以參考性能測(cè)試小組發(fā)布的《性能測(cè)試調(diào)優(yōu)寶典》
第四部分 總結(jié)
內(nèi)存溢出主要是由于代碼編寫(xiě)時(shí)對(duì)某些方法、類(lèi)應(yīng)用不合理,或者沒(méi)有預(yù)估到臨時(shí)對(duì)象會(huì)占用很大內(nèi)存量,或者把過(guò)多的數(shù)據(jù)放入JVM緩存,或者性能壓力大導(dǎo)致消息堆積而占用內(nèi)存,以至于在性能測(cè)試時(shí),生成龐大數(shù)量的臨時(shí)對(duì)象,GC時(shí)沒(méi)有做出有效回收甚至根本就不能回收,造成內(nèi)存空間不足,內(nèi)存溢出。
如果編碼之前,對(duì)內(nèi)存使用量進(jìn)行預(yù)估,對(duì)放在內(nèi)存中的數(shù)據(jù)進(jìn)行評(píng)估,保證有用的信息盡快釋放,無(wú)用的信息能夠被GC回收,這樣在一定程度上是可以避免內(nèi)存溢出問(wèn)題的。
聯(lián)系客服