1. 概述
在 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 c.b.lapsedlistener.MovieQuoteService - New quote: Go ahead, make my day.
21:58:24.358 [main] DEBUG c.b.l.LapsedListenerRunner - Earl Runolfsdottir logged in
21:58:24.358 [main] DEBUG c.b.lapsedlistener.MovieQuoteService - Current number of subscribed users: 0
21:58:24.371 [main] DEBUG c.b.l.LapsedListenerRunner - Earl Runolfsdottir logged out
21:58:24.372 [main] DEBUG c.b.l.LapsedListenerRunner - Barbra Rosenbaum logged in
21:58:24.372 [main] DEBUG c.b.lapsedlistener.MovieQuoteService - Current number of subscribed users: 1
21:58:24.383 [main] DEBUG c.b.l.LapsedListenerRunner - Barbra Rosenbaum logged out
21:58:24.383 [main] DEBUG c.b.l.LapsedListenerRunner - Leighann McCullough logged in
21:58:24.383 [main] DEBUG c.b.lapsedlistener.MovieQuoteService - Current number of subscribed users: 2
21:58:24.396 [main] DEBUG c.b.l.LapsedListenerRunner - Leighann McCullough logged out
21:58:24.397 [main] DEBUG c.b.l.LapsedListenerRunner - Mr. Charlie Keeling logged in
21:58:24.397 [main] DEBUG c.b.lapsedlistener.MovieQuoteService - Current number of subscribed users: 3
21:58:24.409 [main] DEBUG c.b.l.LapsedListenerRunner - Mr. Charlie Keeling logged out
21:58:24.410 [main] DEBUG c.b.l.LapsedListenerRunner - Alvin O'Connell logged in
21:58:24.410 [main] DEBUG c.b.lapsedlistener.MovieQuoteService - Current number of subscribed users: 4
21:58:24.423 [main] DEBUG c.b.l.LapsedListenerRunner - Alvin O'Connell logged out
21:58:24.423 [main] DEBUG c.b.l.LapsedListenerRunner - Tracey Stoltenberg logged in
21:58:24.423 [main] DEBUG c.b.lapsedlistener.MovieQuoteService - Current number of subscribed users: 5
我们可以在上面的片段中注意到一个有趣的事情。如前所述,我们的应用程序一次只能处理一个用户。
因此,只有一个用户可以订阅我们的服务。与此同时,日志显示订阅者的数量超过了这个值。 进一步阅读提供了更多证据,证明我们的系统存在问题。
尽管日志没有显示问题发生的地方,但这是防止系统出现问题的第一步。
3.2. 性能分析
与前一个步骤一样,此步骤旨在找到正在运行的应用程序中的异常。然而,性能分析器 可以显著简化对正在运行的应用程序的内存占用的监控。
首先要注意的是,随着时间的推移,使用的内存单调增加。 这并不总是内存泄漏的标志。然而,在像我们这样的应用程序上,内存使用的增加可能是我们有问题的一个很好的迹象。
我们将使用 JConsole 分析器。这是一个基本的分析器,但它提供了所有所需的功能,并包含在每个 JDK 分发中。另外,它很容易在任何系统上启动:
$ jconsole
让我们启动应用程序,看看 JConsole 会告诉我们什么。在启动应用程序后,其内存消耗增加:
然而,内存使用并不总是内存泄漏的迹象。
让我们尝试提示垃圾收集器清理一些死亡对象:
如我们所见,垃圾收集器工作得相当好,清理了一些空间。因此,我们可以假设我们根本没有任何问题。然而,让我们看看 Old Generation。这是应用程序中一些对象经过几次垃圾回收后依然存在的空间。我们可以看到它的大小不断增加:
一个解释是,除了用户之外,我们还有引用。我们的应用程序中没有存储引用的引用,所以垃圾收集器在清理它们时没有问题。与此同时,我们的服务保留了对每个用户的引用,阻止它们被垃圾收集,并将它们提升到 Old Generation:
尽管垃圾收集器定期清理,但很明显,总体内存消耗随着时间的推移在增长。我们在几分钟内从大约 10 MB 增加到了 30 MB。 在服务器上,这可能几个小时甚至几天都不会造成任何问题。如果服务器定期重启,我们可能永远不会看到 OutOfMemoryError:
我们在 old generation 中也有同样的情况:内存消耗只是在增长。对于我们这样的应用程序,一次只能为一个用户提供服务,这是一个问题的迹象。
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 上找到。
原文地址:https://www.baeldung.com/java-create-detect-memory-leaks