前言
闲暇之中在优锐课的精讲java中学习中get了,Java应用程序中的内存泄漏,以了解它们的来源以及如何为它们做准备,查找和修复它们,特整理出来分享干货给大家
Java的核心优势之一是JVM,这是一种开箱即用的内存管理。 本质上,我们可以创建对象,而Java Garbage Collector将负责为我们分配和释放内存。
但是,Java应用程序中仍然可能发生内存泄漏。
在本文中,我们将描述最常见的内存泄漏,了解它们的原因,并介绍几种检测/避免它们的技术。 在整篇文章中,我们还将使用Java YourKit探查器,以分析运行时内存的状态
1.什么是Java中的内存泄漏?
内存泄漏的标准定义是以下情况:应用程序不再使用对象,但是垃圾收集器无法将它们从工作内存中删除–因为它们仍在被引用。 结果,应用程序消耗了越来越多的资源-最终导致致命的OutOfMemoryError。
为了更好地理解该概念,以下是一个简单的视觉表示:
如我们所见,我们有两种类型的对象–引用对象和未引用对象; 垃圾收集器可以删除未引用的对象。 引用的对象将不会被收集,即使应用程序实际上不再使用它们也是如此。
检测内存泄漏可能很困难。 许多工具会执行静态分析来确定潜在的泄漏,但是这些技术并不是完美的,因为最重要的方面是正在运行的系统的实际运行时行为。
因此,让我们通过分析一些常见情况来重点研究防止内存泄漏的一些标准做法。
2.Java堆泄漏
在本节的第一部分中,我们将重点介绍经典的内存泄漏情况-不断创建Java对象而不释放它们。
理解这些情况的一种有利技术是通过为Heap设置较小的大小来使再现内存泄漏更容易。 因此,在启动应用程序时,我们可以调整JVM以适应我们的内存需求:
-Xms % 3尺寸%3E
-Xmx % 3尺寸%3E
这些参数指定初始Java堆大小以及最大堆大小。
2.1.保持对象引用的静态字段
可能导致Java内存泄漏的第一种情况是使用静态字段引用重对象。
让我们看一个简单的例子:
private Random random = new Random();
public static final ArrayList<Double> list = new ArrayList<Double>(1000000);
@Test
public void givenStaticField_whenLotsOfOperations_thenMemoryLeak() throws InterruptedException {
for (int i = 0; i < 1000000; i++) {
list.add(random.nextDouble());
}
System.gc();
Thread.sleep(10000); // to allow GC do its job
}
我们将ArrayList创建为一个静态字段-即使在完成了用于它的计算后,在JVM进程的生存期内它也永远不会被JVM Garbage Collector收集。 我们还调用了Thread.sleep(10000)以允许GC执行完整收集并尝试回收所有可以回收的内容。
让我们运行测试并使用分析器分析JVM:
注意,从一开始,当然所有内存都是免费的。
然后,只需2秒钟,迭代过程便会运行并完成-将所有内容加载到列表中(自然地,这将取决于你运行测试的计算机)。
此后,将触发一个完整的垃圾回收周期,并继续执行测试,以允许该周期时间运行并完成。 如你所见,该列表不会被回收,内存消耗也不会减少。
现在让我们看一看完全相同的示例,只是这一次,静态变量未引用ArrayList。 相反,它是一个创建,使用并随后丢弃的局部变量:
@Test
public void givenNormalField_whenLotsOfOperations_thenGCWorksFine() throws InterruptedException {
addElementsToTheList();
System.gc();
Thread.sleep(10000); // to allow GC do its job
}
private void addElementsToTheList(){
ArrayList<Double> list = new ArrayList<Double>(1000000);
for (int i = 0; i < 1000000; i++) {
list.add(random.nextDouble());
}
}
该方法完成工作后,我们将在下图大约50秒处观察主要的GC收集:
请注意,GC现在如何能够回收JVM使用的某些内存。
如何预防
既然你已经了解了这种情况,那么当然有防止这种情况发生的方法。
首先,我们需要密切注意静电的使用; 将任何集合或重对象声明为静态会将其生命周期与JVM本身的生命周期联系在一起,并使整个对象图无法收集。
我们还需要总体上注意集合-这是无意间保留引用的时间长于我们所需的一种常见方法。
2.2在长字符串上调用
String.intern()
第二组经常导致内存泄漏的场景涉及字符串操作-特别是String.intern()API。
让我们看一个简单的例子:
@Test
public void givenLengthString_whenIntern_thenOutOfMemory()
throws IOException, InterruptedException {
Thread.sleep(15000);
String str
= new Scanner(new File("src/test/resources/large.txt"), "UTF-8")
.useDelimiter("A").next();
str.intern();
System.gc();
Thread.sleep(15000);
}
在这里,我们只是简单地尝试将大文本文件加载到运行的内存中,然后使用.intern()返回规范形式。
内部API将str字符串放在JVM内存池中-无法收集它-再次,这将导致GC无法释放足够的内存:
我们可以清楚地看到,在最初的15秒内JVM稳定了,然后我们加载文件并由JVM执行垃圾回收(第20秒)。
最后,调用str.intern()导致内存泄漏-稳定的行表示堆内存使用率很高,永远不会释放。
如何预防
请记住,内嵌的String对象存储在PermGen空间中-如果我们的应用程序打算对大型字符串执行大量操作,则可能需要增加永久代的大小:
-XX:MaxPermSize=<size>
第二种解决方案是使用Java 8-将PermGen空间替换为Metaspace-在字符串上使用intern时不会导致任何OutOfMemoryError:
最后,还有一些避免在字符串上使用.intern()API的选项。
2.3. 未封闭的流
忘记关闭流是一种非常普遍的情况,当然,大多数开发人员都可以涉及到这种情况。 当自动关闭所有类型的流的功能引入try-with-resource子句时,该问题在Java 7中已部分解决。
为什么要部分? 因为try-with-resources语法是可选的:
@Test(expected = OutOfMemoryError.class)
public void givenURL_whenUnclosedStream_thenOutOfMemory()
throws IOException, URISyntaxException {
String str = "";
URLConnection conn
= new URL("http://norvig.com/big.txt").openConnection();
BufferedReader br = new BufferedReader(
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
while (br.readLine() != null) {
str += br.readLine();
}
//
}
让我们看看从URL加载大文件时应用程序的内存外观:
如我们所见,堆使用率随着时间逐渐增加–这是由于不关闭流而导致的内存泄漏的直接影响。
如何预防
我们总是需要记住手动关闭流,或者利用Java 8中引入的自动关闭功能:
try (BufferedReader br = new BufferedReader(
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
// further implementation
} catch (IOException e) {
e.printStackTrace();
}
在这种情况下,BufferedReader会在try语句结束时自动关闭,而无需在显式的finally块中将其关闭。
2.4. 未封闭的连接
这种情况与前一种情况非常相似,主要区别是处理未关闭的连接(例如,到数据库,到FTP服务器等)。 再次,不正确的实现可能造成很多危害,从而导致内存问题。
让我们看一个简单的例子:
@Test(expected = OutOfMemoryError.class)
public void givenConnection_whenUnclosed_thenOutOfMemory()
throws IOException, URISyntaxException {
URL url = new URL("ftp://speedtest.tele2.net");
URLConnection urlc = url.openConnection();
InputStream is = urlc.getInputStream();
String str = "";
//
}
URLConnection保持打开状态,并且可以预料的是内存泄漏:
请注意,垃圾收集器如何无法执行任何操作来释放未使用但引用的内存。 第1分钟后,情况立即清除-GC操作数量迅速减少,导致堆内存使用增加,这导致OutOfMemoryError。
如何预防
答案很简单–我们需要始终以有条理的方式关闭连接。
2.5.将没有hashCode()和equals()的对象添加到HashSet中
一个可能导致内存泄漏的简单但非常常见的示例是对对象缺少其hashCode()或equals()实现的对象使用HashSet。
具体来说,当我们开始将重复的对象添加到Set中时-这只会增长,而不是像往常一样忽略重复项。 添加后,我们也将无法删除这些对象。
让我们创建一个不包含相等或哈希码的简单类:
public class Key {
public String key;
public Key(String key) {
Key.key = key;
}
}
现在,让我们看一下场景:
@Test(expected = OutOfMemoryError.class)
public void givenMap_whenNoEqualsNoHashCodeMethods_thenOutOfMemory()
throws IOException, URISyntaxException {
Map < Object, Object > map = System.getProperties();
while (true) {
map.put(new Key("key"), "value");
}
}
这个简单的实现将在运行时导致以下情况:
注意垃圾收集器如何停止能够在1:40左右回收内存,并注意内存泄漏; 之后,GC收集数量几乎下降了四倍。
如何预防
在这种情况下,解决方案很简单-提供hashCode()和equals()实现至关重要。
这里值得一提的工具是Lombok项目-它通过注释提供了许多默认实现,例如 @EqualsAndHashCode。
3.如何在你的应用程序中查找泄漏源
诊断内存泄漏是一个漫长的过程,需要大量的实践经验,调试技能和详细的应用程序知识。
让我们看看除了标准配置文件之外哪些技术可以为你提供帮助。
3.1. 详细垃圾回收
识别内存泄漏的最快方法之一是启用详细垃圾回收。
通过将verbose:gc参数添加到我们应用程序的JVM配置中,我们可以实现非常详细的GC跟踪。 摘要报告显示在默认错误输出文件中,该文件应有助于你了解如何管理内存。
3.2.做分析
第二种技术是我们整篇文章中一直在使用的技术-这就是分析。 最受欢迎的探查器是Visual VM-在这里,它是开始摆脱命令行JDK工具并进入轻量级配置文件的好地方。
在本文中,我们使用了另一个分析器– YourKit –与Visual VM相比,它具有一些其他更高级的功能。
3.3.查看你的代码
最后,与处理内存泄漏的特定技术相比,这更是一种通用的良好实践。
简而言之–彻底检查代码,定期进行代码检查,并充分利用静态分析工具来帮助你理解代码和系统。
结论
在本教程中,我们实际研究了JVM上的内存泄漏如何发生。了解这些情况如何发生是处理它们的第一步。
然后,拥有技巧和工具来真正了解运行时发生的泄漏(泄漏发生)也很关键。静态分析和仔细的以代码为中心的审查只能做很多事情,而且,到了一天结束时,运行时将向你显示无法在代码中立即识别的更复杂的泄漏。
最后,众所周知,泄漏可能很难找到和繁殖,因为许多泄漏仅发生在通常在生产中发生的高负荷下。在这里,你不仅需要进行代码级分析,还需要研究两个主要方面-再现和早期检测。
再现内存泄漏的最佳,最可靠的方法是在一套良好的性能测试的帮助下,尽可能模拟生产环境的使用模式。
早期检测是可靠的性能管理解决方案甚至早期检测解决方案都可以在其中发挥重大作用的地方,因为这是对生产中的应用程序运行时进行必要洞察的唯一方法。
可以在GitHub上找到本教程的完整实现。这是一个基于Maven的项目,因此可以直接将其导入并按原样运行。
好了各位,以上就是这篇文章的全部内容了,能看到这里的人呀,都是人才。
如果这个文章写得还不错,觉得有点东西的话 求点赞 求关注❤️ 求分享,感谢大家的支持和认可。
还有就是这我总结出了一些架构视频资料和互联网公司java程序员面试涉及到的绝大部分面试题和答案做成了文档和架构视频资料还有完整高清的java进阶架构学习路线图free分享给大家(包括Dubbo、Redis、Netty、zookeeper、Springcloud、分布式、高并发等架构技术资料),有问题的可以关注找我来交流