性能測試總結之內存泄露和內存溢出

注:本文轉自http://www.uml.org.cn/Test/200912106.asp

剛剛做完了一個項目的性能測試,“有幸”也遇到了內存泄露的案例,所以在此和大家分享一下。

主要從以下幾部分來說明,關於內存和內存泄露、溢出的概念,區分內存泄露和內存溢出;內存的區域劃分,瞭解GC回收機制;重點關注如何去監控和發現內存問題;此外分析出問題還要如何解決內存問題。

下面就開始本篇的內容:

第一部分 概念

衆所周知,java中的內存java虛擬機自己去管理的,他不想C++需要自己去釋放。籠統地去講,java的內存分配分爲兩個部分,一個是數據堆,一個是棧。程序在運行的時候一般分配數據堆,把局部的臨時的變量都放進去,生命週期和進程有關係。但是如果程序員聲明瞭static的變量,就直接在棧中運行的,進程銷燬了,不一定會銷燬static變量。

另外爲了保證java內存不會溢出,java中有垃圾回收機制。 System.gc()即垃圾收集機制是指jvm用於釋放那些不再使用的對象所佔用的內存。java語言並不要求jvm有gc,也沒有規定gc如何工作。垃圾收集的目的在於清除不再使用的對象。gc通過確定對象是否被活動對象引用來確定是否收集該對象。

而其中,內存溢出就是你要求分配的java虛擬機內存超出了系統能給你的,系統不能滿足需求,於是產生溢出。

內存泄漏是指你向系統申請分配內存進行使用(new),可是使用完了以後卻不歸還(delete),結果你申請到的那塊內存你自己也不能再訪問,該塊已分配出來的內存也無法再使用,隨着服務器內存的不斷消耗,而無法使用的內存越來越多,系統也不能再次將它分配給需要的程序,產生泄露。一直下去,程序也逐漸無內存使用,就會溢出。

第二部分 原理

JAVA垃圾回收及對內存區劃分

在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虛擬機自身使用的相對比較穩定的數據對象,比如類和對象方法等。

關於代的劃分,可以從下圖中獲得一個概況:

JVM

如果垃圾回收器影響了系統的性能,或者成爲系統的瓶頸,你可以通過自定義各個代的大小來優化它的性能。使用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回收,這樣在一定程度上是可以避免內存溢出問題的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章