我的程序跑了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:


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


目录
相关文章
|
3月前
|
安全 Linux Shell
Linux上执行内存中的脚本和程序
【9月更文挑战第3天】在 Linux 系统中,可以通过多种方式执行内存中的脚本和程序:一是使用 `eval` 命令直接执行内存中的脚本内容;二是利用管道将脚本内容传递给 `bash` 解释器执行;三是将编译好的程序复制到 `/dev/shm` 并执行。这些方法虽便捷,但也需谨慎操作以避免安全风险。
196 6
|
21天前
|
监控 Oracle Java
JDK 21中的分代ZGC:一场内存管理的革命
JDK 21引入了分代ZGC,为Java应用程序的内存管理带来了革命性的进步。分代ZGC通过将堆内存划分为年轻代和老年代,采用并发处理和染色指针技术,实现了高吞吐量、低延迟和更好的可扩展性。这一特性显著提升了系统的性能和稳定性。
127 51
|
2月前
|
NoSQL 测试技术
内存程序崩溃
【10月更文挑战第13天】
141 62
|
24天前
|
存储 缓存 Java
结构体和类在内存管理方面的差异对程序性能有何影响?
【10月更文挑战第30天】结构体和类在内存管理方面的差异对程序性能有着重要的影响。在实际编程中,需要根据具体的应用场景和性能要求,合理地选择使用结构体或类,以优化程序的性能和内存使用效率。
|
2月前
|
存储 安全 Java
jdk21的外部函数和内存API(MemorySegment)(官方翻译)
本文介绍了JDK 21中引入的外部函数和内存API(MemorySegment),这些API使得Java程序能够更安全、高效地与JVM外部的代码和数据进行互操作,包括调用外部函数、访问外部内存,以及使用不同的Arena竞技场来分配和管理MemorySegment。
53 1
jdk21的外部函数和内存API(MemorySegment)(官方翻译)
|
2月前
|
存储 程序员 编译器
简述 C、C++程序编译的内存分配情况
在C和C++程序编译过程中,内存被划分为几个区域进行分配:代码区存储常量和执行指令;全局/静态变量区存放全局变量及静态变量;栈区管理函数参数、局部变量等;堆区则用于动态分配内存,由程序员控制释放,共同支撑着程序运行时的数据存储与处理需求。
124 21
|
3月前
|
C语言 Android开发 C++
基于MTuner软件进行qt的mingw编译程序的内存泄漏检测
本文介绍了使用MTuner软件进行Qt MinGW编译程序的内存泄漏检测的方法,提供了MTuner的下载链接和测试代码示例,并通过将Debug程序拖入MTuner来定位内存泄漏问题。
基于MTuner软件进行qt的mingw编译程序的内存泄漏检测
|
2月前
|
监控 数据可视化 Java
如何使用JDK自带的监控工具JConsole来监控线程池的内存使用情况?
如何使用JDK自带的监控工具JConsole来监控线程池的内存使用情况?
|
3月前
|
安全 Java API
【性能与安全的双重飞跃】JDK 22外部函数与内存API:JNI的继任者,引领Java新潮流!
【9月更文挑战第7天】JDK 22外部函数与内存API的发布,标志着Java在性能与安全性方面实现了双重飞跃。作为JNI的继任者,这一新特性不仅简化了Java与本地代码的交互过程,还提升了程序的性能和安全性。我们有理由相信,在外部函数与内存API的引领下,Java将开启一个全新的编程时代,为开发者们带来更加高效、更加安全的编程体验。让我们共同期待Java在未来的辉煌成就!
71 11
|
3月前
|
安全 Java API
【本地与Java无缝对接】JDK 22外部函数和内存API:JNI终结者,性能与安全双提升!
【9月更文挑战第6天】JDK 22的外部函数和内存API无疑是Java编程语言发展史上的一个重要里程碑。它不仅解决了JNI的诸多局限和挑战,还为Java与本地代码的互操作提供了更加高效、安全和简洁的解决方案。随着FFM API的逐渐成熟和完善,我们有理由相信,Java将在更多领域展现出其强大的生命力和竞争力。让我们共同期待Java编程新纪元的到来!
105 11