我的程序跑了60多小时,就是为了让你看一眼JDK的BUG导致的内存泄漏。 (2)

简介: 我的程序跑了60多小时,就是为了让你看一眼JDK的BUG导致的内存泄漏。 (2)

然后在凌晨 2 点 57 分(这个时间点,大佬都是不用睡觉的吗?还是说刚修完福报,下班了), max 回复到:


我不敢相信 CLQ 使用起来会有这样的问题,他们至少应该在 API 文档里面说明一下。


这里的他们,应该指的是 JDK 团队的成员,特指 Doug Lea,毕竟是他老爷子的作品。


为什么没有在 API 文档里面说明呢?


因为他们自己也不知道有这个 BUG 啊。


Greg 连着回复了两条,并且直接指出了解决方案:


问题的原因是 remove 方法的源码里面,有上图中标号为 ① 的这样一行代码。


这行代码会去取消被移除的这个 node (其值已经被替换为 null)和 list 之间的链接,然后可以让 GC 回收这个 node。


但是,当集合里面只有一个元素的时候, next != null 这个判断是不成立的。


所以就会出现这个需要移除的节点已经被置为 null 了,但却没有取消和队列之间的连接,导致 GC 线程不会回收这个节点。


他给出的解决方案也很简单,就是标号为②、③的地方。总之,只需要让代码执行 pred.casNext 方法就行。


总之一句话,导致内存泄漏的原因是一个被置为 null 的 node,由于代码问题,导致该 node 节点,既不会被使用,也不会被 GC 回收掉。


如果你还没理解到这个 BUG 的原因,说明你对 CLQ 这个队列的结构还不太清晰。


那么我建议你读一下《Java并发编程的艺术》这一本书,里面有一小节专门讲这个队列的,图文并茂,写的还是非常清晰。



这个 BUG 在 jetty 里面的来龙去脉算是说清楚了。


然后,我们再回到 JDK BUG 的这个链接中去:


他这里写的原因就是我前面说的原因,没有 unlink,所以不能被回收。


而且他说到:这个 BUG 在最新的JDK 7、8和9版本中都存在。


他说的最新是指截止这个 BUG 被提出来之前:


Demo跑起来


这一小节里面,我们跑一下 Greg 给的那个修复 Demo,亲手去摸一下这个 BUG 的样子。


https://bugs.eclipse.org/bugs/attachment.cgi?id=256704


你可以打开上面那个链接,直接复制粘贴到你的 IDEA 里面去:


注意第 13 行,因为 Greg 给的是修复 Demo,所以用的是 ConcurrentHashSet,由于我们要演示这个bug,所以使用 CLQ。


这个 Demo 就是在死循环里面调用 queue 的 add(obj) 和 remove(obj) 方法。每循环 10000 次,就打印出时间间隔、队列大小、最大内存、剩余内存、总内存的值。


最终运行起来的效果是这样的(JDK 版本是 1.7.0_71):


可以看到每次打印 duration 这个时间间隔是越来越大,队列大小始终为 1。


后面三个内存相关的参数可以先不关心,下一小节我们用图形化工具来看。


你知道上面这个程序,到我写文章写到这里的时候,我跑了多久了吗?


61 小时 32 分 53 秒。


最新一次循环 10000 次所需要的时间间隔是 575615ms,快接近 10 分钟:


这就是 Greg 说的:不仅仅是内存泄漏,而且越来越慢。


但是,同样的程序,我用 JDK 1.8.0_212 版本跑的时候情况却是这样的:


时间间隔很稳定,不会随着时间的推移而增加。


说明这个版本是修复了这个 BUG 的,我带大家看看源码:


JDK 1.8.0_212 版本的源码里面,在 CLQ 的 remove(obj) 方法的 502 行末尾注释了一个 unlink。


官方的修复方法可以看这里:


http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/rev/8efe549f3c87


改动比较多,但是原理还是和之前分析的一样:


我仅仅在两个 JDK 版本中跑过示例代码。


在 JDK 1.8.0_212 没有发现内存泄漏的问题,我看了对应的 remove(obj) 方法的源码确实是修复了。


在 JDK 1.7.0_71 中可以看到内存泄漏的问题。


unlink,一个简简单单的词,背后原来藏了这么多故事。


jconsole、VisualVM、jmc


既然都说到内存泄漏了,那必须得介绍几个可视化的故障排除工具。


前面说了,这个程序跑了 61 个小时了,给大家看一下这个时间段里面堆内存的使用情况:


可以看到整个堆内存的使用量是一个明显的、缓慢的上升趋势。


上面这个图就是来自 jconsole。


结合程序,通过图片我们可以分析出,这种情况一定是内存泄漏了,这是一个非常经典的内存泄漏的走势。


接下来,我们再看一下 jmc 的监控情况:


上面展示的是已经使用的堆内存的大小,走势和 jconsole 的走势一样。


然后再看看 VisualVM 的图:


VisualVM 的图,我不知道怎么看整个运行了 60 多小时的走势图,但是从上面的图也是能看出是有上升趋势的。


在 VisualVM 里面,我们可以直接 Dump 堆,然后进行分析:


可以清楚的看到, CLQ 的 Node 的大小占据了 94.2%。


但是,从我们的程序来看,我们根本就没有用到这么多 Node。我们只是用了一个而已。


你说,这不是内存泄漏是什么。


内存泄漏最终会导致 OOM。


所以当发生 OOM 的时候,我们需要分析是不是有内存泄漏。也就是看内存里面的对象到底应不应该存活,如果都应该存活那就不是内存泄漏,是内存不足了。需要检查一下 JVM 的参数配置(-Xmx/-Xms),根据机器内存情况,判断是否还能再调大一点。


同时,也需要检查一下代码,是否存在生命周期过程的对象,是否有数据结构使用不合理的地方,尽量减少程序运行期的内存消耗。


我们可以通过把堆内存设置的小一点,来模拟一下内存泄漏导致的 OOM。


还是用之前的测试案例,但是我们指定 -Xmx 为 20m,即最大可用的堆大小为 20m。


然后把代码跑起来,同时通过 VisualVM 、jconsole、jmc 这三个工具监控起来,为了我们有足够的时候准备好检测工具,我在第 8 行加入休眠代码,其他的代码和之前的一样:


加入 -Xmx20m 参数:


运行起来之后,我们同时通过工具来查看内存变化,下面三个图从上到下的工具分别是 VisualVM、jconsole、jmc:


从图片的走势来看,和我们之前分析的是一样的,内存一直在增长。


目录
相关文章
|
23天前
|
安全 Linux Shell
Linux上执行内存中的脚本和程序
【9月更文挑战第3天】在 Linux 系统中,可以通过多种方式执行内存中的脚本和程序:一是使用 `eval` 命令直接执行内存中的脚本内容;二是利用管道将脚本内容传递给 `bash` 解释器执行;三是将编译好的程序复制到 `/dev/shm` 并执行。这些方法虽便捷,但也需谨慎操作以避免安全风险。
|
21天前
|
安全 Java API
【性能与安全的双重飞跃】JDK 22外部函数与内存API:JNI的继任者,引领Java新潮流!
【9月更文挑战第7天】JDK 22外部函数与内存API的发布,标志着Java在性能与安全性方面实现了双重飞跃。作为JNI的继任者,这一新特性不仅简化了Java与本地代码的交互过程,还提升了程序的性能和安全性。我们有理由相信,在外部函数与内存API的引领下,Java将开启一个全新的编程时代,为开发者们带来更加高效、更加安全的编程体验。让我们共同期待Java在未来的辉煌成就!
46 11
|
22天前
|
安全 Java API
【本地与Java无缝对接】JDK 22外部函数和内存API:JNI终结者,性能与安全双提升!
【9月更文挑战第6天】JDK 22的外部函数和内存API无疑是Java编程语言发展史上的一个重要里程碑。它不仅解决了JNI的诸多局限和挑战,还为Java与本地代码的互操作提供了更加高效、安全和简洁的解决方案。随着FFM API的逐渐成熟和完善,我们有理由相信,Java将在更多领域展现出其强大的生命力和竞争力。让我们共同期待Java编程新纪元的到来!
43 11
|
14天前
|
监控 数据可视化 Java
使用JDK自带的监控工具JConsole来监控线程池的内存使用情况
使用JDK自带的监控工具JConsole来监控线程池的内存使用情况
|
19天前
|
监控 Java 大数据
【Java内存管理新突破】JDK 22:细粒度内存管理API,精准控制每一块内存!
【9月更文挑战第9天】虽然目前JDK 22的确切内容尚未公布,但我们可以根据Java语言的发展趋势和社区的需求,预测细粒度内存管理API可能成为未来Java内存管理领域的新突破。这套API将为开发者提供前所未有的内存控制能力,助力Java应用在更多领域发挥更大作用。我们期待JDK 22的发布,期待Java语言在内存管理领域的持续创新和发展。
|
21天前
|
存储 运维
.NET开发必备技巧:使用Visual Studio分析.NET Dump,快速查找程序内存泄漏问题!
.NET开发必备技巧:使用Visual Studio分析.NET Dump,快速查找程序内存泄漏问题!
|
1月前
|
存储 安全 Java
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程是什么,JDK、JRE、JVM的联系与区别;什么是程序计数器,堆,虚拟机栈,栈内存溢出,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
|
1月前
|
存储 Java API
【Azure Developer】通过Azure提供的Azue Java JDK 查询虚拟机的CPU使用率和内存使用率
【Azure Developer】通过Azure提供的Azue Java JDK 查询虚拟机的CPU使用率和内存使用率
|
1月前
|
监控 Java API
如何从 Java 程序中查找内存使用情况
【8月更文挑战第22天】
18 0
|
1月前
|
Oracle Java 关系型数据库
简单记录在Linux上安装JDK环境的步骤,以及解决运行Java程序时出现Error Could not find or load main class XXX问题
本文记录了在Linux系统上安装JDK环境的步骤,并提供了解决运行Java程序时出现的"Error Could not find or load main class XXX"问题的方案,主要是通过重新配置和刷新JDK环境变量来解决。
74 0