瞭解Java中的內存泄漏

原文連接:https://www.baeldung.com/java-memory-leakshtml

做者:baeldungjava

譯者:thornhillgit

1. 簡介

Java的核心優點之一是在內置垃圾收集器(簡稱GC)的幫助下實現自動內存管理。GC隱含地負責分配和釋放內存,所以可以處理大多數內存泄漏問題。github

雖然GC有效地處理了大部份內存,但它並不能成爲保證內存泄漏的萬無一失的解決方案。GC很聰明,但並不完美。即便在盡職盡責的開發人員的應用程序中,內存仍然可能會泄漏。數據庫

仍然可能存在應用程序生成大量多餘對象的狀況,從而耗盡關鍵內存資源,有時會致使整個應用程序失敗。apache

內存泄漏是Java中的一個真實存在的問題。在本教程中,咱們將瞭解內存泄漏的潛在緣由是什麼,如何在運行時識別它們,以及如何在咱們的應用程序中處理它們api

2. 什麼是內存泄漏

內存泄漏是堆中存在再也不使用的對象但垃圾收集器沒法從內存中刪除它們的狀況,所以它們會被沒必要要地一直存在。緩存

內存泄漏很糟糕,由於它會耗盡內存資源並下降系統性能。若是不處理,應用程序最終將耗盡其資源,最終以至命的java.lang.OutOfMemoryError終止。tomcat

堆內存中有兩種不一樣類型的對象 - 被引用和未被引用。被引用的對象是在應用程序中仍具備活動引用的對象,而未被引用的對象沒有任何的活動引用。安全

垃圾收集器會按期刪除未引用的對象,但它永遠不會收集仍在引用的對象。這是可能發生內存泄漏的地方:img

內存泄漏的症狀

  • 應用程序長時間連續運行時性能嚴重降低
  • 應用程序中的OutOfMemoryError堆錯誤
  • 自發且奇怪的應用程序崩潰
  • 應用程序偶爾會耗盡鏈接對象

讓咱們仔細看看其中一些場景以及如何處理它們。

3. Java中內存泄漏類型

在任何應用程序中,數不清的緣由可能致使內存泄漏。在本節中,咱們將討論最多見的問題。

3.1 static字段引發的內存泄漏

可能致使潛在內存泄漏的第一種狀況是大量使用static(靜態)變量。

在Java中,靜態字段一般擁有與整個應用程序相匹配的生命週期(除非ClassLoader複合垃圾回收的條件)。

讓咱們建立一個填充靜態列表的簡單Java程序:

public class StaticTest {
    public static List<Double> list = new ArrayList<>();

    public void populateList() {
        for (int i = 0; i < 10000000; i++) {
            list.add(Math.random());
        }
        Log.info("Debug Point 2");
    }

    public static void main(String[] args) {
        Log.info("Debug Point 1");
        new StaticTest().populateList();
        Log.info("Debug Point 3");
    }
}

如今若是咱們在程序中分析堆內存,咱們會發如今調試點1和2之間,和預期中的同樣,對內存增長了。

但當咱們在調試點3,離開populateList()方法時,堆內存並無被垃圾回收,正如咱們在VisualVM響應中看到的同樣:

img

可是,在上面的程序中,在第2行中,若是咱們只刪除關鍵字 static,那麼它將對內存使用量帶來巨大的變化,這個Visual VM響應顯示: img

直到調試點的第一部分幾乎與咱們在static狀況下得到的部分相同 。但此次當咱們離開populateList()方法,列表中全部的內存都被垃圾回收掉了,由於咱們沒有任何對他的引用

所以,咱們須要很是關注static(靜態)變量的使用。若是集合或大對象被聲明爲static,那麼它們將在應用程序的整個生命週期中保留在內存中,從而阻止可能在其餘地方使用的重要內存。

如何預防呢?

  • 最大限度地減小靜態變量的使用
  • 使用單例時,依賴於延遲加載對象而不是當即加載的方式

3.2 未關閉的資源致使的內存泄漏

每當咱們建立鏈接或打開一個流時,JVM都會爲這些資源分配內存。例如數據庫鏈接,輸入流或者會話對象。

忘記關閉這些資源會致使持續佔有內存,從而使他們沒法GC。若是異常阻止程序執行到達處理關閉這些資源的代碼,則甚至可能發生這種狀況。

在任一種狀況下,資源留下的開放鏈接都會消耗內存,若是咱們不處理他們,他們可能會下降性能,甚至可能致使OutOfMemoryError

如何預防呢?

  • 始終使用finally塊來關閉資源
  • 關閉資源的代碼(甚至在 finally塊中)自己不該該有任何異常
  • 使用Java 7+時,咱們可使用try -with-resources

3.3 不正確的equals()hashCode()實現

在定義新類時,一個很是常見的疏忽是不爲equals()hashCode()方法編寫適當的重寫方法。

HashSet 和 HashMap 在許多操做中使用這些方法,若是它們沒有被正確覆蓋,那麼它們可能成爲潛在的內存泄漏問題的來源。

讓咱們以一個簡單的Person 類爲例, 並將其用做HashMap中的鍵 :

public class Person {
    public String name;

    public Person(String name) {
        this.name = name;
    }
}

如今咱們將重複的Person對象插入到使用此鍵的Map中。

請記住,Map不能包含重複的鍵:

@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
    Map<Person, Integer> map = new HashMap<>();
    for(int i=0; i<100; i++) {
        map.put(new Person("jon"), 1);
    }
    Assert.assertFalse(map.size() == 1);
}

這裏咱們使用Person做爲關鍵。因爲Map不容許重複鍵,所以咱們做爲鍵插入的衆多重複Person對象不該增長內存。

可是因爲咱們沒有定義正確的equals()方法,重複的對象會堆積並增長內存,這就是咱們在內存中看到多個對象的緣由。VisualVM中的堆內存以下所示:img

可是,**若是咱們正確地重寫了equals() 和hashCode()方法,那麼在這個Map中只會存在一個Person對象。

讓咱們看一下正確的實現了equals()hashCode()Person類:

public class Person {
    public String name;

    public Person(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof Person)) {
            return false;
        }
        Person person = (Person) o;
        return person.name.equals(name);
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + name.hashCode();
        return result;
    }
}

在這種狀況下,下面的斷言將會是true:

@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
    Map<Person, Integer> map = new HashMap<>();
    for(int i=0; i<2; i++) {
        map.put(new Person("jon"), 1);
    }
    Assert.assertTrue(map.size() == 1);
}

在適當的重寫equals()hashCode()以後,堆內存在同一程序中以下所示:

img

另外一個例子是當使用像hibernate這樣的ORM框架,他們使用equals()hashCode()方法去分析對象而後將他們保存在緩存中。

如何預防呢?

  • 根據經驗,定義新的實體時,總要重寫equals()hashCode()方法。
  • 只是重寫他們是不夠的,這些方法必須以最佳的方式被重寫。

有關更多信息,請訪問咱們的 Generate equals() and hashCode() with Eclipse 和Guide to hashCode() in Java

3.4引用了外部類的內部類

這種狀況發生在非靜態內部類(匿名類)的狀況下。對於初始化,這些內部類老是須要外部類的實例。

默認狀況下,每一個非靜態內部類都包含對其包含類的隱式引用。若是咱們在應用程序中使用這個內部類'對象,那麼即便在咱們的包含類'對象超出範圍以後,它也不會被垃圾收集

考慮一個類,它包含對大量龐大對象的引用,並具備非靜態內部類。如今,當咱們建立一個內部類的對象時,內存模型以下所示:

img

可是,若是咱們只是將內部類聲明爲static,那麼相同的內存模型以下所示:

img

發生這種狀況是由於內部類對象隱式地保存對外部類對象的引用,從而使其成爲垃圾收集的無效候選者。在匿名類的狀況下也是如此。

如何預防呢?

  • 若是內部類不須要訪問包含的類成員,請考慮將其轉換爲靜態類

3.5finalize()方法形成的內存泄漏

使用finalizers是潛在的內存泄漏問題的另外一個來源。每當重寫類的 finalize()方法時,該類的對象不會當即被垃圾收集。相反,GC將它們排隊等待最終肯定,這將在稍後的時間點發生。

另外,若是用finalize()方法編寫的代碼不是最佳的,而且終結器隊列沒法跟上Java垃圾收集器,那麼早晚,咱們的應用程序註定要遇到 OutOfMemoryError

爲了證實這一點,讓咱們考慮一下咱們已經覆蓋了 finalize()方法的類,而且該方法須要一些時間來執行。當這個類的大量對象被垃圾收集時,那麼在VisualVM中,它看起來像:

img

可是,若是咱們只刪除重寫的finalize()方法,那麼同一程序會給出如下響應:

img

如何預防呢?

  • 咱們應該老是避免finalizers

有關finalize()的更多詳細信息,請閱讀咱們的 Guide to the finalize Method in Java 第3節(避免終結器) 。

常量字符串形成的內存泄漏

Java String池Java 7時經歷了在從永生代(PermGen)轉移到堆空間(HeapSpace)的重大變化。可是對於在版本6及更低版本上運行的應用程序,在使用大型字符串時咱們應該更加專心。

若是咱們讀取一個龐大的大量String對象,並在該對象上調用intern(),那麼它將轉到字符串池,它位於PermGen(永生代)中,而且只要咱們的應用程序運行就會保留在那裏。這會佔用內存並在咱們的應用程序中形成重大內存泄漏。

JVM 1.6中這種狀況的PermGen在VisualVM中看起來像這樣:

img

與此相反,在一個方法中,若是咱們只是從文件中讀取一個字符串而不是intern(),那麼PermGen看起來像:

img

如何預防呢?

  • 解決此問題的最簡單方法是升級到最新的Java版本,由於String池從Java版本7開始轉移到HeapSpace

  • 若是處理大型字符串,請增長PermGen空間的大小以免任何潛在的OutOfMemoryErrors

    -XX:MaxPermSize=512m

3.7 使用ThreadLocal形成的內存泄漏

ThreadLocal (在Introduction to ThreadLocal in Java 中詳細介紹),是一種能將狀態隔離到特定線程,從而保證咱們實現線程安全的結構。

使用此結構時,每一個線程只要處於存活狀態便可將保留對其ThreadLocal變量副本的隱式引用,而且將保留其本身的副本,而不是跨多個線程共享資源。

儘管有其優勢,ThreadLocal 變量的使用仍存在爭議,由於若是使用不當,它們會因引入內存泄漏而臭名昭着。 Joshua Bloch once commented on thread local usage

「Sloppy use of thread pools in combination with sloppy use of thread locals can cause unintended object retention, as has been noted in many places. But placing the blame on thread locals is unwarranted.」

"隨意的在線程池中使用ThreadLocal會保留不少意外的對象。但把責任歸咎於ThreadLocal是沒有根據的 "

ThreadLocal中的內存泄漏

一旦保持線程再也不存在,ThreadLocals應該被垃圾收集。可是當ThreadLocals與現代應用程序服務器一塊兒使用時,問題就出現了。

現代應用程序服務器使用線程池來處理請求而不是建立新請求(例如在Apache Tomcat的狀況下爲Executor)。此外,他們還使用單獨的類加載器。

因爲應用程序服務器中的線程池在線程重用的概念上工做,所以它們永遠不會被垃圾收集 - 相反,它們會被重用來處理另外一個請求。

如今,若是任何類建立 ThreadLocal 變量但未顯式刪除它,則即便在Web應用程序中止後,該對象的副本仍將保留在工做線程中,從而防止對象被垃圾回收。

如何預防呢?

  • 在再也不使用ThreadLocals時清理ThreadLocals是一個很好的作法- ThreadLocals提供了 remove())方法,該方法刪除了此變量的當前線程值

  • 不要使用 ThreadLocal.set(null) 來清除該值 - 它實際上不會清除該值,而是查找與當前線程關聯的Map並將鍵值對設置爲當前線程並分別爲null

  • 最好將 ThreadLocal 視爲須要在finally塊中關閉的資源,以 確保它始終關閉,即便在異常的狀況下:

    try {
        threadLocal.set(System.nanoTime());
        //... further processing
    }
    finally {
        threadLocal.remove();
    }

4. 處理內存泄漏的其餘策略

雖然在處理內存泄漏時沒有一個通用的解決方案,但有一些方法能夠最大限度地減小這些泄漏。

4.1 使用Profiling工具

Java分析器是經過應用程序監視和診斷內存泄漏的工具。他們分析咱們的應用程序內部發生了什麼 - 例如,如何分配內存。

使用分析器,咱們能夠比較不一樣的方法,並找到咱們能夠最佳地使用咱們的資源的領域。

咱們在本教程的第3部分中使用了Java VisualVM。請查看咱們的 Java Profilers指南, 瞭解不一樣類型的分析器,如Mission Control,JProfiler,YourKit,Java VisualVM和Netbeans Profiler。

4.2 詳細垃圾回收

經過啓用詳細垃圾收集,咱們將跟蹤GC的詳細跟蹤。要啓用此功能,咱們須要將如下內容添加到JVM配置中:

經過添加此參數,咱們能夠看到GC內部發生的詳細信息:

img

4.3 使用引用對象避免內存泄漏

咱們還可使用java中的引用對象來構建java.lang.ref包來處理內存泄漏。使用java.lang.ref包,咱們使用對象的特殊引用,而不是直接引用對象,這些對象能夠很容易地進行垃圾回收。

引用隊列旨在讓咱們瞭解垃圾收集器執行的操做。有關更多信息,請閱讀Baeldung的 Soft References in Java ,特別是第4節。

Eclipse的內存泄漏警告

對於JDK 1.5及更高版本的項目,Eclipse會在遇到明顯的內存泄漏狀況時顯示警告和錯誤。所以,在Eclipse中開發時,咱們能夠按期訪問「問題」選項卡,並對內存泄漏警告(若是有)更加警戒:

img

4.5 基準分析

咱們能夠經過執行基準來測量和分析Java代碼的性能。這樣,咱們能夠比較替代方法的性能來完成相同的任務。這能夠幫助咱們選擇更好的方法,並能夠幫助咱們節約內存。

有關基準測試的更多信息,請訪問咱們的 Microbenchmarking with Java 教程。

4.6 代碼審覈

最後,咱們老是採用經典懷舊方式進行簡單的代碼審覈。

在某些狀況下,即便是這種微不足道的方法也能夠幫助消除一些常見的內存泄漏問題。

5 結論

通俗地說,咱們能夠將內存泄漏視爲一種經過阻止重要內存資源來下降應用程序性能的疾病。和全部其餘疾病同樣,若是不治癒,它可能致使致命的應用程序崩潰隨着時間的推移。

內存泄漏很難解決,找到它們須要經過Java語言進行復雜的掌握和命令。在處理內存泄漏時,沒有一個通用的解決方案,由於泄漏可能經過各類各樣的事件發生。

可是,若是咱們採用最佳實踐並按期執行嚴格的代碼演練和分析,那麼咱們能夠最大程度地下降應用程序中內存泄漏的風險。

相關文章
相關標籤/搜索