Java代码中的内存泄漏,springboot中等项目实战

本文讲述了在Java代码中发现的内存泄漏问题,特别是在SpringBoot项目中。作者通过生成大量测试用例观察程序行为,发现在执行过程中内存逐渐消耗,疑似内存泄漏。分析了内存泄漏的可能原因,包括未正确设置null引用和使用不当的引用类型。文章介绍了使用JConsole监控内存,通过hprof和jmap生成堆映像,以及使用jhat分析堆映像来定位问题。最后,作者建议通过深入代码和使用内存分析工具来排查和修复内存泄漏问题。
摘要由CSDN通过智能技术生成

提神了嘛朋友们,先看技术

这段时间我正在研究我继承的一些Java代码。我正在关键的地方做一些速度改进,为了测试我的改进,我需要测试用例来比较不同的实现。不幸的是,手动生成测试用例太耗时了(需要数千个,手动生成一个测试用例需要几分钟甚至几个小时)。不幸的是,随机生成的测试用例也不起作用,因为我的测试用例是(命题的)LTL公式,并且随机生成的一个我期望在实际使用程序时出现的大小,是不太可能令人满意的,而现实生活中的公式是期望的,这在一定程度上得益于程序的持续集成和测试工具。

为了解决这个问题,我设计了一个简单的计划:生成测试用例并根据它们是否可满足对它们进行排序,并为不易分类的公式添加第三个类别(即,在使用简单但快速高效的实现进行测试时内存不足)。这一切导致我产生了数十万个公式,测试它们是否令人满意,收集其中一些,然后丢弃其余的。

这应该很简单,但我注意到我的程序在运行时的奇怪行为:执行之后,它变慢了,容易分类的公式变得更加频繁。在一个例子中,我没有可满足的公式,8000个不可满足的公式和1或2个失败。让计算运行,我有0个可满足的公式,9000个不可满足的公式,还有20个失败。然后程序内存不足。这种行为是不可能的:我在生成过程中捕捉到OutOfMemoryException,然后应该释放所有内存,所以我永远不会耗尽内存。其次,公式是完全随机生成的,因此8000次运行中有2次失败,但1000次运行中有18次失败似乎不太可能。总而言之,这指向了内存中积累的东西。

通常,您可以查看代码和发生内存泄漏的pin点。在垃圾收集语言(如Java)中,通常会以两种方式遇到内存泄漏:垃圾收集器看不到某些内存不再使用,或者您忘记删除对不再使用的对象的引用。前者不太可能,因为Java使用非常复杂的技术,包括遍历整个堆的标记和扫描算法(尽管这不是非常频繁地运行,因为它的计算成本很高)。这两者中的哪一个是真正的问题并不重要,因为解决方法是相同的:确保将不再使用的引用设置为null,这样垃圾收集器就可以发现它们不再使用。

您可能有兴趣看看java.lang.ref package。它包含的类可以让您与垃圾收集器交互,特别是WeakReference,它引用一个对象,但不会强制它停留在内存中,因此,如果只有对对象的弱引用,则可以对它进行垃圾收集,而SoftReference,它引用的对象应该保存在内存中,但如果程序内存不足,则可以对其进行垃圾回收。弱引用对于实现工厂非常有用,这些工厂确保a.equals(b)那么a==b。如果对复杂对象进行大量比较,这是一个优势,可以通过向工厂添加一组生成的对象并确保始终在该集中查找对象并在要求时返回相同的实例来实现创造一个相等的。不幸的是,除非使用弱引用,否则您的集合将使对象保持活动状态,从而导致内存泄漏。软引用对于创建复杂计算的内存缓存或从较慢的存储中获得的值非常有用。可以很容易地重新计算/重新读取该值,但不这样做会更快。因此,如果可能,缓存的值应该保存在内存中,但是如果应用程序需要内存用于其他用途,则回收缓存比将重要值交换到磁盘更有用。

使用JConsole监视程序

==============

首先要做的是弄清楚程序是否真的在泄漏内存。我们可以使用JConsole来实现这一点,JConsole附带JDK表单Sun(或Oracle,但我不喜欢它们,因为它们将java api放在一个速度较慢的服务器上)。我们只需启动JConsole(在osx或Unix上,只要在提示符处键入JConsole,在Windows上,我想可以在开始菜单中找到JConsole)。首先,我们选择要监控的流程:

当JConsole启动时,我们切换到Memory选项卡并监视内存。由于我们对长时间行为感兴趣,我们选择只显示堆对象的最老年代。年轻化的波动很大,我像疯了一样分配和销毁物品,我们只关心是否有东西积累起来,这意味着它最终会被提升到旧的状态。为了更好地衡量,我们可以偶尔点击performgc按钮来清理内存。这会使使用的内存略有减少,但您仍会得到这样的总体趋势:

这是一个不健康程序的内存配置文件:大小只会增加。3或4个颠簸是因为我按了执行GC按钮。当内存消耗达到2GB时,它会变平(因为我已经强迫Java最多使用2GB内存)。当内存达到2GB时,性能也会明显下降,我的程序现在每隔一段时间就会停止执行垃圾回收。这肯定很糟糕。

生成堆映像

=====

现在,我们已经确定我的代码包含内存泄漏。我们如何从那里继续?一种方法是遍历我们的代码并随机删除引用,直到您不再看到错误的行为,但这有点特别,而且很难知道何时完成。相反,我们将使用内存分析器。幸运的是,JDK内置了所有需要的工具。您所要做的就是创建一个堆映像,并对孤立对象进行分析。

基本上可以用两种方法生成堆,每种方法各有优缺点:使用hprof或使用jmap。使用hprof要慢得多,但可以提供更多信息,例如类名和分配堆栈,而jmap全速运行程序,但只提供某些类的名称。这一部分是基于这篇博客文章,但我的文章使选项和阶段之间的区别更清楚,并提供了图片。

使用Hprof

=======

只需使用hprof启动您的程序:

java -Dcom.sun.management.jmxremote \

-Dcom.sun.management.jmxremote.port=9000 \

-Dcom.sun.management.jmxremote.authenticate=false \

-Dcom.sun.management.jmxremote.ssl=false \

-agentlib:hprof=heap=dump,file=hprof.bin,format=b,depth=10 \

-jar java_app.jar

这基本上启动探查器,禁用所有安全性并告诉它转储到一个名为hprof.bin文件. 如果您的程序不在jar文件中,您可以简单地用应用程序的主类替换最后一行。现在练习您的程序,完成后,按Ctrl-C转储堆并退出应用程序。

使用Jmap

======

正常启动程序,可以从命令行、文件管理器(Finder或Windows资源管理器或现在Lunix用户使用的任何讨厌的工具)或IDE启动程序。然后找到程序的进程id。您可以用几种不同的方法来实现这一点,但更简单的方法是查看JConsole,在JConsole中可以使用它。在上面的示例中,进程id是59735(请参阅进程选择屏幕上的PID列或监视进程时的标题栏)。现在练习你的程序。

然后使用jmap转储图像:

jmap -dump:format=b,file=hprof.bin 59735

记住用JConsole找到的进程id替换59735。

训练你的程序

======

我们现在从两个不同的分支合并。你的读者没有比你更愉快的阅读体验,但是你呢?嗯,我们需要锻炼我们的计划。也就是说,执行我们认为会导致错误的操作。在我的例子中,我应该启动程序并让它运行,但是你可能需要加载一个文件,打印它或者你的程序做什么。重复几次,这样堆中的错误就更明显了。

分析堆映像

=====

当我们使用任何一种

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值