在 Java 中創建和檢測內存洩漏
一、概述
在 Java 應用程序中,內存洩漏會導致嚴重的性能下降和系統故障。開發人員必須了解內存洩漏是如何發生的以及如何識別和解決它們。
在本教程中,我們將使用失效偵聽器問題作為示例,提供有關在 Java 中創建內存洩漏的指南。我們還將討論檢測內存洩漏的各種方法,包括日誌記錄、分析、詳細垃圾收集和堆轉儲。
2. 造成內存洩漏
我們將把失效的偵聽器問題視為內存洩漏的示例。這是了解 Java 中的內存分配和垃圾回收的絕佳方式。
讓我們創建一個應用程序,向登錄並訂閱我們服務的用戶發送隨機電影名言。這個應用程序很簡單,一次只能為一個用戶服務:
public static void main(String[] args) {
while (true) {
User user = generateUser();
logger.debug("{} logged in", user.getName());
user.subscribe(movieQuoteService);
userUsingService();
logger.debug("{} logged out", user.getName());
}
}
UserGenerator
是一個簡單的類,提供無限量的隨機用戶。我們將使用 Datafaker 進行隨機化:
public class UserGenerator {
private final static Faker faker = new Faker();
public static User generateUser() {
System.out.println("Generating user");
String name = faker.name().fullName();
String email = faker.internet().emailAddress();
String phone = faker.phoneNumber().cellPhone();
String street = faker.address().streetAddress();
String city = faker.address().city();
String state = faker.address().state();
String zipCode = faker.address().zipCode();
return new User(name, email, phone, street, city, state, zipCode);
}
}
用戶和我們的服務之間的關係將基於觀察者模式。因此, Users
可以訂閱該服務,我們的MovieQuoteService
將向用戶更新新的電影報價。
此示例的主要問題是Users
永遠不會取消訂閱該服務。這會造成內存洩漏,因為即使用戶超出範圍,垃圾收集器也無法將其刪除,因為服務持有他們的引用。
我們可以明確取消訂閱用戶來緩解這個問題,這會奏效。但是,最好的解決方案是使用WeakReferences
來自動執行此過程。
3.檢測內存洩漏
在上一節中,我們創建了一個存在重大問題的應用程序——內存洩漏。雖然這個問題可能是災難性的,但通常很難檢測到。讓我們回顧一下我們可以找到這個問題的一些方法。
3.1.記錄
讓我們從最直接的方法開始,使用日誌記錄來查找我們系統的問題。這不是檢測內存洩漏的最先進方法,但它易於使用並且可能有助於發現異常情況。
在運行我們的服務時,日誌輸出會向我們顯示用戶活動:
21:58:24.280 [pool-1-thread-1] DEBUG cblapsedlistener.MovieQuoteService - New quote: Go ahead, make my day.
21:58:24.358 [main] DEBUG cblLapsedListenerRunner - Earl Runolfsdottir logged in
21:58:24.358 [main] DEBUG cblapsedlistener.MovieQuoteService - Current number of subscribed users: 0
21:58:24.371 [main] DEBUG cblLapsedListenerRunner - Earl Runolfsdottir logged out
21:58:24.372 [main] DEBUG cblLapsedListenerRunner - Barbra Rosenbaum logged in
21:58:24.372 [main] DEBUG cblapsedlistener.MovieQuoteService - Current number of subscribed users: 1
21:58:24.383 [main] DEBUG cblLapsedListenerRunner - Barbra Rosenbaum logged out
21:58:24.383 [main] DEBUG cblLapsedListenerRunner - Leighann McCullough logged in
21:58:24.383 [main] DEBUG cblapsedlistener.MovieQuoteService - Current number of subscribed users: 2
21:58:24.396 [main] DEBUG cblLapsedListenerRunner - Leighann McCullough logged out
21:58:24.397 [main] DEBUG cblLapsedListenerRunner - Mr. Charlie Keeling logged in
21:58:24.397 [main] DEBUG cblapsedlistener.MovieQuoteService - Current number of subscribed users: 3
21:58:24.409 [main] DEBUG cblLapsedListenerRunner - Mr. Charlie Keeling logged out
21:58:24.410 [main] DEBUG cblLapsedListenerRunner - Alvin O'Connell logged in
21:58:24.410 [main] DEBUG cblapsedlistener.MovieQuoteService - Current number of subscribed users: 4
21:58:24.423 [main] DEBUG cblLapsedListenerRunner - Alvin O'Connell logged out
21:58:24.423 [main] DEBUG cblLapsedListenerRunner - Tracey Stoltenberg logged in
21:58:24.423 [main] DEBUG cblapsedlistener.MovieQuoteService - Current number of subscribed users: 5
我們可以在前面的代碼片段中註意到一件有趣的事情。如前所述,我們的應用程序一次只能處理一個用戶。
因此,只有一個用戶可以訂閱我們的服務。同時日誌顯示訂閱人數超過了這個值。進一步閱讀進一步證明我們的系統存在問題。
雖然日誌沒有顯示問題發生的位置,但這是防止我們的系統出現問題的第一步。
3.2.剖析
與上一步一樣,此步驟旨在查找工作應用程序中的異常情況。但是,分析器可以極大地簡化對工作應用程序內存佔用的監視。
第一個要點是使用的內存隨時間單調增加。這並不總是內存洩漏的跡象。然而,在像我們這樣的應用程序中,增加內存使用量可能是我們遇到問題的一個好兆頭。
我們將使用 JConsole 探查器。這是一個基本的分析器,但它提供了所有需要的功能,並且包含在每個 JDK 發行版中。此外,在任何系統上啟動它都很容易:
$ jconsole
讓我們啟動我們的應用程序,看看 JConsole 會告訴我們什麼。啟動應用程序後,其內存消耗增加:
但是,內存使用並不總是內存洩漏的標誌。讓我們嘗試提示垃圾收集器清理一些死對象:
正如我們所見,垃圾收集器工作得很好並且清理了一些空間。因此,我們可以假設我們根本沒有任何問題。但是,讓我們看看老一代。這是一段時間內在我們的應用程序中的幾次垃圾收集中倖存下來的對象的空間。我們可以看到它的大小不斷增加:
一種解釋是,除了用戶,我們還有報價。我們不在我們的應用程序中存儲對引號的引用,因此垃圾收集器可以毫無問題地清理它們。同時,我們的服務保留了對每個用戶的引用,防止它們被垃圾收集,並將它們提升到老年代:
儘管垃圾收集器會定期清理,但很明顯總體內存消耗會隨著時間的推移而增長。我們在幾分鐘內從大約 10 MB 增加到 30 MB。這可能不會在服務器上造成數小時甚至數天的任何問題。如果服務器定期重啟,我們可能永遠不會看到OutOfMemoryError
:
我們在老年代也有同樣的畫面:內存消耗只會增長。對於我們一次只能服務一個用戶的應用程序,這是一個問題的跡象。
3.3.詳細垃圾收集
這是檢查堆狀態和垃圾收集過程的另一種方法。根據 Java 版本,我們可以使用幾個標誌來打開詳細垃圾收集。日誌上的輸出將反映我們在 JConsole 中獲得的先前信息:
[0.004s][info][gc] Using G1
[0.210s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 23M->6M(392M) 1.693ms
[33.169s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 38M->7M(392M) 1.994ms
[250.890s][info][gc] GC(2) Pause Young (Normal) (G1 Evacuation Pause) 203M->16M(392M) 11.420ms
[507.259s][info][gc] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 228M->25M(392M) 14.321ms
[786.181s][info][gc] GC(4) Pause Young (Normal) (G1 Evacuation Pause) 229M->33M(392M) 17.410ms
[1073.277s][info][gc] GC(5) Pause Young (Normal) (G1 Evacuation Pause) 241M->41M(392M) 11.251ms
[1341.717s][info][gc] GC(6) Pause Young (Normal) (G1 Evacuation Pause) 241M->48M(392M) 17.132ms
這些日誌使用特定格式來顯示總體內存消耗隨時間增加。這是檢查應用程序的內存佔用並找出問題的一種非常快速和直接的方法。
但是,在這一步之後,我們需要找到這個問題的原因。在我們有幾個類的應用程序中,該任務可能微不足道,我們可以通過檢查代碼來解決它。同時,僅通過查看大型應用程序中的代碼可能無法檢測到問題。
3.4.堆轉儲
有多種方法可以捕獲堆轉儲,並且 JDK 包含多種控制台工具。我們將使用 VisualVM 來捕獲和讀取堆轉儲:
這是捕獲堆轉儲的便捷工具,包括 JConsole 的所有功能,
使過程變得簡單。
捕獲堆轉儲後,我們可以查看和分析它。在我們的例子中,我們將嘗試找到不應該存在的活動對象。對我們來說幸運的是,VisualVM 生成堆轉儲的摘要,顯示重要信息:
我們系統中的用戶在實例數量和整體大小方面排名第三。我們已經知道我們有一個內存消耗問題,現在我們找到了罪魁禍首。
此外,VisualVM 允許我們更徹底地分析堆轉儲並檢查堆中的所有實例:
這可能對具有復雜對象交互的大型應用程序有幫助。此外,這可能有助於調整應用程序和查找有問題的地方。
找到有問題的實例後,我們仍然需要檢查代碼以查看何時出現內存洩漏,但現在我們可以縮小搜索範圍。
4。結論
內存洩漏會嚴重影響 Java 應用程序,導致內存逐漸耗盡和潛在的系統故障。在本教程中,我們出於教育目的創建了一個內存洩漏,並討論了各種檢測技術,包括日誌記錄、分析、詳細垃圾收集和堆轉儲。
每種方法都提供了對應用程序的運行時行為和內存消耗的寶貴見解。日誌記錄有助於識別異常,同時分析和詳細的垃圾收集日誌可監控內存使用情況和垃圾收集過程。堆轉儲識別有問題的對象及其引用,縮小內存洩漏的來源。
了解 Java 中的內存分配和垃圾回收有助於開發人員防止內存洩漏並構建更高效、更健壯的應用程序。與往常一樣,源代碼可在 GitHub 上獲得。