Java內(nèi)存泄漏分析和解決

最近正在熟悉Java內(nèi)存泄漏的相關(guān)知識,上網(wǎng)查閱了一些資料,在此做個整理算是對收獲的一些總結(jié),希望能對各位有所幫助,有問題可以文末留言探討、補充。

如下是整篇文章的結(jié)構(gòu),所需閱讀時間大約20min

Java當中的內(nèi)存泄漏

1. 什么是內(nèi)存泄漏?

內(nèi)存泄漏:對象已經(jīng)沒有被應用程序使用,但是垃圾回收器沒辦法移除它們,因為還在被引用著。
在Java中,內(nèi)存泄漏就是存在一些被分配的對象,這些對象有下面兩個特點,首先,這些對象是可達的,即在有向圖中,存在通路可以與其相連其次這些對象是無用的,即程序以后不會再使用這些對象。如果對象滿足這兩個條件,這些對象就可以判定為Java中的內(nèi)存泄漏,這些對象不會被GC所回收,然而它卻占用內(nèi)存。

在C++中,內(nèi)存泄漏的范圍更大一些。有些對象被分配了內(nèi)存空間,然后卻不可達,由于C++中沒有GC(Garbage Collection垃圾回收),這些內(nèi)存將永遠收不回來。在Java中,這些不可達的對象都由GC負責回收,因此程序員不需要考慮這部分的內(nèi)存泄露。

通過分析,我們得知,對于C++,程序員需要自己管理邊和頂點,而對于Java程序員只需要管理邊就可以了(不需要管理頂點的釋放)。通過這種方式,Java提高了編程的效率。

C++與Java當中的內(nèi)存泄漏

因此,通過以上分析,我們知道在Java中也有內(nèi)存泄漏,但范圍比C++要小一些。因為Java從語言上保證,任何對象都是可達的,所有的不可達對象都由GC管理。

對于程序員來說,GC基本是透明的,不可見的。雖然,我們只有幾個函數(shù)可以訪問GC,例如運行GC的函數(shù)System.gc(),但是根據(jù)Java語言規(guī)范定義, 該函數(shù)不保證JVM的垃圾收集器一定會執(zhí)行。因為,不同的JVM實現(xiàn)者可能使用不同的算法管理GC。通常,GC的線程的優(yōu)先級別較低。JVM調(diào)用GC的策略也有很多種,有的是內(nèi)存使用到達一定程度時,GC才開始工作,也有定時執(zhí)行的,有的是平緩執(zhí)行GC,有的是中斷式執(zhí)行GC。但通常來說,我們不需要關(guān)心這些。除非在一些特定的場合,GC的執(zhí)行影響應用程序的性能,例如對于基于Web的實時系統(tǒng),如網(wǎng)絡游戲等,用戶不希望GC突然中斷應用程序執(zhí)行而進行垃圾回收,那么我們需要調(diào)整GC的參數(shù),讓GC能夠通過平緩的方式釋放內(nèi)存,例如將垃圾回收分解為一系列的小步驟執(zhí)行,Sun提供的HotSpot JVM就支持這一特性。

下面給出一個 Java 內(nèi)存泄漏的典型例子,

Vector v = new Vector(10);

for (int i = 0; i < 100; i++) {
    Object o = new Object();
    v.add(o);
    o = null;
}

在這個例子中,我們循環(huán)申請Object對象,并將所申請的對象放入一個 Vector 中,如果我們僅僅釋放引用本身,那么 Vector 仍然引用該對象,所以這個對象對 GC 來說是不可回收的。因此,如果對象加入到Vector 后,還必須從 Vector 中刪除,最簡單的方法就是將 Vector 對象設置為 null。

v = null

要想理解這個定義,我們需要先了解一下對象在內(nèi)存中的狀態(tài)。下面的這張圖就解釋了什么是無用對象以及什么是未被引用對象

內(nèi)存泄漏示意圖

上面圖中可以看出,里面有被引用對象未被引用對象。未被引用對象會被垃圾回收器回收,而被引用的對象卻不會。未被引用的對象當然是不再被使用的對象,因為沒有對象再引用它。然而無用對象卻不全是未被引用對象。其中還有被引用的。就是這種情況導致了內(nèi)存泄漏。

2. 詳細Java中的內(nèi)存泄漏

2.1 Java內(nèi)存回收機制

不論哪種語言的內(nèi)存分配方式,都需要返回所分配內(nèi)存的真實地址,也就是返回一個指針到內(nèi)存塊的首地址。Java中對象是采用new或者反射的方法創(chuàng)建的,這些對象的創(chuàng)建都是在堆(Heap)中分配的,所有對象的回收都是由Java虛擬機通過垃圾回收機制完成的。GC為了能夠正確釋放對象,會監(jiān)控每個對象的運行狀況,對他們的申請、引用、被引用、賦值等狀況進行監(jiān)控,Java會使用有向圖的方法進行管理內(nèi)存,實時監(jiān)控對象是否可以達到,如果不可到達,則就將其回收,這樣也可以消除引用循環(huán)的問題。在Java語言中,判斷一個內(nèi)存空間是否符合垃圾收集的標準有兩個:一個是給對象賦予了空值null,以下再沒有調(diào)用過另一個是給對象賦予了新值,這樣重新分配了內(nèi)存空間

2.2 Java內(nèi)存泄漏引起的原因

內(nèi)存泄漏是指無用對象(不再使用的對象)持續(xù)占有內(nèi)存或無用對象的內(nèi)存得不到及時釋放,從而造成內(nèi)存空間的浪費稱為內(nèi)存泄漏。內(nèi)存泄露有時不嚴重且不易察覺,這樣開發(fā)者就不知道存在內(nèi)存泄露,但有時也會很嚴重,會提示你Out of memory。

Java內(nèi)存泄漏的根本原因是什么呢?長生命周期的對象持有短生命周期對象的引用就很可能發(fā)生內(nèi)存泄漏,盡管短生命周期對象已經(jīng)不再需要,但是因為長生命周期持有它的引用而導致不能被回收,這就是Java中內(nèi)存泄漏的發(fā)生場景

來先看看下面的例子,為什么會發(fā)生內(nèi)存泄漏。下面這個例子中,A對象引用B對象,A對象的生命周期(t1-t4)比B對象的生命周期(t2-t3)長的多。當B對象沒有被應用程序使用之后,A對象仍然在引用著B對象。這樣,垃圾回收器就沒辦法將B對象從內(nèi)存中移除,從而導致內(nèi)存問題,因為如果A引用更多這樣的對象,那將有更多的未被引用對象存在,并消耗內(nèi)存空間。

B對象也可能會持有許多其他的對象,那這些對象同樣也不會被垃圾回收器回收。所有這些沒在使用的對象將持續(xù)的消耗之前分配的內(nèi)存空間。

生命周期圖

具體主要有如下幾大類:

2.2.1 靜態(tài)集合類引起內(nèi)存泄漏

像HashMap、Vector等的使用最容易出現(xiàn)內(nèi)存泄露,這些靜態(tài)變量的生命周期和應用程序一致,他們所引用的所有的對象Object也不能被釋放,因為他們也將一直被Vector等引用著。

例如:

Static Vector v = new Vector(10);

for (int i = 0; i < 100; i++) {
    Object o = new Object();
    v.add(o);
    o = null;
}

在這個例子中,循環(huán)申請Object 對象,并將所申請的對象放入一個Vector 中,如果僅僅釋放引用本身(o=null),那么Vector 仍然引用該對象,所以這個對象對GC 來說是不可回收的。因此,如果對象加入到Vector 后,還必須從Vector 中刪除,最簡單的方法就是將Vector對象設置為null。

2.2.2 監(jiān)聽器

java 編程中,我們都需要和監(jiān)聽器打交道,通常一個應用當中會用到很多監(jiān)聽器,我們會調(diào)用一個控件的諸如addXXXListener() 等方法來增加監(jiān)聽器,但往往在釋放對象的時候卻沒有記住去刪除這些監(jiān)聽器,從而增加了內(nèi)存泄漏的機會。

2.2.3 各種連接

比如數(shù)據(jù)庫連接(dataSourse.getConnection()),網(wǎng)絡連接(socket)和io連接,除非其顯式的調(diào)用了其close() 方法將其連接關(guān)閉,否則是不會自動被GC 回收的。對于ResultsetStatement 對象可以不進行顯式回收,但Connection 一定要顯式回收,因為Connection 在任何時候都無法自動回收,而Connection一旦回收,ResultsetStatement 對象就會立即為NULL。但是如果使用連接池,情況就不一樣了,除了要顯式地關(guān)閉連接,還必須顯式地關(guān)閉Resultset Statement 對象(關(guān)閉其中一個,另外一個也會關(guān)閉),否則就會造成大量的Statement 對象無法釋放,從而引起內(nèi)存泄漏。這種情況下一般都會在try 里面去的連接,在finally里面釋放連接。

2.2.4 內(nèi)部類和外部模塊的引用

內(nèi)部類的引用是比較容易遺忘的一種,而且一旦沒釋放可能導致一系列的后繼類對象沒有釋放。此外程序員還要小心外部模塊不經(jīng)意的引用,例如程序員A 負責A 模塊,調(diào)用了B 模塊的一個方法如:

public void registerMsg(Object b);

這種調(diào)用就要非常小心了,傳入了一個對象,很可能模塊B就保持了對該對象的引用,這時候就需要注意模塊B是否提供相應的操作去除引用。

2.2.5 單例模式

不正確使用單例模式是引起內(nèi)存泄漏的一個常見問題,單例對象在初始化后將在 JVM 的整個生命周期中存在(以靜態(tài)變量的方式),如果單例對象持有外部的引用,那么這個對象將不能被 JVM 正常回收,導致內(nèi)存泄漏,考慮下面的例子:

public class A {
    public A() {
        B.getInstance().setA(this);
    }
    ...
}

//B類采用單例模式
class B{
    private A a;
    private static B instance = new B();
    
    public B(){}
    
    public static B getInstance() {
        return instance;
    }
    
    public void setA(A a) {
        this.a = a;
    }

    public A getA() {
        return a;
    }
}

3. Java 內(nèi)存分配策略

Java 程序運行時的內(nèi)存分配策略有三種,分別是靜態(tài)分配,棧式分配,和堆式分配,對應的,三種存儲策略使用的內(nèi)存空間主要分別是靜態(tài)存儲區(qū)(也稱方法區(qū))、棧區(qū)和堆區(qū)

靜態(tài)存儲區(qū)(方法區(qū)):主要存放靜態(tài)數(shù)據(jù)、全局 static 數(shù)據(jù)和常量。這塊內(nèi)存在程序編譯時就已經(jīng)分配好,并且在程序整個運行期間都存在。

棧區(qū) :當方法被執(zhí)行時,方法體內(nèi)的局部變量(其中包括基礎數(shù)據(jù)類型、對象的引用)都在棧上創(chuàng)建,并在方法執(zhí)行結(jié)束時這些局部變量所持有的內(nèi)存將會自動被釋放。因為棧內(nèi)存分配運算內(nèi)置于處理器的指令集中,效率很高,但是分配的內(nèi)存容量有限。

堆區(qū) : 又稱動態(tài)內(nèi)存分配,通常就是指在程序運行時直接 new 出來的內(nèi)存,也就是對象的實例。這部分內(nèi)存在不使用時將會由 Java 垃圾回收器來負責回收。

3.1 棧與堆的區(qū)別

在方法體內(nèi)定義的(局部變量)一些基本類型的變量和對象的引用變量都是在方法的棧內(nèi)存中分配的。當在一段方法塊中定義一個變量時,Java 就會在棧中為該變量分配內(nèi)存空間,當超過該變量的作用域后,該變量也就無效了,分配給它的內(nèi)存空間也將被釋放掉,該內(nèi)存空間可以被重新使用。

堆內(nèi)存用來存放所有由 new 創(chuàng)建的對象(包括該對象其中的所有成員變量)和數(shù)組。在堆中分配的內(nèi)存,將由 Java 垃圾回收器來自動管理。在堆中產(chǎn)生了一個數(shù)組或者對象后,還可以在棧中定義一個特殊的變量,這個變量的取值等于數(shù)組或者對象在堆內(nèi)存中的首地址,這個特殊的變量就是我們上面說的引用變量。我們可以通過這個引用變量來訪問堆中的對象或者數(shù)組。

舉個栗子:

public class Sample {
    int s1 = 0;
    Sample mSample1 = new Sample();
    
    public void method() {
        int s2 = 1;
        Sample mSample2 = new Sample();
    }
}
Sample mSample3 = new Sample();

Sample 類的局部變量 s2 和引用變量 mSample2 都是存在于棧中,但 mSample2 指向的對象是存在于堆上的。

mSample3 指向的對象實體存放在堆上,包括這個對象的所有成員變量 s1 和 mSample1,而它自己存在于棧中。

結(jié)論
局部變量的基本數(shù)據(jù)類型和引用存儲于中,引用的對象實體存儲于中。—— 因為它們屬于方法中的變量,生命周期隨方法而結(jié)束。
成員變量全部存儲于中(包括基本數(shù)據(jù)類型,引用和引用的對象實體)—— 因為它們屬于類,類對象終究是要被new出來使用的。

了解了 Java 的內(nèi)存分配之后,我們再來看看 Java 是怎么管理內(nèi)存的。

3.2 Java如何管理內(nèi)存

Java的內(nèi)存管理就是對象的分配和釋放問題。在 Java 中,程序員需要通過關(guān)鍵字 new 為每個對象申請內(nèi)存空間 (基本類型除外),所有的對象都在堆 (Heap)中分配空間。另外,對象的釋放是由 GC 決定和執(zhí)行的。在 Java 中,內(nèi)存的分配是由程序完成的,而內(nèi)存的釋放是由 GC 完成的,這種收支兩條線的方法確實簡化了程序員的工作。但同時,它也加重了JVM的工作。這也是 Java 程序運行速度較慢的原因之一。因為GC 為了能夠正確釋放對象,GC 必須監(jiān)控每一個對象的運行狀態(tài),包括對象的申請、引用、被引用、賦值等,GC 都需要進行監(jiān)控

監(jiān)視對象狀態(tài)是為了更加準確地、及時地釋放對象,而釋放對象的根本原則就是該對象不再被引用

為了更好理解 GC 的工作原理,我們可以將對象考慮為有向圖的頂點,將引用關(guān)系考慮為圖的有向邊,有向邊從引用者指向被引對象。另外,每個線程對象可以作為一個圖的起始頂點,例如大多程序從 main 進程開始執(zhí)行,那么該圖就是以 main 進程頂點開始的一棵根樹。在這個有向圖中,根頂點可達的對象都是有效對象,GC將不回收這些對象。如果某個對象 (連通子圖)與這個根頂點不可達(注意,該圖為有向圖),那么我們認為這個(這些)對象不再被引用,可以被 GC 回收。

以下,我們舉一個例子說明如何用有向圖表示內(nèi)存管理。對于程序的每一個時刻,我們都有一個有向圖表示JVM的內(nèi)存分配情況。以下右圖,就是左邊程序運行到第6行的示意圖。

public class Test {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Object o1 = new Object();
        Object o2 = new Object();
        o2 = o1;//此行為第6行
    }
}
有向圖

4. 如何防止內(nèi)存泄漏的發(fā)生?

在了解了引起內(nèi)存泄漏的一些原因后,應該盡可能地避免和發(fā)現(xiàn)內(nèi)存泄漏。

4.1 好的編碼習慣

最基本的建議就是盡早釋放無用對象的引用,大多數(shù)程序員在使用臨時變量的時候,都是讓引用變量在退出活動域后,自動設置為 null 。在使用這種方式時候,必須特別注意一些復雜的對象圖,例如數(shù)組、列、樹、圖等,這些對象之間有相互引用關(guān)系較為復雜。對于這類對象,GC 回收它們一般效率較低。如果程序允許,盡早將不用的引用對象賦為null。另外建議幾點:

在確認一個對象無用后,將其所有引用顯式的置為null;

當類從 JpanelJdialog 或其它容器類繼承的時候,刪除該對象之前不妨調(diào)用它的 removeall() 方法;在設一個引用變量為 null 值之前,應注意該引用變量指向的對象是否被監(jiān)聽,若有,要首先除去監(jiān)聽器,然后才可以賦空值;當對象是一個 Thread 的時候,刪除該對象之前不妨調(diào)用它的
interrupt() 方法;內(nèi)存檢測過程中不僅要關(guān)注自己編寫的類對象,同時也要關(guān)注一些基本類型的對象,例如:int[]、String、char[] 等等;如果有數(shù)據(jù)庫連接,使用 try…finally 結(jié)構(gòu),在 finally 中關(guān)閉 Statement 對象和連接。

4.2 好的測試工具

在開發(fā)中不能完全避免內(nèi)存泄漏,關(guān)鍵要在發(fā)現(xiàn)有內(nèi)存泄漏的時候能用好的測試工具迅速定位問題的所在。市場上已有幾種專業(yè)檢查 Java 內(nèi)存泄漏的工具,它們的基本工作原理大同小異,都是通過監(jiān)測 Java 程序運行時,所有對象的申請、釋放等動作,將內(nèi)存管理的所有信息進行統(tǒng)計、分析、可視化。開發(fā)人員將根據(jù)這些信息判斷程序是否有內(nèi)存泄漏問題。這些工具包括 Optimizeit Profiler、JProbe Profiler、JinSight、Rational 公司的 Purify 等。

4.3 注意像 HashMapArrayList 的集合對象

特別注意一些像 HashMapArrayList 的集合對象,它們經(jīng)常會引發(fā)內(nèi)存泄漏。當它們被聲明為 static 時,它們的生命周期就會和應用程序一樣長。

4.4 注意 事件監(jiān)聽回調(diào)函數(shù)

特別注意 事件監(jiān)聽回調(diào)函數(shù) 。當一個監(jiān)聽器在使用的時候被注冊,但不再使用之后卻未被反注冊。

“如果一個類自己管理內(nèi)存,那開發(fā)人員就得小心內(nèi)存泄漏問題了。” 通常一些成員變量引用其他對象,初始化的時候需要置空。

參考文章:
1.介紹Java中的內(nèi)存泄漏
2.Java的內(nèi)存泄漏
3.Java中關(guān)于內(nèi)存泄漏出現(xiàn)的原因匯總及如何避免內(nèi)存泄漏
4.Java內(nèi)存泄漏的幾大原因及預防檢測

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內(nèi)容