天天看點

一次記憶體洩露問題的排查

系統對外提供的Solr查詢接口,在來自外部調用的壓力加大之後,就會出現solr查詢報Read

Timed Out的異常,從表面現象上看是此時solr核壓力過大,無法響應過多的查詢請求。

但實際上此時并發查詢壓力并不是很大,那麼為何solr核會無法及時響應查詢請求呢?首先用top檢視了下load

average,也是很低,也佐證了系統本身壓力并不大。

然後,用jstack –l <pid>

檢視那些cpu使用率過高的線程,發現全都是GC線程,說明GC過于頻繁,而且耗時過長,導緻應用線程被挂起,無法響應用戶端發來的請求,這種情況就應該是有存在記憶體洩露的問題咯。

一次記憶體洩露問題的排查

于是,就用jmap将程序的堆轉儲檔案dump出來到heap.bin檔案中

JMap -dump:format=b,file=/tmp/heap.bin <pid>

然後用Eclipse Memory Analyzer(MAT)打開堆轉儲檔案進行分析

通常我們都會采用下面的“三步曲”來分析記憶體洩露問題:

首先,對問題發生時刻的系統記憶體狀态擷取一個整體印象。

第二步,找到最有可能導緻記憶體洩露的元兇,通常也就是消耗記憶體最多的對象

    接下來,進一步去檢視這個記憶體消耗大戶的具體情況,看看是否有什麼異常的行為。

下面将用一個基本的例子來展示如何采用“三步曲”來檢視生産的分析報告。

一次記憶體洩露問題的排查

如上圖所示,在報告上最醒目的就是一張簡潔明了的餅圖,從圖上我們可以清晰地看到一個可疑對象消耗了系統75%

的記憶體。

現在,讓我們開始真正的尋找記憶體洩露之旅,點選“Leak  Suspects”連結,可以看到如下圖所示對可疑對象的詳細分析報告。

一次記憶體洩露問題的排查

我們檢視下從 GC

根元素到記憶體消耗聚集點的最短路徑

一次記憶體洩露問題的排查

我們可以很清楚的看到整個引用鍊,記憶體聚集點是一個擁有大量對象的清單,如果你對代碼比較熟悉的話,相信這些資訊應該能給你提供一些找到記憶體洩露的思路了。

接下來,我們再繼續看看,這個對象集合裡到底存放了什麼,為什麼會消耗掉如此多的記憶體。

一次記憶體洩露問題的排查

在這張圖上,我們可以清楚的看到,這個清單中儲存了大量 HashMap

對象的引用,就是它導緻的記憶體洩露。

至此,我們已經擁有了足夠的資訊去尋找洩露點,回到代碼中就發現,List沒有clear或者設定為null,導緻其包含的強引用的各個HashMap沒有得到釋放。至此,問題得到解決。

下面我們來繼續深入研究java的記憶體洩露問題。Java的一個重要優點就是通過垃圾收集器(Garbage

Collection,GC)自動管理記憶體的回收,程式員不需要通過調用函數來釋放記憶體。是以,很多程式員認為Java不存在記憶體洩漏問題,或者認為即使有記憶體洩漏也不是程式的責任,而是GC或JVM的問題。其實,這種想法是不正确的,因為Java也存在記憶體洩露,但它的表現與C++不同。

随着越來越多的伺服器程式采用Java技術,例如JSP,Servlet,

EJB等,伺服器程式往往長期運作。另外,在很多嵌入式系統中,記憶體的總量非常有限。記憶體洩露問題也就變得十分關鍵,即使每次運作少量洩漏,長期運作之後,系統也是面臨崩潰的危險。

為了判斷Java中是否有記憶體洩露,我們首先必須了解Java是如何管理記憶體的。Java的記憶體管理就是對象的配置設定和釋放問題。在Java中,程式員需要通過關鍵字new為每個對象申請記憶體空間

(基本類型除外),所有的對象都在堆

(Heap)中配置設定空間。另外,對象的釋放是由GC決定和執行的。在Java中,記憶體的配置設定是由程式完成的,而記憶體的釋放是有GC完成的,這種收支兩條線的方法确實簡化了程式員的工作。但同時,它也加重了JVM的工作。這也是Java程式運作速度較慢的原因之一。因為,GC為了能夠正确釋放對象,GC必須監控每一個對象的運作狀态,包括對象的申請、引用、被引用、指派等,GC都需要進行監控。

監視對象狀态是為了更加準确地、及時地釋放對象,而釋放對象的根本原則就是該對象不再被引用。

為了更好了解GC的工作原理,我們可以将對象考慮為有向圖的頂點,将引用關系考慮為圖的有向邊,有向邊從引用者指向被引對象。另外,每個線程對象可以作為一個圖的起始頂點,例如大多程式從main程序開始執行,那麼該圖就是以main程序頂點開始的一棵根樹。在這個有向圖中,根頂點可達的對象都是有效對象,GC将不回收這些對象。如果某個對象

(連通子圖)與這個根頂點不可達(注意,該圖為有向圖),那麼我們認為這個(這些)對象不再被引用,可以被GC回收。

以下,我們舉一個例子說明如何用有向圖表示記憶體管理。對于程式的每一個時刻,我們都有一個有向圖表示JVM的記憶體配置設定情況。以下右圖,就是左邊程式運作到第6行的示意圖。

一次記憶體洩露問題的排查

Java使用有向圖的方式進行記憶體管理,可以消除引用循環的問題,例如有三個對象,互相引用,隻要它們和根程序不可達的,那麼GC也是可以回收它們的。這種方式的優點是管理記憶體的精度很高,但是效率較低。另外一種常用的記憶體管理技術是使用計數器,例如COM模型采用計數器方式管理構件,它與有向圖相比,精度行低(很難處理循環引用的問題),但執行效率很高。

下面,我們就可以描述什麼是記憶體洩漏。在Java中,記憶體洩漏就是存在一些被配置設定的對象,這些對象有下面兩個特點,首先,這些對象是可達的,即在有向圖中,存在通路可以與其相連;其次,這些對象是無用的,即程式以後不會再使用這些對象。如果對象滿足這兩個條件,這些對象就可以判定為Java中的記憶體洩漏,這些對象不會被GC所回收,然而它卻占用記憶體。

在C++中,記憶體洩漏的範圍更大一些。有些對象被配置設定了記憶體空間,然後卻不可達,由于C++中沒有GC,這些記憶體将永遠收不回來。在Java中,這些不可達的對象都由GC負責回收,是以程式員不需要考慮這部分的記憶體洩露。

一次記憶體洩露問題的排查

是以,通過以上分析,我們知道在Java中也有記憶體洩漏,但範圍比C++要小一些。因為Java從語言上保證,任何對象都是可達的,所有的不可達對象都由GC管理。

對于程式員來說,GC基本是透明的,不可見的。雖然,我們隻有幾個函數可以通路GC,例如運作GC的函數System.gc(),但是根據Java語言規範定義,

該函數不保證JVM的垃圾收集器一定會執行。因為,不同的JVM實作者可能使用不同的算法管理GC。通常,GC的線程的優先級别較低。JVM調用GC的政策也有很多種,有的是記憶體使用到達一定程度時,GC才開始工作,也有定時執行的,有的是平緩執行GC,有的是中斷式執行GC。但通常來說,我們不需要關心這些。除非在一些特定的場合,GC的執行影響應用程式的性能,例如對于基于Web的實時系統,如網絡遊戲等,使用者不希望GC突然中斷應用程式執行而進行垃圾回收,那麼我們需要調整GC的參數,讓GC能夠通過平緩的方式釋放記憶體,例如将垃圾回收分解為一系列的小步驟執行,Sun提供的HotSpot

JVM就支援這一特性。

   下面給出了一個簡單的記憶體洩露的例子。在這個例子中,我們循環申請Object對象,并将所申請的對象放入一個Vector中,如果我們僅僅釋放引用本身,那麼Vector仍然引用該對象,是以這個對象對GC來說是不可回收的。是以,如果對象加入到Vector後,還必須從Vector中删除,最簡單的方法就是将Vector對象設定為null。

Vector v=new Vector(10); 

for (int i=1;i<100; i++)

{

    Object o=new Object();

    v.add(o);

    o=null;    

}

作者 phinecos