导语:我将在本文详解Java应用程序中的内存泄露,并顺带介绍一些检测和缓解泄露的方法。

Java-Memory-Leaks-793x397.webp.jpg

Java的核心优势之一就是利用JVM(Java虚拟机),JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。一句话,它是一种开箱即用的内存管理。你只管创建对象,Java的垃圾回收器帮你分配以及回收内存。然而,实际的情况并没有那么简单,因为内存泄漏在Java应用程序中还是时有发生的。Java垃圾回收器是Java虚拟机(JVM)的三个重要模块(另外两个是解释器和多线程机制)之一,为应用程序提供内存的自动分配(Memory Allocation)、自动回收(Garbage Collect)功能,这两个操作都发生在Java堆上(一段内存快)。

尽管如此,Java应用程序仍然会发生内存泄漏。

我将在本文详解Java应用程序中的内存泄露,并顺带介绍一些检测和缓解泄露的方法。不过,我要提前说一下,在本文中,我使用的都是java性能分析利器YourKit Java Profiler来分析运行时内存状态的。

1 什么是Java的内存泄漏?

内存泄漏的标准定义是当应用程序不再使用对象时发生的情况,但是垃圾回收器无法将其从工作内存中删除,因为它们仍在被引用。因此,应用程序会消耗越来越多的资源,最终导致致命的OutOfMemoryError。

垃圾回收操作需要消耗CPU、线程、时间等资源,所以容易理解的是垃圾回收操作不是实时发生,当内存消耗完或者是达到某一个指标,才能触发垃圾回收操作。

下面的这张图就解释了什么是无用对象以及什么是未被引用对象:

1.webp.jpg

可以看到,有两种类型的对象——引用和未引用,垃圾回收器可以删除未被引用的对象。即使不再被应用程序使用,也不会收集被引用的对象。

检测内存泄漏可能很困难,虽然许多工具都会执行静态分析来确定潜在的泄漏事故,但这些技术并不完美,因为它们无法对运行系统的实际运行时行为作出判断。

所以,通过分析一些常见的情况,让我们来重点关注一些防止内存泄漏的标准方法。 

2 Java堆泄漏

在不释放Java对象的情况下,不断地创建Java对象,这是内存泄漏的常见场景。就让我从整个场景谈起。

理解这个内存泄漏场景的一个有利的技术就是通过为堆设置较小的内存来简化过程,再现内存泄漏。这就是为什么当启动我的应用程序时,可以通过调整JVM以适应测试的内存需求:

2.jpg

这些参数是用来指定初始Java堆大小以及最大堆大小的。

2.1 静态字段保持对象引用

第一种可能导致Java内存泄漏的场景是用一个静态字段引用一个重对象。

来看一个简单的例子:

3.jpg

当我将ArrayList创建为一个静态字段时,在JVM进程中,JVM垃圾回收器将永远不会收集这些字段,即使在计算完成之后也是如此。我还会调用Thread.sleep(10000)来允许GC执行完整的收集,并尝试收回所有可以回收的内容。

现在,我就可以开始测试并使用YourKit Java Profiler来分析JVM。

4.webp.jpg

注意,一开始,所有的内存当然都是正常的。然后,只需2秒钟,迭代过程就会运行并完成 ,此时所有内容都将加载到列表中(当然具体时间取决于你正在运行测试的设备)。

接下来,会触发一个完整的垃圾回收的进程,然后继续执行测试,直到回收进程的运行时间结束时该进程才停止。正如你所看到的,这个列表不会被回收,而且内存消耗也不会下降。

现在让我们来看看完全相同的例子,不过这一次,ArrayList没有被静态变量引用。相反,它被设置成一个本地变量,然后被创建、使用,然后被回收。

5.jpg

一旦使用该方法完成回收,你就将观察到大量的GC收集,大约50秒后,会出现下面的图像:

6.webp.jpg

请注意,GC现在是如何回收JVM使用的一些内存的?

如何预防?

现在你已经明白了泄露发生的原因,当然也有办法阻止它的发生了。

首先,你需要密切关注静态分析的整个过程,要将JVM进程中的任何集合或重对象都进行静态标记,并使所有对象无法被收集。

另外,你也需要了解总体的集合,将引用时间加长。

2.2 在长字符串上调用String.intern()

经常导致内存泄漏的第二种情况会涉及字符串操作,特别是String.intern()API。

来看一个简单的例子:

7.jpg

现在,我会简单地尝试将大文本文件加载到运行内存中,然后使用.intern()返回规范形式。

intern API 会将把str字符串放在JVM内存池中,不过在内存池中str字符串不但不能被收集,而且还会导致GC无法释放足够的内存。

8.webp.jpg

你可以清楚地看到,在15秒内JVM是稳定的,然后当我加载文件时,JVM将执行垃圾回收(第20秒)。

最后,str.intern()被调用,导致内存泄漏 ,上图中稳定的线条表示堆内存使用率长时间处于高位,这表明堆内存永远不会被释放。

如何预防?

请记住,.intern()字符串对象会被存储在PermGen空间中,如果你的应用程序打算对大字符串执行操作,则可能需要增加持续生成的内存大小。

9.jpg

第二个解决方案是使用Java 8, 这样,PermGen空间被Metaspace替换,在字符串中使用.intern()时不会导致任何OutOfMemoryError:

10.webp.jpg

最后,还有几个避免在字符串中使用.intern()API的选项。

2.3 忘记关闭Stream

大多数开发人员都可能忘记关闭Stream,在Java 7中,当自动关闭所有类型的Stream的功能被置入到 try-with-resource clause中时,忘记关闭Stream这事就基本上不会存在了。

为什么说是基本上呢?因为try-with-resources语法是可选的:

11.jpg

让我们来看看当从URL加载一个大文件时,应用程序的内存是怎样运行的吧。

12.webp.jpg

如上图所示,堆的使用量随着时间的推移而逐渐增加,这就是不关闭Stream而造成的内存溢出。从技术上讲,未关闭的Stream将导致两种类型的泄漏:低层资源泄漏(low-level resource leak )和内存泄漏。

低层资源泄漏就是操作系统层级的资源泄漏,比如文件描述符,打开的连接等。这些资源也可能泄漏,就像内存一样。

当然,JVM也会跟踪这些底层资源,这也是导致内存泄漏的原因。

如何预防呢?

记住不要忘记关闭Stream(手动操作的情况下),或者使用Java 8中引入的自动关闭功能。

13.jpg

在这种情况下,BufferedReader将在try语句结束时自动关闭,而不需要在明显的finally块中关闭它。

2.4未关闭的连接

这与上面的忘记关闭Stream得情境非常相似,它们之间的主要区别主要表现为处理未关闭的连接,例如数据库,FTP服务器等。同理,不适当的执行也会造成很大的损害,导致内存问题。

我们来看一个简单的例子:

14.jpg

URLConnection仍然打开,其结果就是会发生内存泄漏。

15.webp.jpg

请注意,垃圾回收器是如何释放未使用但已引用的内存。从上图可以看到,1分钟后, GC操作的数量迅速减少,从而导致堆内存使用量剧增,最后发生OutOfMemoryError。

如何预防呢?

答案很简单,记得关闭连接即可。

2.5 将没有hashCode()和equals()的对象添加到HashSet中

一个简单但非常常见的导致内存泄漏的例子是使用HashSet,其中的对象缺少hashCode()或equals()实现。

具体来说,当你开始将重复的对象添加到一个集合中时,占用的资源会不断地增长,而不会因为是重复字段就把它们忽略掉,更重要的是,一旦添加完了,你也将无法删除这些对象了。

如下所示,我会创建一个没有equals或hashCode的简单类:

16.jpg

现在,让我们看看会发生什么。

17.jpg

这个简单类在运行时会发生以下情境:

18.webp.jpg

不知你有没有注意到,垃圾回收器是如何在1:40左右停止回收内存的,与此同时内存泄漏也发生了,之后,GC收集的数量几乎下降了四倍。

如何预防呢?

解决方法很简单,只需提供hashCode()和equals()实现即可。在此,我要推荐一个工具——Project Lombok,它通过注释提供了大量的默认实现,例如@ equalsandhashcode

3 如何在你的应用程序中找到泄漏源

发现内存泄漏是一个即漫长又费力的过程,且需要大量的实际经验,调试技巧和应用程序的背景知识。

下面,就让我们看看有哪些技术可以帮你快速地找到内存泄漏?

3.1 详细的垃圾回收

识别内存泄漏的最快方法之一是启用详细的垃圾回收。

通过将-verbose:gc参数添加到我们应用程序的JVM配置中,就可以启用非常详细的GC跟踪。此时,默认错误输出文件中就会出现汇总报告,这应该有助于你了解内存管理的情况。

3.2 进行一定的分析

第二种技术就是我通篇使用的技术——性能分析,性能分析中最受欢迎的工具便是Visual VM ,VisualVM 是一款免费的,集成了多个 JDK 命令行工具的可视化工具,它能为你提供强大的分析能力,对 Java 应用程序做性能分析和调优。

不过在本文中,我使用的是另一个性能分析工具—— YourKit,它与Visual VM相比,具有一些附加的、更高级的功能。

3.3 查看你的代码

定期进行代码审查,并充分利用静态分析工具来帮助你了解代码和系统的状况。

4 总结

我在本文中详细介绍了JVM上的内存泄漏是如何发生的,并介绍了一些提前预防的方法和工具。

本文的完整实现方法可以在GitHub上找到,这是一个基于Maven的项目,所以你可以直接导入并运行

源链接

Hacking more

...