如何在您的Java應用中查找并修復內存泄漏
譯文【51CTO.com快譯】您是否碰到過某個Java應用程序起初運行良好,經過一段時間后卻緩慢下來了?或者它在處理少量文件時性能不錯,文件量一旦增加就性能下降的情況呢?如出現(xiàn)這樣的情況,很可能您遇到了內存泄漏的問題。
在應對內存泄漏時,如果有人問我:“你是否知道此事的前因后果和應對方法?”那么,我就會做出如下回答:
一、目標受眾
盡管在一般情況下,本文中所介紹的方法是獨立于IDE和操作系統(tǒng)的,但是我在此所用到的截圖和說明仍然來自于Fedora Linux和帶插件開發(fā)的Eclipse。
二、內存泄漏的癥狀
起初運行速度快,但隨著時間的推移速度就慢下來了。比如說:
- 能夠正常處理少量數(shù)據(jù)集,但應對大量數(shù)據(jù)集時出現(xiàn)嚴重的性能問題。
- 在您的JVM中,舊版本(Old-Generation)內存的使用率持續(xù)增加。
- 在您的JVM中,出現(xiàn)內存耗盡的跳轉錯誤。
- 無故自我崩潰。
三、常見的內存泄漏
Java中的內存泄漏通常發(fā)生在您忘記關閉某個資源,或是某個對象的引用沒能釋放的時候。例如:
- 文件/文本緩沖區(qū)沒被關閉。(請參見:https://git.eclipse.org/r/#/c/31313/中的案例)
- 在equals()和hashcode()不被使用時,各種哈希映射的引用仍然保持激活的狀態(tài),例如:
- import java.util.Map;
- public class MemLeak {
- public final String key;
- public MemLeak(String key) {
- this.key = key;
- }
- public static void main(String args[]) {
- try {
- Map map = System.getProperties();
- for(;;) {
- map.put(new MemLeak("key"), "value");
- }
- } catch(Exception e) {
- e.printStackTrace();
- }
- }
- }
四、如何一次性修復它們?
這里提供兩種方法。第一種是嘗試“快速修復”。如果此法失敗,那么您就必須往下嘗試一條漫長的解決之路了。
- 快速修復:使用Eclipse內存泄漏的警告(去捕捉一些泄漏)。
- 手動禁用和啟用您代碼的各個部分,并使用VisualVM(Jconsole或Thermostat)之類的工具觀察JVM的內存使用情況。
1. 快速修復:Eclipse內存泄漏的警告/錯誤。
為了遵從JDK 1.5+的代碼規(guī)范,Eclipse會向您“拋出”一些明顯泄漏用例的警告和錯誤。更精確地說,任何使用了closable(如1.5后出現(xiàn)的outputStream)的對象,如果它的引用是被銷毀而不是封閉的話,就會拋出一個警告。然而在Eclipse的各個項目中,其檢漏功能并非總是被啟用的。因此,為了事先打開它們,您可以到項目的設置里,按照下圖所示進行開啟:
此處Eclipse羅列出了各種內存泄漏:
然而,就算使用了Eclipse的此項功能,系統(tǒng)仍無法探測到所有的文件關閉與泄漏。尤其是在使用舊式(1.5之前)代碼時,您很可能會因為它們在使用過程中僅僅只是“關閉”(closable)了,而遇到泄漏問題了。也有時候,文件在深度嵌套中被打開/關閉,也會導致Eclipse無法檢測到。因此如果您碰到這種情況,就可能需要去嘗試第2種方法了。
2. 手動禁用和啟用您代碼的各個部分,并使用VisualVM之類的工具觀察JVM的內存使用情況。
如果您步入了這一步,那就不得不卷起袖子,做一些體力勞動了。您需要通讀您的所有代碼,以試圖找出發(fā)生泄漏的地方。作為幫助,我建議您使用VisualVM之類的工具(當然,Thermostat和MAT也是可行的)。
a. 配置的VisualVM
(1) 下載該工具。
(2) 打開終端,到達目錄.../visualvm_xyz/bin下,運行shell腳本'./visualvm' (或在Windows上運行visualvm.exe)。
(3) 您會看到彈出的主窗口。如果展開“本地”并雙擊您正在運行的應用(如下圖,我的應用是一個子Eclipse),您就可以看到它的各種屬性。
(4) 在Fedora上用VisualVM進行故障診斷:對我來說,最初我無法連接到自己的JVM,也不能夠使堆轉儲(heap-dumps)和分析(profiling)運行起來。于是我探索出了如下步驟:
- 確保用自己的登錄用戶身份運行它,而不是使用sudo。
- 對系統(tǒng)進行全面更新(sudo yum update)。
- 考慮重新啟動是否有所幫助。
- 嘗試在關閉所有正在運行的Java應用程序之后,再啟動VisualVM。
(5) 添加一些插件。在使用VisualVM之前,我事先添加了一些插件。請點擊進入工具->插件->“可用插件”。請選擇如下的插件(如果您喜歡,則可以隨意瀏覽并添加更多的插件):
- 內存池
- 可視的GC
- 終止應用程序
b. 用VisualVM分析運行的代碼
(1) 現(xiàn)在運行您的Java應用程序。
(2) 將VisualVM連接到您的應用程序。
(3) 執(zhí)行那些容易導致性能變緩的操作。
(4) 檢查“監(jiān)控”和“內存池”選項卡。如果您看到在“監(jiān)視器”選項卡中內存顯示增加的話,那就按下“執(zhí)行GC”(垃圾收集),并監(jiān)視內存的使用情況是否有所減少。
(5) 如果并不減少的話,那么就切換到“內存池”選項卡,并檢查“Old Gen”(最開始的對象會停留在“Eden”中,然后通過Survivor空間進行過渡,比較舊的對象會被移到“Old Gen”池中。如果出現(xiàn)泄漏,則會出現(xiàn)在Old-Gen池里。)。
(6) 現(xiàn)在返回去,并注釋掉程序代碼的大部分,從而定位到應用程序開始變慢的位置。
(7) 重復上述過程,直到應用程序完全不再有泄漏的發(fā)生。
(8) 然后,經過反復迭代來重新啟用代碼的各個部分,并檢查VisualVM的內存使用情況。一旦您的應用程序再次開始泄漏,則馬上進入導致內存泄漏的該函數(shù)方法,從而進一步縮小代碼的考察范圍。
(9) 最終,您將能夠把問題縮小到具體某一個類,甚至某一個單一的方法上。請仔細驗證所有文件的緩沖區(qū)是否已被關閉,而HashMap是否被正確的使用了。
五、標準化您的代碼
有時候會很難確定您那“金光閃閃”的新代碼是否真的會比舊代碼更好。面對這種情況下,您需要去標準化應用程序的性能。您可以將下面的這段代碼插入到任何您認為適當?shù)奈恢?,以獲取有關運行時間和垃圾收集次數(shù)的相關信息:
- long start = System.currentTimeMillis();
- ..
- //your code
- ..
- long end = System.currentTimeMillis();
- System.out.println("Run time: " + Long.toString(end - start));
- System.out.println(printGCStats());
- public static String printGCStats() {
- long totalGarbageCollections = 0;
- long garbageCollectionTime = 0;
- for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
- long count = gc.getCollectionCount();
- if (count >= 0) {
- totalGarbageCollections += count;
- }
- long time = gc.getCollectionTime();
- if (time >= 0) {
- garbageCollectionTime += time;
- }
- }
- return "Garbage Collections: " + totalGarbageCollections + "n" +
- "Garbage Collection Time (ms): " + garbageCollectionTime;
- }
特別提醒一下:如果您是在主Eclipse中進行測試的話,我建議去測試一個“干凈”的子Eclipse;或者是在您的Eclipse的一些“干凈”實例中進行。因為這樣的話,其他各種插件是不會對標準的耗時產生影響的。
六、附加說明:堆轉儲
我個人使用的并不多,但有些人比較熱衷于“堆轉儲”。您可以在任何時候采取堆轉儲,然后查看有多少類的實例被打開,以及它們使用了多大的空間。您可以通過雙擊它們來查看具體的內容。如果您想獲悉自己的應用程序產生了多少個對象的話,這種方法會非常有用。
七、我的應用并沒有泄漏,可為何還是很慢?
當然也存在著一種可能性:就算您的代碼中并沒有任何的泄漏,它仍然運行緩慢。如果出現(xiàn)這種情況的話,您就必須進行代碼分析了。不過,代碼分析已經超出了本文所涉及的范圍。這里推薦一個很好的YouTube視頻,它講解了如何去使用免費和付費的分析器來對Eclipse進行分析,請參見:https://www.youtube.com/watch?v=YCC-CpTE2LU。
八、還能看哪些?
至此您可以潛下心來,花上一到兩天的時間去修復您的內存泄漏問題了。在此過程中,如果您仍碰到麻煩的話,請參考如下的鏈接:
- 捕捉內存泄漏:https://www.toptal.com/java/hunting-memory-leaks-in-java
- 內部類的內存泄漏問題:https://blogs.oracle.com/olaf/entry/memory_leaks_made_easy
- 瀏覽Oracle的JVM GC指南:www.oracle.com/technetwork/java/javase/gc-tuning-6-140523.html
原標題:How to Find and Fix Memory Leaks in Your Java Application,作者: Leo Ufimtsev
【51CTO譯稿,合作站點轉載請注明原文譯者和出處為51CTO.com】