教程:查找内存泄漏
最后修改时间:2023 年 9 月 7 日
我们经常发现自己处于代码无法正常工作的情况,并且我们不知道从哪里开始调查。
难道我们不能只盯着代码直到解决方案最终出现吗?当然可以,但是如果没有对项目的深入了解和大量的脑力劳动,这种方法可能行不通。更明智的方法是使用您手头的工具。他们可以为您指明正确的方向。
在本教程中,我们将了解如何使用 IntelliJ IDEA 的一些内置工具来调查运行时问题。
笔记
本教程中的大多数工具都可以在 Community Edition 中使用,但对于分析工具,您需要 Ultimate。
问题
让我们从克隆 以下存储库开始: https://github.com/flounder4130/party-parrot 。
之后,使用
Parrot
项目中包含的运行配置启动应用程序。
运行应用程序
-
按然后选择 鹦鹉 。 Alt Shift F10
该应用程序似乎运行良好:您可以调整动画颜色和速度。然而,没过多久事情就开始出问题了。
工作一段时间后,动画冻结,没有任何迹象表明原因是什么。还可能存在一个
OutOfMemoryError
,其堆栈跟踪不会告诉我们有关问题根源的任何信息。

没有可靠的方法来说明问题将如何具体表现出来。动画冻结的有趣之处在于我们仍然可以使用 UI 的其余部分。
笔记
我们使用 Amazon Corretto 11 运行此程序。结果在其他 JVM 上甚至在 Corretto 11(如果使用自定义配置)上可能会有所不同。
调试器
看来我们有一个错误。让我们尝试使用调试器!
暂停应用程序
-
首先,我们需要在调试模式下运行应用程序。按,然后选择 鹦鹉 。 Alt Shift F9
-
等到动画冻结。转到 “运行”| 调试操作| 暂停程序 。
我们获取线程列表及其当前堆栈跟踪。

不幸的是,这并没有告诉我们太多信息,因为鹦鹉派对中涉及的所有线程都处于等待状态。我们甚至不知道线程是否正在等待锁或刚刚完成当前的工作。显然,我们需要尝试另一种方法。
CPU 和内存实时图表
由于我们得到了一个很好的分析起点,因此
CPU 和内存实时图表
OutOfMemoryError
是一个很好的分析起点。它们使我们能够可视化正在运行的进程的实时资源使用情况。让我们打开 Parrot 应用程序的图表,看看当动画冻结时我们是否能发现任何东西。
打开 CPU 和内存实时图表
-
前往 查看| 工具窗口 | 探查器 。
-
在Profiler 工具窗口中右键单击必要的进程,然后选择 CPU 和内存实时图表 。
将打开一个新选项卡,您可以在其中查看所选进程消耗的资源量。
事实上,我们看到内存使用量在达到稳定水平之前不断上升。这正是动画挂起的时刻,而且似乎没有办法摆脱这种情况。

这给了我们一个线索。通常,内存使用曲线是锯齿形的:当分配新对象时,图表会上升,当垃圾收集器回收内存时,图表会定期下降。您可以在下图中看到这样的示例:

如果锯齿变得太频繁,则意味着正在创建大量对象,并且经常调用垃圾收集器来回收内存。如果我们看到一个平台期,则意味着垃圾收集器无法释放任何垃圾。
我们可以在CPU 和内存实时图表 中测试垃圾收集是否产生任何结果。
调用垃圾回收
-
如果您需要测试垃圾收集在特定条件下的工作原理,可以向CPU 和内存实时图表 请求。为此,请单击 执行 GC 按钮。
内存使用量在达到稳定水平后不会下降。这支持了我们的假设,即没有符合垃圾回收条件的对象。
由于内存不足,一个简单的解决方案是添加更多内存。
将内存添加到运行配置
-
按住并单击主工具栏上的运行配置。 Shift
-
在 虚拟机选项 字段中,输入
-Xmx1024M
。这会将内存堆增加到 1024 MB。
再次运行应用程序。唉,无论有多少可用内存,鹦鹉都会耗尽内存。我们再次看到同一张图片。额外内存唯一明显的影响是我们推迟了“聚会”的结束。

分配分析
由于我们知道我们的应用程序永远不会获得足够的内存,因此我们可能需要分析其内存使用情况。
使用分析器运行
-
按。选择 鹦鹉| 使用“IntelliJ Profiler”进行配置 。 Alt Shift F10
运行时,探查器会记录对象放置在堆上时的应用程序状态。然后,这些数据以人类可读的形式聚合,让我们了解应用程序在分配这些对象时正在做什么。
运行探查器一段时间后,让我们打开报告看看里面有什么。
打开分析器报告
-
单击Profiler 工具窗口按钮附近出现的气球。
默认情况下,IntelliJ Profiler 显示 CPU 样本数据。为了分析内存分配,我们需要切换到该模式:
切换观看模式
-
使用工具窗口右上角的菜单。
有多种视图可用于收集的数据。在本教程中,我们将使用火焰图。它将收集的堆栈聚合在单个堆栈状结构中,根据收集的样本数量调整元素宽度。最宽的元素代表分析期间分配最多的类型。

这里需要注意的重要一点是,大量分配并不一定表明存在问题。仅当分配的对象无法被垃圾收集时才会发生内存泄漏,因为它们是从正在运行的应用程序中的某个位置引用的。虽然分配分析没有告诉我们有关垃圾收集的任何信息,但它仍然可以为我们提供进一步调查的提示。
让我们看看两个最大质量的元素
byte[]
和
int[]
是从哪里来的。堆栈的顶部告诉我们这些数组是在图像处理过程中由
java.awt.image
包中的代码创建的。堆栈的底部告诉我们,所有这一切都发生在由执行程序服务管理的单独线程中。我们不是在寻找库代码中的错误,所以让我们看看介于两者之间的用户代码。
从上到下,我们看到的第一个应用程序方法是
recolor()
,它又被 调用
updateParrot()
。从名字上看,这个方法正是让我们的鹦鹉移动的方法。让我们看看它是如何实现的以及为什么需要这么多数组。
跳转至源码
-
单击框架并选择 跳转到源 。这将带我们到相应方法的源代码。
导航到声明站点后,我们看到以下代码:
public void updateParrot() {
currentParrotIndex = (currentParrotIndex + 1) % parrots.size();
BufferedImage baseImage = parrots.get(currentParrotIndex);
State state = new State(baseImage, getHue());
BufferedImage coloredImage = cache.computeIfAbsent(state, (s) -> Recolor.recolor(baseImage, hue));
parrot.setIcon(new ImageIcon(coloredImage));