教程:查找内存泄漏
最后修改时间:2023 年 9 月 7 日我们经常发现自己处于代码无法正常工作的情况,并且我们不知道从哪里开始调查。
难道我们不能只盯着代码直到解决方案最终出现吗?当然可以,但是如果没有对项目的深入了解和大量的脑力劳动,这种方法可能行不通。更明智的方法是使用您手头的工具。他们可以为您指明正确的方向。
在本教程中,我们将了解如何使用 IntelliJ IDEA 的一些内置工具来调查运行时问题。
笔记
本教程中的大多数工具都可以在 Community Edition 中使用,但对于分析工具,您需要 Ultimate。
问题
让我们从克隆以下存储库开始: https://github.com/flounder4130/party-parrot。
之后,使用Parrot
项目中包含的运行配置启动应用程序。
运行应用程序
按然后选择鹦鹉。AltShiftF10
该应用程序似乎运行良好:您可以调整动画颜色和速度。然而,没过多久事情就开始出问题了。
工作一段时间后,动画冻结,没有任何迹象表明原因是什么。还可能存在一个OutOfMemoryError
,其堆栈跟踪不会告诉我们有关问题根源的任何信息。
没有可靠的方法来说明问题将如何具体表现出来。动画冻结的有趣之处在于我们仍然可以使用 UI 的其余部分。
笔记
我们使用 Amazon Corretto 11 运行此程序。结果在其他 JVM 上甚至在 Corretto 11(如果使用自定义配置)上可能会有所不同。
调试器
看来我们有一个错误。让我们尝试使用调试器!
暂停应用程序
首先,我们需要在调试模式下运行应用程序。按,然后选择鹦鹉。AltShiftF9
等到动画冻结。转到“运行”| 调试操作| 暂停程序。
我们获取线程列表及其当前堆栈跟踪。
不幸的是,这并没有告诉我们太多信息,因为鹦鹉派对中涉及的所有线程都处于等待状态。我们甚至不知道线程是否正在等待锁或刚刚完成当前的工作。显然,我们需要尝试另一种方法。
CPU 和内存实时图表
由于我们得到了一个很好的分析起点,因此CPU 和内存实时图表OutOfMemoryError
是一个很好的分析起点。它们使我们能够可视化正在运行的进程的实时资源使用情况。让我们打开 Parrot 应用程序的图表,看看当动画冻结时我们是否能发现任何东西。
打开 CPU 和内存实时图表
前往查看| 工具窗口 | 探查器。
在Profiler工具窗口中右键单击必要的进程,然后选择CPU 和内存实时图表。
将打开一个新选项卡,您可以在其中查看所选进程消耗的资源量。
事实上,我们看到内存使用量在达到稳定水平之前不断上升。这正是动画挂起的时刻,而且似乎没有办法摆脱这种情况。
这给了我们一个线索。通常,内存使用曲线是锯齿形的:当分配新对象时,图表会上升,当垃圾收集器回收内存时,图表会定期下降。您可以在下图中看到这样的示例:
如果锯齿变得太频繁,则意味着正在创建大量对象,并且经常调用垃圾收集器来回收内存。如果我们看到一个平台期,则意味着垃圾收集器无法释放任何垃圾。
我们可以在CPU 和内存实时图表中测试垃圾收集是否产生任何结果。
调用垃圾回收
如果您需要测试垃圾收集在特定条件下的工作原理,可以向CPU 和内存实时图表请求。为此,请单击执行 GC按钮。
内存使用量在达到稳定水平后不会下降。这支持了我们的假设,即没有符合垃圾回收条件的对象。
由于内存不足,一个简单的解决方案是添加更多内存。
将内存添加到运行配置
按住并单击主工具栏上的运行配置。Shift
在虚拟机选项字段中,输入
-Xmx1024M
。这会将内存堆增加到 1024 MB。
再次运行应用程序。唉,无论有多少可用内存,鹦鹉都会耗尽内存。我们再次看到同一张图片。额外内存唯一明显的影响是我们推迟了“聚会”的结束。
分配分析
由于我们知道我们的应用程序永远不会获得足够的内存,因此我们可能需要分析其内存使用情况。
使用分析器运行
按。选择鹦鹉| 使用“IntelliJ Profiler”进行配置。AltShiftF10
运行时,探查器会记录对象放置在堆上时的应用程序状态。然后,这些数据以人类可读的形式聚合,让我们了解应用程序在分配这些对象时正在做什么。
运行探查器一段时间后,让我们打开报告看看里面有什么。
打开分析器报告
单击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));
}
似乎updateParrot()
需要一些基础图像,然后重新着色。为了避免额外的工作,该实现首先尝试从某个缓存中检索图像。检索的关键是一个State
对象,其构造函数采用基本图像和色调。
分析数据流
使用内置的静态分析器,我们可以跟踪State
构造函数调用的输入值的范围。
分析方法输入
右键单击
baseImage
构造函数参数,然后从菜单中选择分析| 数据流向这里。
展开节点并注意ImageIO.read(path.toFile())
。它向我们表明基础镜像来自一组文件。如果我们双击该行并查看PARROTS_PATH
附近的常量,我们就会发现文件位置:
这是与鹦鹉的可能位置相对应的十个基本图像。那么,hue
构造函数参数呢?让我们对hue
参数运行数据流分析:
如果我们检查修改变量的代码hue
,我们会发现它的起始值为50
。然后它可以使用滑块设置或从updateHue()
方法自动更新。1
无论哪种方式,它总是在to的范围内100
。
因此,我们有100
色调和10
基础图像的变体,这应该保证缓存永远不会增长到超过 1000 个元素。让我们检查一下这是否成立。
条件断点
现在,这就是调试器可以发挥作用的地方。我们可以使用条件断点检查缓存的大小。
让我们在更新操作处设置一个断点,并添加一个条件,以便仅当缓存大小超过 1000 个元素时才挂起应用程序。
设置条件断点
单击行处的装订线
85
并选择行断点。右键单击断点并
cache.size() > 1000
在“条件”字段中输入。
现在以调试模式运行应用程序。
事实上,我们在运行程序一段时间后停在这个断点处。所以我们可以确定问题出在缓存上。
检查代码
Ctrl0Boncache
将我们带到其声明站点:
private static final Map<State, BufferedImage> cache = new HashMap<>();
如果我们检查 的文档HashMap
,我们会发现它的实现依赖于 和equals()
方法hashcode()
,并且用作键的类型必须正确覆盖它们。让我们检查一下。on将我们带到类定义。Ctrl0BState
equals()
看来我们已经找到了罪魁祸首:和的实现hashcode()
不仅仅是不正确的。完全不见了!
重写方法
equals()
为和编写实现hashcode()
是一项平凡的任务。幸运的是,IntelliJ IDEA 可以为我们生成它们。
生成 hashcode() 和 equals()
在课堂上
State
,开始输入equals
,IDE 将理解我们想要的内容。只需接受建议并单击“下一步”,直到方法出现在插入符号处。
提示
您还可以使用它作为覆盖其他成员或生成访问器方法的快速方法。例如,键入
get
将向您显示生成 getter 方法的建议。
检查修复情况
让我们重新启动应用程序,看看情况是否有所改善。同样,我们可以使用CPU 和内存实时图表来实现这一点:
那好多了!
概括
在本教程中,我们研究了如何从问题的一般症状开始,然后使用我们的推理和 IntelliJ IDEA 为我们提供的各种工具,逐步缩小搜索范围,直到找到确切的问题。导致问题的代码行。更重要的是,我们确保鹦鹉派对无论如何都会继续下去!
感谢您的反馈意见!