java 内存泄漏_Java开发者必须知道的内存泄漏问题
1. 簡介
Java的核心優(yōu)勢之一是在內(nèi)置垃圾收集器(簡稱GC)的幫助下實現(xiàn)自動內(nèi)存管理。GC隱含地負責(zé)分配和釋放內(nèi)存,因此能夠處理大多數(shù)內(nèi)存泄漏問題。
雖然GC有效地處理了大部分內(nèi)存,但它并不能成為保證內(nèi)存泄漏的萬無一失的解決方案。GC很聰明,但并不完美。即使在盡職盡責(zé)的開發(fā)人員的應(yīng)用程序中,內(nèi)存仍然可能會泄漏。
仍然可能存在應(yīng)用程序生成大量多余對象的情況,從而耗盡關(guān)鍵內(nèi)存資源,有時會導(dǎo)致整個應(yīng)用程序失敗。
內(nèi)存泄漏是Java中的一個真實存在的問題。在本教程中,我們將了解內(nèi)存泄漏的潛在原因是什么,如何在運行時識別它們,以及如何在我們的應(yīng)用程序中處理它們。
2. 什么是內(nèi)存泄漏
內(nèi)存泄漏是堆中存在不再使用的對象但垃圾收集器無法從內(nèi)存中刪除它們的情況,因此它們會被不必要地一直存在。
內(nèi)存泄漏很糟糕,因為它會耗盡內(nèi)存資源并降低系統(tǒng)性能。如果不處理,應(yīng)用程序最終將耗盡其資源,最終以致命的java.lang.OutOfMemoryError終止。
堆內(nèi)存中有兩種不同類型的對象 - 被引用和未被引用。被引用的對象是在應(yīng)用程序中仍具有活動引用的對象,而未被引用的對象沒有任何的活動引用。
垃圾收集器會定期刪除未引用的對象,但它永遠不會收集仍在引用的對象。這是可能發(fā)生內(nèi)存泄漏的地方:
內(nèi)存泄漏的癥狀
- 應(yīng)用程序長時間連續(xù)運行時性能嚴重下降
- 應(yīng)用程序中的OutOfMemoryError堆錯誤
- 自發(fā)且奇怪的應(yīng)用程序崩潰
- 應(yīng)用程序偶爾會耗盡連接對象
讓我們仔細看看其中一些場景以及如何處理它們。
3. Java中內(nèi)存泄漏類型
在任何應(yīng)用程序中,數(shù)不清的原因可能導(dǎo)致內(nèi)存泄漏。在本節(jié)中,我們將討論最常見的問題。
3.1 static字段引起的內(nèi)存泄漏
可能導(dǎo)致潛在內(nèi)存泄漏的第一種情況是大量使用static(靜態(tài))變量。
在Java中,靜態(tài)字段通常擁有與整個應(yīng)用程序相匹配的生命周期(除非 ClassLoader復(fù)合垃圾回收的條件)。
讓我們創(chuàng)建一個填充靜態(tài)列表的簡單Java程序:
public class StaticTest { public static List 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"); }}現(xiàn)在如果我們在程序中分析堆內(nèi)存,我們會發(fā)現(xiàn)在調(diào)試點1和2之間,和預(yù)期中的一樣,對內(nèi)存增加了。
但當我們在調(diào)試點3,離開 populateList()方法時,堆內(nèi)存并沒有被垃圾回收,正如我們在 VisualVM響應(yīng)中看到的一樣:
但是,在上面的程序中,在第2行中,如果我們只刪除關(guān)鍵字 static,那么它將對內(nèi)存使用量帶來巨大的變化,這個 VisualVM響應(yīng)顯示:
直到調(diào)試點的第一部分幾乎與我們在 static情況下獲得的部分相同 。但這次當我們離開 populateList()方法,列表中所有的內(nèi)存都被垃圾回收掉了,因為我們沒有任何對他的引用。
因此,我們需要非常關(guān)注static(靜態(tài))變量的使用。如果集合或大對象被聲明為static,那么它們將在應(yīng)用程序的整個生命周期中保留在內(nèi)存中,從而阻止可能在其他地方使用的重要內(nèi)存。
如何預(yù)防呢?
- 最大限度地減少靜態(tài)變量的使用
- 使用單例時,依賴于延遲加載對象而不是立即加載的方式
3.2 未關(guān)閉的資源導(dǎo)致的內(nèi)存泄漏
每當我們創(chuàng)建連接或打開一個流時,JVM都會為這些資源分配內(nèi)存。例如數(shù)據(jù)庫連接,輸入流或者會話對象。
忘記關(guān)閉這些資源會導(dǎo)致持續(xù)占有內(nèi)存,從而使他們無法GC。如果異常阻止程序執(zhí)行到達處理關(guān)閉這些資源的代碼,則甚至可能發(fā)生這種情況。
在任一種情況下,資源留下的開放連接都會消耗內(nèi)存,如果我們不處理他們,他們可能會降低性能,甚至可能導(dǎo)致 OutOfMemoryError。
如何預(yù)防呢?
- 始終使用 finally塊來關(guān)閉資源
- 關(guān)閉資源的代碼(甚至在 finally塊中)本身不應(yīng)該有任何異常
- 使用Java 7+時,我們可以使用 try-with-resources塊
3.3 不正確的 equals()和 hashCode()實現(xiàn)
在定義新類時,一個非常常見的疏忽是不為 equals()和 hashCode()方法編寫適當?shù)闹貙懛椒ā?/p>
HashSet 和 HashMap 在許多操作中使用這些方法,如果它們沒有被正確覆蓋,那么它們可能成為潛在的內(nèi)存泄漏問題的來源。
讓我們以一個簡單的 Person 類為例, 并將其用作 HashMap中的鍵 :
public class Person { public String name; public Person(String name) { this.name = name; }}現(xiàn)在我們將重復(fù)的Person對象插入到使用此鍵的Map中。
請記住,Map不能包含重復(fù)的鍵:
@Testpublic void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() { Map map = new HashMap<>(); for(int i=0; i<100; i++) { map.put(new Person("jon"), 1); } Assert.assertFalse(map.size() == 1);}這里我們使用Person作為關(guān)鍵。由于 Map不允許重復(fù)鍵,因此我們作為鍵插入的眾多重復(fù) Person對象不應(yīng)增加內(nèi)存。
但是由于我們沒有定義正確的equals()方法,重復(fù)的對象會堆積并增加內(nèi)存,這就是我們在內(nèi)存中看到多個對象的原因。VisualVM中的堆內(nèi)存如下所示:
但是,**如果我們正確地重寫了 equals() 和 hashCode()方法,那么在這個 Map中只會存在一個 Person對象。
讓我們看一下正確的實現(xiàn)了 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:
@Testpublic void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() { Map map = new HashMap<>(); for(int i=0; i<2; i++) { map.put(new Person("jon"), 1); } Assert.assertTrue(map.size() == 1);}在適當?shù)闹貙?equals()和 hashCode()之后,堆內(nèi)存在同一程序中如下所示:
另一個例子是當使用像 hibernate這樣的ORM框架,他們使用 equals()和 hashCode()方法去分析對象然后將他們保存在緩存中。
如何預(yù)防呢?
- 根據(jù)經(jīng)驗,定義新的實體時,總要重寫 equals()和 hashCode()方法。
- 只是重寫他們是不夠的,這些方法必須以最佳的方式被重寫。
有關(guān)更多信息,請訪問我們的 Generate equals() and hashCode() with Eclipse 和Guide to hashCode() in Java。
3.4引用了外部類的內(nèi)部類
這種情況發(fā)生在非靜態(tài)內(nèi)部類(匿名類)的情況下。對于初始化,這些內(nèi)部類總是需要外部類的實例。
默認情況下,每個非靜態(tài)內(nèi)部類都包含對其包含類的隱式引用。如果我們在應(yīng)用程序中使用這個內(nèi)部類'對象,那么即使在我們的包含類'對象超出范圍之后,它也不會被垃圾收集。
考慮一個類,它包含對大量龐大對象的引用,并具有非靜態(tài)內(nèi)部類。現(xiàn)在,當我們創(chuàng)建一個內(nèi)部類的對象時,內(nèi)存模型如下所示:
但是,如果我們只是將內(nèi)部類聲明為static,那么相同的內(nèi)存模型如下所示:
發(fā)生這種情況是因為內(nèi)部類對象隱式地保存對外部類對象的引用,從而使其成為垃圾收集的無效候選者。在匿名類的情況下也是如此。
如何預(yù)防呢?
- 如果內(nèi)部類不需要訪問包含的類成員,請考慮將其轉(zhuǎn)換為靜態(tài)類
3.5 finalize()方法造成的內(nèi)存泄漏
使用 finalizers是潛在的內(nèi)存泄漏問題的另一個來源。每當重寫類的 finalize()方法時,該類的對象不會立即被垃圾收集。相反,GC將它們排隊等待最終確定,這將在稍后的時間點發(fā)生。
另外,如果用 finalize()方法編寫的代碼不是最佳的,并且終結(jié)器隊列無法跟上Java垃圾收集器,那么遲早,我們的應(yīng)用程序注定要遇到 OutOfMemoryError。
為了證明這一點,讓我們考慮一下我們已經(jīng)覆蓋了 finalize()方法的類,并且該方法需要一些時間來執(zhí)行。當這個類的大量對象被垃圾收集時,那么在VisualVM中,它看起來像:
但是,如果我們只刪除重寫的finalize()方法,那么同一程序會給出以下響應(yīng):
如何預(yù)防呢?
- 我們應(yīng)該總是避免 finalizers
有關(guān)finalize()的更多詳細信息,請閱讀我們的 Guide to the finalize Method in Java 第3節(jié)(避免終結(jié)器) 。
常量字符串造成的內(nèi)存泄漏
Java String池Java 7時經(jīng)歷了在從永生代(PermGen)轉(zhuǎn)移到堆空間(HeapSpace)的重大變化。但是對于在版本6及更低版本上運行的應(yīng)用程序,在使用大型字符串時我們應(yīng)該更加專心。
如果我們讀取一個龐大的大量String對象,并在該對象上調(diào)用intern(),那么它將轉(zhuǎn)到字符串池,它位于PermGen(永生代)中,并且只要我們的應(yīng)用程序運行就會保留在那里。這會占用內(nèi)存并在我們的應(yīng)用程序中造成重大內(nèi)存泄漏。
JVM 1.6中這種情況的PermGen在VisualVM中看起來像這樣:
與此相反,在一個方法中,如果我們只是從文件中讀取一個字符串而不是 intern(),那么PermGen看起來像:
如何預(yù)防呢?
解決此問題的最簡單方法是升級到最新的Java版本,因為String池從Java版本7開始轉(zhuǎn)移到HeapSpace
如果處理大型字符串,請增加PermGen空間的大小以避免任何潛在的OutOfMemoryErrors:- -XX:MaxPermSize=512m
3.7 使用 ThreadLocal造成的內(nèi)存泄漏
ThreadLocal (在Introduction to ThreadLocal in Java 中詳細介紹),是一種能將狀態(tài)隔離到特定線程,從而保證我們實現(xiàn)線程安全的結(jié)構(gòu)。
使用此結(jié)構(gòu)時,每個線程只要處于存活狀態(tài)即可將保留對其ThreadLocal變量副本的隱式引用,并且將保留其自己的副本,而不是跨多個線程共享資源。
盡管有其優(yōu)點,ThreadLocal 變量的使用仍存在爭議,因為如果使用不當,它們會因引入內(nèi)存泄漏而臭名昭著。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會保留很多意外的對象。但把責(zé)任歸咎于 ThreadLocal是沒有根據(jù)的 "
ThreadLocal中的內(nèi)存泄漏
一旦保持線程不再存在, ThreadLocals應(yīng)該被垃圾收集。但是當 ThreadLocals與現(xiàn)代應(yīng)用程序服務(wù)器一起使用時,問題就出現(xiàn)了。
現(xiàn)代應(yīng)用程序服務(wù)器使用線程池來處理請求而不是創(chuàng)建新請求(例如在Apache Tomcat的情況下為Executor)。此外,他們還使用單獨的類加載器。
由于應(yīng)用程序服務(wù)器中的線程池在線程重用的概念上工作,因此它們永遠不會被垃圾收集 - 相反,它們會被重用來處理另一個請求。
現(xiàn)在,如果任何類創(chuàng)建 ThreadLocal 變量但未顯式刪除它,則即使在Web應(yīng)用程序停止后,該對象的副本仍將保留在工作線程中,從而防止對象被垃圾回收。
如何預(yù)防呢?
在不再使用 ThreadLocals時清理 ThreadLocals是一個很好的做法- ThreadLocals提供了 remove()方法,該方法刪除了此變量的當前線程值- 不要使用 ThreadLocal.set(null) 來清除該值 - 它實際上不會清除該值,而是查找與當前線程關(guān)聯(lián)的 Map并將鍵值對設(shè)置為當前線程并分別為null
最好將 ThreadLocal 視為需要在 finally塊中關(guān)閉的資源,以 確保它始終關(guān)閉,即使在異常的情況下:- try{
- threadLocal.set(System.nanoTime());
- //... further processing
- }
- finally{
- threadLocal.remove();
- }
4. 處理內(nèi)存泄漏的其他策略
雖然在處理內(nèi)存泄漏時沒有一個通用的解決方案,但有一些方法可以最大限度地減少這些泄漏。
4.1 使用 Profiling工具
Java分析器是通過應(yīng)用程序監(jiān)視和診斷內(nèi)存泄漏的工具。他們分析我們的應(yīng)用程序內(nèi)部發(fā)生了什么 - 例如,如何分配內(nèi)存。
使用分析器,我們可以比較不同的方法,并找到我們可以最佳地使用我們的資源的領(lǐng)域。
我們在本教程的第3部分中使用了Java VisualVM。請查看我們的 Java Profilers指南, 了解不同類型的分析器,如Mission Control,JProfiler,YourKit,Java VisualVM和Netbeans Profiler。
4.2 詳細垃圾回收
通過啟用詳細垃圾收集,我們將跟蹤GC的詳細跟蹤。要啟用此功能,我們需要將以下內(nèi)容添加到JVM配置中:
通過添加此參數(shù),我們可以看到GC內(nèi)部發(fā)生的詳細信息:
4.3 使用引用對象避免內(nèi)存泄漏
我們還可以使用java中的引用對象來構(gòu)建 java.lang.ref包來處理內(nèi)存泄漏。使用 java.lang.ref包,我們使用對象的特殊引用,而不是直接引用對象,這些對象可以很容易地進行垃圾回收。
引用隊列旨在讓我們了解垃圾收集器執(zhí)行的操作。有關(guān)更多信息,請閱讀Baeldung的 Soft References in Java ,特別是第4節(jié)。
Eclipse的內(nèi)存泄漏警告
對于JDK 1.5及更高版本的項目,Eclipse會在遇到明顯的內(nèi)存泄漏情況時顯示警告和錯誤。因此,在Eclipse中開發(fā)時,我們可以定期訪問“問題”選項卡,并對內(nèi)存泄漏警告(如果有)更加警惕:
4.5 基準分析
我們可以通過執(zhí)行基準來測量和分析Java代碼的性能。這樣,我們可以比較替代方法的性能來完成相同的任務(wù)。這可以幫助我們選擇更好的方法,并可以幫助我們節(jié)約內(nèi)存。
有關(guān)基準測試的更多信息,請訪問我們的 Microbenchmarking with Java 教程。
4.6 代碼審核
最后,我們總是采用經(jīng)典懷舊方式進行簡單的代碼審核。
在某些情況下,即使是這種微不足道的方法也可以幫助消除一些常見的內(nèi)存泄漏問題。
5 結(jié)論
通俗地說,我們可以將內(nèi)存泄漏視為一種通過阻止重要內(nèi)存資源來降低應(yīng)用程序性能的疾病。和所有其他疾病一樣,如果不治愈,它可能導(dǎo)致致命的應(yīng)用程序崩潰隨著時間的推移。
內(nèi)存泄漏很難解決,找到它們需要通過Java語言進行復(fù)雜的掌握和命令。在處理內(nèi)存泄漏時,沒有一個通用的解決方案,因為泄漏可能通過各種各樣的事件發(fā)生。
但是,如果我們采用最佳實踐并定期執(zhí)行嚴格的代碼演練和分析,那么我們可以最大程度地降低應(yīng)用程序中內(nèi)存泄漏的風(fēng)險。
Java初學(xué)者福利,在入門學(xué)習(xí)Java的過程當中有缺乏基礎(chǔ)的視頻教程,可以申請加入我的Java交流學(xué)習(xí)群:308139472,進群備注2,群里有最新的Java精講基礎(chǔ)視頻,Java學(xué)習(xí)手冊,面試題,Java開發(fā)工具,PDF文檔教程等,需要的都可以自行來下載。
總結(jié)
以上是生活随笔為你收集整理的java 内存泄漏_Java开发者必须知道的内存泄漏问题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: nexus 安装_Jenkins Pip
- 下一篇: python保存文件到指定文件夹_pyt