剛剛作完了一個項目的性能測試,「有幸」也遇到了內存泄露的案例,因此在此和你們分享一下。html
主要從如下幾部分來講明,關於內存和內存泄露、溢出的概念,區份內存泄露和內存溢出;內存的區域劃分,瞭解GC回收機制;重點關注如何去監控和發現內存問題;此外分析出問題還要如何解決內存問題。java
下面就開始本篇的內容:程序員
第一部分 概念算法
衆所周知,java中的內存java虛擬機本身去管理的,他不想C++須要本身去釋放。籠統地去講,java的內存分配分爲兩個部分,一個是數據堆,一個是棧。程序在運行的時候通常分配數據堆,把局部的臨時的變量都放進去,生命週期和進程有關係。可是若是程序員聲明瞭static的變量,就直接在棧中運行的,進程銷燬了,不必定會銷燬static變量。緩存
另外爲了保證java內存不會溢出,java中有垃圾回收機制。 System.gc()即垃圾收集機制是指jvm用於釋放那些再也不使用的對象所佔用的內存。java語言並不要求jvm有gc,也沒有規定gc如何工做。垃圾收集的目的在於清除再也不使用的對象。gc經過肯定對象是否被活動對象引用來肯定是否收集該對象。服務器
而其中,內存溢出就是你要求分配的java虛擬機內存超出了系統能給你的,系統不能知足需求,因而產生溢出。架構
內存泄漏是指你向系統申請分配內存進行使用(new),但是使用完了之後卻不歸還(delete),結果你申請到的那塊內存你本身也不能再訪問,該塊已分配出來的內存也沒法再使用,隨着服務器內存的不斷消耗,而沒法使用的內存愈來愈多,系統也不能再次將它分配給須要的程序,產生泄露。一直下去,程序也逐漸無內存使用,就會溢出。dom
第二部分 原理異步
JAVA垃圾回收及對內存區劃分jvm
在Java虛擬機規範中,說起了以下幾種類型的內存空間:
◇ 棧內存(Stack):每一個線程私有的。
◇ 堆內存(Heap):全部線程公用的。
◇ 方法區(Method Area):有點像之前常說的「進程代碼段」,這裏面存放了每一個加載類的反射信息、類函數的代碼、編譯時常量等信息。
◇ 原生方法棧(Native Method Stack):主要用於JNI中的原生代碼,平時不多涉及。
而Java的使用的是堆內存,java堆是一個運行時數據區,類的實例(對象)從中分配空間。Java虛擬機(JVM)的堆中儲存着正在運行的應用程序所創建的全部對象,「垃圾回收」也是主要是和堆內存(Heap)有關。
垃圾回收的概念就是JAVA虛擬機(JVM)回收那些再也不被引用的對象內存的過程。通常咱們認爲正在被引用的對象狀態爲「alive」,而沒有被應用或者取不到引用屬性的對象狀態爲「dead」。垃圾回收是一個釋放處於」dead」狀態的對象的內存的過程。而垃圾回收的規則和算法被動態的做用於應用運行當中,自動回收。
JVM的垃圾回收器採用的是一種分代(generational )回收策略,用較高的頻率對年輕的對象(young generation)進行掃描和回收,這種叫作minor collection,而對老對象(old generation)的檢查回收頻率要低不少,稱爲major collection。這樣就不須要每次GC都將內存中全部對象都檢查一遍,這種策略有利於實時觀察和回收。
(Sun JVM 1.3 有兩種最基本的內存收集方式:一種稱爲copying或scavenge,將全部仍然生存的對象搬到另一塊內存後,整塊內存就可回收。這種方法有效率,但須要有必定的空閒內存,拷貝也有開銷。這種方法用於minor collection。另一種稱爲mark-compact,將活着的對象標記出來,而後搬遷到一塊兒連成大塊的內存,其餘內存就能夠回收了。這種方法不須要佔用額外的空間,但速度相對慢一些。這種方法用於major collection. )
一些對象被建立出來只是擁有短暫的生命週期,好比 iterators 和本地變量。
另一些對象被建立是擁有很長的生命週期,好比 高持久化對象等。
垃圾回收器的分代策略是把內存區劃分爲幾個代,而後爲每一個代分配一到多個內存區塊。當其中一個代用完了分配給他的內存後,JVM會在分配的內存區內執行一個局部的GC(也能夠叫minor collection)操做,爲了回收處於「dead」狀態的對象所佔用的內存。局部GC一般要不Full GC要快不少。
JVM定義了兩個代,年輕代(yong generation)(有時稱爲「nursery」託兒所)和老年代(old generation)。年輕代包括 「Eden space(伊甸園)」和兩個「survivor spaces」。虛擬內存初始化的時候會把全部對象都分配到 Eden space,而且大部分對象也會在該區域被釋放。 當進行 minor GC的時候,VM會把剩下的沒有釋放的對象從Eden space移動到其中一個survivor spaces當中。此外,VM也會把那些長期存活在survivor spaces 裏的對象移動到 老生代的「tenured」 space中。當 tenured generation 被填滿後,就會產生Full GC,Full GC會相對比較慢由於回收的內容包括了全部的 live狀態的對象。pemanet generation這個代包括了全部java虛擬機自身使用的相對比較穩定的數據對象,好比類和對象方法等。
關於代的劃分,能夠從下圖中得到一個概況:
若是垃圾回收器影響了系統的性能,或者成爲系統的瓶頸,你能夠經過自定義各個代的大小來優化它的性能。使用JConsole,能夠方便的查看到當前應用所配置的垃圾回收器的各個參數。想要得到更詳細的參數,能夠參考如下調優介紹:
Tuning Garbage collection with the 5.0 HotSpot VM
http://java.sun.com/docs/hotspot/gc/index.html
最後,總結一下各區內存:
Eden Space (heap): 內存最初從這個線程池分配給大部分對象。
Survivor Space (heap):用於保存在eden space內存池中通過垃圾回收後沒有被回收的對象。
Tenured Generation (heap):用於保持已經在 survivor space內存池中存在了一段時間的對象。
Permanent Generation (non-heap): 保存虛擬機本身的靜態(refective)數據,例如類(class)和方法(method)對象。Java虛擬機共享這些類數據。這個區域被分割爲只讀的和只寫的,
Code Cache (non-heap):HotSpot Java虛擬機包括一個用於編譯和保存本地代碼(native code)的內存,叫作「代碼緩存區」(code cache)
第三部分 監控(工具發現問題)
談到內存監控工具,JConsole是必需要介紹的,它是一個用JAVA寫的GUI程序,用來監控VM,並可監控遠程的VM,易用且功能強大。具體可監控JAVA內存、JAVA CPU使用率、線程執行狀況、加載類概況等,Jconsole須要在JVM參數中配置端口才能使用。
因爲是GUI程序,界面可視化,這裏就不作詳細介紹,
具體幫助支持文檔請參閱性能測試JConsole使用方法總結:
http://www.taobao.ali.com/chanpin/km/test/DocLib/性能測試輔助工具-JConsole的使用方法.aspx
或者參考SUN官網的技術文檔:
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
在實際測試某一個項目時,內存出現泄露現象。起初在性能測試的1個小時中,並不明顯,而在穩定性測試的時候才發現,應用的HSF調用在通過幾個小時運行後,就出現性能明顯降低的狀況。在服務日誌中報大量HSF超時,但所調用系統沒有任何超時日誌,而且壓力應用的load都很低。通過查看日誌後,認爲應用可能存在內存泄漏。經過jconsole 以及 jmap 工具進行分析發現,確實存在內存泄漏問題,其中PS Old Gen最終達到佔用 100%的佔用。如圖所示:
從上圖能夠看到,雖然每次Full GC,JVM內存會有部分回收,但回收並不完全,不可回收的內存對象會愈來愈多,這樣便會出現以上的一個趨勢。在Full GC沒法回收的對象愈來愈多時,最終已使用內存達到系統分配的內存最大值,系統最後無內存可分配,最終down機。
第四部分 分析
通過開發和架構師對應用的分析,查看此時內存隊列,看哪一個對象佔用數據最多,再利用jmap命令,對線程數據分析,以下所示:
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
前三個instances不斷增長,指代的是同一個代碼邏輯,異步分發的問題,堵塞消息,回收屢次都沒法回收成功。致使內存溢出。
此外,對應用的性能單獨作了壓測,他的性能只能支撐到一半左右,故發送消息的TPS,應用確定沒法處理過來,致使消息堆積,而JAVA垃圾回收期認爲這些都是有用的對象,致使內存堆積,直至系統崩潰。
調優方法
因爲具體調優方法涉及到應用的配置信息,故在此暫不列出,能夠參考性能測試小組發佈的《性能測試調優寶典》
第四部分 總結
內存溢出主要是因爲代碼編寫時對某些方法、類應用不合理,或者沒有預估到臨時對象會佔用很大內存量,或者把過多的數據放入JVM緩存,或者性能壓力大致使消息堆積而佔用內存,以致於在性能測試時,生成龐大數量的臨時對象,GC時沒有作出有效回收甚至根本就不能回收,形成內存空間不足,內存溢出。
若是編碼以前,對內存使用量進行預估,對放在內存中的數據進行評估,保證有用的信息儘快釋放,無用的信息可以被GC回收,這樣在必定程度上是能夠避免內存溢出問題的。