原文連接:https://www.baeldung.com/java-memory-leakshtml
做者:baeldungjava
譯者:thornhillgit
Java的核心優點之一是在內置垃圾收集器(簡稱GC)的幫助下實現自動內存管理。GC隱含地負責分配和釋放內存,所以可以處理大多數內存泄漏問題。github
雖然GC有效地處理了大部份內存,但它並不能成爲保證內存泄漏的萬無一失的解決方案。GC很聰明,但並不完美。即便在盡職盡責的開發人員的應用程序中,內存仍然可能會泄漏。數據庫
仍然可能存在應用程序生成大量多餘對象的狀況,從而耗盡關鍵內存資源,有時會致使整個應用程序失敗。apache
內存泄漏是Java中的一個真實存在的問題。在本教程中,咱們將瞭解內存泄漏的潛在緣由是什麼,如何在運行時識別它們,以及如何在咱們的應用程序中處理它們。api
內存泄漏是堆中存在再也不使用的對象但垃圾收集器沒法從內存中刪除它們的狀況,所以它們會被沒必要要地一直存在。緩存
內存泄漏很糟糕,由於它會耗盡內存資源並下降系統性能。若是不處理,應用程序最終將耗盡其資源,最終以至命的java.lang.OutOfMemoryError終止。tomcat
堆內存中有兩種不一樣類型的對象 - 被引用和未被引用。被引用的對象是在應用程序中仍具備活動引用的對象,而未被引用的對象沒有任何的活動引用。安全
垃圾收集器會按期刪除未引用的對象,但它永遠不會收集仍在引用的對象。這是可能發生內存泄漏的地方:
內存泄漏的症狀
讓咱們仔細看看其中一些場景以及如何處理它們。
在任何應用程序中,數不清的緣由可能致使內存泄漏。在本節中,咱們將討論最多見的問題。
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
響應中看到的同樣:
可是,在上面的程序中,在第2行中,若是咱們只刪除關鍵字 static
,那麼它將對內存使用量帶來巨大的變化,這個Visual VM
響應顯示:
直到調試點的第一部分幾乎與咱們在static
狀況下得到的部分相同 。但此次當咱們離開populateList()
方法,列表中全部的內存都被垃圾回收掉了,由於咱們沒有任何對他的引用。
所以,咱們須要很是關注static(靜態)變量的使用。若是集合或大對象被聲明爲static,那麼它們將在應用程序的整個生命週期中保留在內存中,從而阻止可能在其餘地方使用的重要內存。
如何預防呢?
每當咱們建立鏈接或打開一個流時,JVM都會爲這些資源分配內存。例如數據庫鏈接,輸入流或者會話對象。
忘記關閉這些資源會致使持續佔有內存,從而使他們沒法GC。若是異常阻止程序執行到達處理關閉這些資源的代碼,則甚至可能發生這種狀況。
在任一種狀況下,資源留下的開放鏈接都會消耗內存,若是咱們不處理他們,他們可能會下降性能,甚至可能致使OutOfMemoryError
。
如何預防呢?
finally
塊來關閉資源finally
塊中)自己不該該有任何異常try -with-resources
塊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中的堆內存以下所示:
可是,**若是咱們正確地重寫了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()
以後,堆內存在同一程序中以下所示:
另外一個例子是當使用像hibernate
這樣的ORM框架,他們使用equals()
和hashCode()
方法去分析對象而後將他們保存在緩存中。
如何預防呢?
equals()
和hashCode()
方法。有關更多信息,請訪問咱們的 Generate equals() and hashCode() with Eclipse 和Guide to hashCode() in Java。
這種狀況發生在非靜態內部類(匿名類)的狀況下。對於初始化,這些內部類老是須要外部類的實例。
默認狀況下,每一個非靜態內部類都包含對其包含類的隱式引用。若是咱們在應用程序中使用這個內部類'對象,那麼即便在咱們的包含類'對象超出範圍以後,它也不會被垃圾收集。
考慮一個類,它包含對大量龐大對象的引用,並具備非靜態內部類。如今,當咱們建立一個內部類的對象時,內存模型以下所示:
可是,若是咱們只是將內部類聲明爲static,那麼相同的內存模型以下所示:
發生這種狀況是由於內部類對象隱式地保存對外部類對象的引用,從而使其成爲垃圾收集的無效候選者。在匿名類的狀況下也是如此。
如何預防呢?
finalize()
方法形成的內存泄漏使用finalizers
是潛在的內存泄漏問題的另外一個來源。每當重寫類的 finalize()
方法時,該類的對象不會當即被垃圾收集。相反,GC將它們排隊等待最終肯定,這將在稍後的時間點發生。
另外,若是用finalize()
方法編寫的代碼不是最佳的,而且終結器隊列沒法跟上Java垃圾收集器,那麼早晚,咱們的應用程序註定要遇到 OutOfMemoryError
。
爲了證實這一點,讓咱們考慮一下咱們已經覆蓋了 finalize()
方法的類,而且該方法須要一些時間來執行。當這個類的大量對象被垃圾收集時,那麼在VisualVM中,它看起來像:
可是,若是咱們只刪除重寫的finalize()方法,那麼同一程序會給出如下響應:
如何預防呢?
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中看起來像這樣:
與此相反,在一個方法中,若是咱們只是從文件中讀取一個字符串而不是intern()
,那麼PermGen看起來像:
如何預防呢?
解決此問題的最簡單方法是升級到最新的Java版本,由於String池從Java版本7開始轉移到HeapSpace
若是處理大型字符串,請增長PermGen空間的大小以免任何潛在的OutOfMemoryErrors:
-XX:MaxPermSize=512m
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(); }
雖然在處理內存泄漏時沒有一個通用的解決方案,但有一些方法能夠最大限度地減小這些泄漏。
Profiling
工具Java分析器是經過應用程序監視和診斷內存泄漏的工具。他們分析咱們的應用程序內部發生了什麼 - 例如,如何分配內存。
使用分析器,咱們能夠比較不一樣的方法,並找到咱們能夠最佳地使用咱們的資源的領域。
咱們在本教程的第3部分中使用了Java VisualVM。請查看咱們的 Java Profilers指南, 瞭解不一樣類型的分析器,如Mission Control,JProfiler,YourKit,Java VisualVM和Netbeans Profiler。
經過啓用詳細垃圾收集,咱們將跟蹤GC的詳細跟蹤。要啓用此功能,咱們須要將如下內容添加到JVM配置中:
經過添加此參數,咱們能夠看到GC內部發生的詳細信息:
咱們還可使用java中的引用對象來構建java.lang.ref
包來處理內存泄漏。使用java.lang.ref
包,咱們使用對象的特殊引用,而不是直接引用對象,這些對象能夠很容易地進行垃圾回收。
引用隊列旨在讓咱們瞭解垃圾收集器執行的操做。有關更多信息,請閱讀Baeldung的 Soft References in Java ,特別是第4節。
對於JDK 1.5及更高版本的項目,Eclipse會在遇到明顯的內存泄漏狀況時顯示警告和錯誤。所以,在Eclipse中開發時,咱們能夠按期訪問「問題」選項卡,並對內存泄漏警告(若是有)更加警戒:
咱們能夠經過執行基準來測量和分析Java代碼的性能。這樣,咱們能夠比較替代方法的性能來完成相同的任務。這能夠幫助咱們選擇更好的方法,並能夠幫助咱們節約內存。
有關基準測試的更多信息,請訪問咱們的 Microbenchmarking with Java 教程。
最後,咱們老是採用經典懷舊方式進行簡單的代碼審覈。
在某些狀況下,即便是這種微不足道的方法也能夠幫助消除一些常見的內存泄漏問題。
通俗地說,咱們能夠將內存泄漏視爲一種經過阻止重要內存資源來下降應用程序性能的疾病。和全部其餘疾病同樣,若是不治癒,它可能致使致命的應用程序崩潰隨着時間的推移。
內存泄漏很難解決,找到它們須要經過Java語言進行復雜的掌握和命令。在處理內存泄漏時,沒有一個通用的解決方案,由於泄漏可能經過各類各樣的事件發生。
可是,若是咱們採用最佳實踐並按期執行嚴格的代碼演練和分析,那麼咱們能夠最大程度地下降應用程序中內存泄漏的風險。