天天看點

Java記憶體洩漏1. 什麼是記憶體洩漏?2. 詳細Java中的記憶體洩漏3. Java 記憶體配置設定政策4. 如何防止記憶體洩漏的發生?

Java記憶體洩漏1. 什麼是記憶體洩漏?2. 詳細Java中的記憶體洩漏3. Java 記憶體配置設定政策4. 如何防止記憶體洩漏的發生?

1. 什麼是記憶體洩漏?

記憶體洩漏 :對象已經沒有被應用程式使用,但是垃圾回收器沒辦法移除它們,因為還在被引用着。

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

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

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

Java記憶體洩漏1. 什麼是記憶體洩漏?2. 詳細Java中的記憶體洩漏3. Java 記憶體配置設定政策4. 如何防止記憶體洩漏的發生?

是以,通過以上分析,我們知道在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就支援這一特性。

下面給出一個 Java 記憶體洩漏的典型例子,

Vector v = new Vector(10);

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

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

v = null
           

要想了解這個定義,我們需要先了解一下對象在記憶體中的狀态。下面的這張圖就解釋了什麼是無用對象以及什麼是未被引用對象。

Java記憶體洩漏1. 什麼是記憶體洩漏?2. 詳細Java中的記憶體洩漏3. Java 記憶體配置設定政策4. 如何防止記憶體洩漏的發生?

上面圖中可以看出,裡面有被引用對象和未被引用對象。未被引用對象會被垃圾回收器回收,而被引用的對象卻不會。未被引用的對象當然是不再被使用的對象,因為沒有對象再引用它。然而無用對象卻不全是未被引用對象。其中還有被引用的。就是這種情況導緻了記憶體洩漏。

2. 詳細Java中的記憶體洩漏

2.1 Java記憶體回收機制

不論哪種語言的記憶體配置設定方式,都需要傳回所配置設定記憶體的真實位址,也就是傳回一個指針到記憶體塊的首位址。Java中對象是采用new或者反射的方法建立的,這些對象的建立都是在堆(Heap)中配置設定的,所有對象的回收都是由Java虛拟機通過垃圾回收機制完成的。GC為了能夠正确釋放對象,會監控每個對象的運作狀況,對他們的申請、引用、被引用、指派等狀況進行監控,Java會使用有向圖的方法進行管理記憶體,實時監控對象是否可以達到,如果不可到達,則就将其回收,這樣也可以消除引用循環的問題。在Java語言中,判斷一個記憶體空間是否符合垃圾收集的标準有兩個:一個是給對象賦予了空值null,以下再沒有調用過另一個是給對象賦予了新值,這樣重新配置設定了記憶體空間。

2.2 Java記憶體洩漏引起的原因

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

Java記憶體洩漏的根本原因是什麼呢?長生命周期的對象持有短生命周期對象的引用就很可能發生記憶體洩漏,盡管短生命周期對象已經不再需要,但是因為長生命周期持有它的引用而導緻不能被回收,這就是Java中記憶體洩漏的發生場景。

來先看看下面的例子,為什麼會發生記憶體洩漏。下面這個例子中,A對象引用B對象,A對象的生命周期(t1-t4)比B對象的生命周期(t2-t3)長的多。當B對象沒有被應用程式使用之後,A對象仍然在引用着B對象。這樣,垃圾回收器就沒辦法将B對象從記憶體中移除,進而導緻記憶體問題,因為如果A引用更多這樣的對象,那将有更多的未被引用對象存在,并消耗記憶體空間。

B對象也可能會持有許多其他的對象,那這些對象同樣也不會被垃圾回收器回收。所有這些沒在使用的對象将持續的消耗之前配置設定的記憶體空間。

Java記憶體洩漏1. 什麼是記憶體洩漏?2. 詳細Java中的記憶體洩漏3. Java 記憶體配置設定政策4. 如何防止記憶體洩漏的發生?

記憶體洩漏的原因如下

2.2.1 靜态集合類引起記憶體洩漏

像HashMap、Vector等的使用最容易出現記憶體洩露,這些靜态變量的生命周期和應用程式一緻,他們所引用的所有的對象Object也不能被釋放,因為他們也将一直被Vector等引用着。

Static Vector v = new Vector(10);

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

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

2.2.2 監聽器

在 java 程式設計中,我們都需要和監聽器打交道,通常一個應用當中會用到很多監聽器,我們會調用一個控件的諸如addXXXListener() 等方法來增加監聽器,但往往在釋放對象的時候卻沒有記住去删除這些監聽器,進而增加了記憶體洩漏的機會。

2.2.3 各種連接配接

比如資料庫連接配接(dataSourse.getConnection()),網絡連接配接(socket)和io連接配接,除非其顯式的調用了其close() 方法将其連接配接關閉,否則是不會自動被GC 回收的。對于Resultset 和Statement 對象可以不進行顯式回收,但Connection 一定要顯式回收,因為Connection 在任何時候都無法自動回收,而Connection一旦回收,Resultset 和Statement 對象就會立即為NULL。但是如果使用連接配接池,情況就不一樣了,除了要顯式地關閉連接配接,還必須顯式地關閉Resultset Statement 對象(關閉其中一個,另外一個也會關閉),否則就會造成大量的Statement 對象無法釋放,進而引起記憶體洩漏。這種情況下一般都會在try 裡面去的連接配接,在finally裡面釋放連接配接。

2.2.4 内部類和外部子產品的引用

内部類的引用是比較容易遺忘的一種,而且一旦沒釋放可能導緻一系列的後繼類對象沒有釋放。此外程式員還要小心外部子產品不經意的引用,例如程式員A 負責A 子產品,調用了B 子產品的一個方法如:

public void registerMsg(Object b);
           

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

2.2.5 單例模式

不正确使用單例模式是引起記憶體洩漏的一個常見問題,單例對象在初始化後将在 JVM 的整個生命周期中存在(以靜态變量的方式),如果單例對象持有外部的引用,那麼這個對象将不能被 JVM 正常回收,導緻記憶體洩漏,考慮下面的例子:

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 記憶體配置設定政策

Java 程式運作時的記憶體配置設定政策有三種,分别是靜态配置設定,棧式配置設定,和堆式配置設定,對應的,三種存儲政策使用的記憶體空間主要分别是靜态存儲區(也稱方法區)、棧區和堆區。

靜态存儲區(方法區):主要存放靜态資料、全局 static 資料和常量。這塊記憶體在程式編譯時就已經配置設定好,并且在程式整個運作期間都存在。

棧區 :當方法被執行時,方法體内的局部變量(其中包括基礎資料類型、對象的引用)都在棧上建立,并在方法執行結束時這些局部變量所持有的記憶體将會自動被釋放。因為棧記憶體配置設定運算内置于處理器的指令集中,效率很高,但是配置設定的記憶體容量有限。

堆區 : 又稱動态記憶體配置設定,通常就是指在程式運作時直接 new 出來的記憶體,也就是對象的執行個體。這部分記憶體在不使用時将會由 Java 垃圾回收器來負責回收。

3.1 棧與堆的差別

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

堆記憶體用來存放所有由 new 建立的對象(包括該對象其中的所有成員變量)和數組。在堆中配置設定的記憶體,将由 Java 垃圾回收器來自動管理。在堆中産生了一個數組或者對象後,還可以在棧中定義一個特殊的變量,這個變量的取值等于數組或者對象在堆記憶體中的首位址,這個特殊的變量就是我們上面說的引用變量。我們可以通過這個引用變量來通路堆中的對象或者數組。

舉個栗子:

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,而它自己存在于棧中。

結論:
局部變量的基本資料類型和引用存儲于棧中,引用的對象實體存儲于堆中。—— 因為它們屬于方法中的變量,生命周期随方法而結束。
成員變量全部存儲于堆中(包括基本資料類型,引用和引用的對象實體)—— 因為它們屬于類,類對象終究是要被new出來使用的。

           

3.2 Java如何管理記憶體

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

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

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

以下,我們舉一個例子說明如何用有向圖表示記憶體管理。對于程式的每一個時刻,我們都有一個有向圖表示JVM的記憶體配置設定情況。以下右圖,就是左邊程式運作到第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行
    }
}
           
Java記憶體洩漏1. 什麼是記憶體洩漏?2. 詳細Java中的記憶體洩漏3. Java 記憶體配置設定政策4. 如何防止記憶體洩漏的發生?

4. 如何防止記憶體洩漏的發生?

在了解了引起記憶體洩漏的一些原因後,應該盡可能地避免和發現記憶體洩漏。

4.1 好的編碼習慣

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

在确認一個對象無用後,将其所有引用顯式的置為null;

當類從 Jpanel 或 Jdialog 或其它容器類繼承的時候,删除該對象之前不妨調用它的 removeall() 方法;在設一個引用變量為 null 值之前,應注意該引用變量指向的對象是否被監聽,若有,要首先除去監聽器,然後才可以賦空值;當對象是一個 Thread 的時候,删除該對象之前不妨調用它的

interrupt() 方法;記憶體檢測過程中不僅要關注自己編寫的類對象,同時也要關注一些基本類型的對象,例如:int[]、String、char[] 等等;如果有資料庫連接配接,使用 try…finally 結構,在 finally 中關閉 Statement 對象和連接配接。

4.2 好的測試工具

在開發中不能完全避免記憶體洩漏,關鍵要在發現有記憶體洩漏的時候能用好的測試工具迅速定位問題的所在。市場上已有幾種專業檢查 Java 記憶體洩漏的工具,它們的基本工作原理大同小異,都是通過監測 Java 程式運作時,所有對象的申請、釋放等動作,将記憶體管理的所有資訊進行統計、分析、可視化。開發人員将根據這些資訊判斷程式是否有記憶體洩漏問題。這些工具包括 Optimizeit Profiler、JProbe Profiler、JinSight、Rational 公司的 Purify 等。

4.3 注意像 HashMap 、ArrayList 的集合對象

特别注意一些像 HashMap 、ArrayList 的集合對象,它們經常會引發記憶體洩漏。當它們被聲明為 static 時,它們的生命周期就會和應用程式一樣長。

4.4 注意 事件監聽 和 回調函數

特别注意 事件監聽 和 回調函數 。當一個監聽器在使用的時候被注冊,但不再使用之後卻未被反注冊。

“如果一個類自己管理記憶體,那開發人員就得小心記憶體洩漏問題了。” 通常一些成員變量引用其他對象,初始化的時候需要置空。

原文連結:

連結:https://www.jianshu.com/p/54b5da7c6816