这次的文章从JDK的J.U.C包下的ConcurrentLinkedQueue队列的一个BUG讲起。jetty框架里面的线程池用到了这个队列,导致了内存泄漏。
同时通过jconsole、VisualVM、jmc这三个可视化监控工具,让你看见“内存泄漏”的发生。有点意思,大家一起看看。
从一个BUG说起
前段时间翻到了一个 JDK 有点意思的 BUG,带大家一起瞅瞅。
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8137185
memory leak,内存泄漏。
是谁导致的内存泄漏呢?
ConcurrentLinkedQueue,这个队列。
这个 BUG 里面说,在 jetty 项目里面也爆出了这个 BUG:
我看了一下,觉得 jetty 的这个写的挺有意思的。
我按照 jetty 的这个讲吧,反正都是同一个 JDK BUG 导致的。地址如下:
https://bugs.eclipse.org/bugs/show_bug.cgi?id=477817
我用我八级半的蹩脚英语给大家翻译一下这个叫做 max 的同学说了些什么。
他说:在 Java 项目里面,错误的使用 ConcurrentLinkedQueue(文章后面用缩写 CLQ 代替)会导致内存泄漏的问题。
在 jetty 的 QueuedThreadPool 这个线程池里面,使用了 CLQ 这个队列,它会导致内存缓慢增长,最终引发内存泄漏。
虽然 QueuedThreadPool 仅仅使用了这个队列的 add 方法和 remove 方法。但不幸的是,remove 方法不会把队列的大小变小,只会使队列里面被删除的 node 为空。因此,该列表将增长到无穷大。
然后他给了一个附件,附件里面是一段程序,可以演示这个问题。
我们先不看他的程序,后面我们统一演示这个问题。
先给大家看一下 jetty 的 QueuedThreadPool 线程池。
看哪个版本的 jetty 呢?
可以看到这个 BUG 是在 2015 年 9 月 18 日被爆出来的。所以,我们找一个这个日期之前的版本就行。
于是我找了一个 2015 年 9 月 3 日发布的 maven 版本:
在这个版本里面的 QueuedThreadPool 是这样的:
可以看到,它确实使用了 CLQ 队列。
而从这个对象所有被调用的地方来看,jetty 只使用了这个队列的 size、add、
remove(obj) 方法:
和前面 max 同学描述的一致。
然后这个 max 同学给了几张图片,来佐证他的论点:
主要关注我框起来的地方,就是说他展示了一张图片。可以从这图片中看出内存泄漏的问题,而这个图片的来源是他们真实的项目。
这个项目已经运行了大约两天,每五分钟就会有一个 web 请求过来。
下面是他给出的图片:
从他的这个图片中,我就只看出了 CLQ 的 node 很多。
但是他说了,他这个项目请求量并不大,用的 jetty 框架也不应该创建这么多的 node 出来。
好了,我们前面分析了 max 同学说的这个问题,接下来就是大佬出场,来解惑了:
我们先不看回答,先看看回答问题的人是谁。
Greg Wilkins,何许人也?
我找到了他的领英地址:
https://www.linkedin.com/in/gregwilkins/?originalSubdomain=au
jetty 项目的领导者,短短的几个单词,就足以让你直呼牛逼。
高端的食材,往往只需要最简单的烹饪。高端的人才,往往只需要寥寥数语的介绍。
大佬的简历就是这么朴实无华,且枯燥。
而且,你看这个头像。哎,酸了酸了。果然再次印证了这句话:变秃了,也变强了,并不适用于外国的神仙。
好了,我们看一下这个 jetty 项目的领导者是怎么回答这个问题的:
首先他用 stupefied 表示了非常的震惊!然后,用到了 Ouch 语气词。相当于我们常说的:
他说:卧槽,我发现它不仅导致内存泄漏,而且会随着时间的推移,导致队列越来越慢。太TM震惊了。
这个问题一定会对使用大量线程的服务器产生影响......希望不是所有的服务器都会有影响。
但不管是不是所有的服务器都有这个问题,只要出现了这个问题,对于某些服务器来说,它一定是一个非常严重的 BUG。
然后他说了一个 Great catch!我理解这是一个语气助词。就类似于:太牛逼了。
这个不好翻译,我贴一个例句,大家自己去体会一下吧:
我也是没想到,在技术文里面还给大家教起了英文。
最后他说:我正在修复这个问题。
然后,在 7 分 37 秒之后, Greg 又回复了一次:
可以看出,过了快 8 分钟,他还在持续震惊。我怀疑这 8 分钟里面他一直在摇头。
他说:我还在为这个 BUG 摇头,它怎么这么久都没被发现呢!对于 jetty 来说修复起来非常的简单,使用 set 结构代替 queue 队列即可实现一样的效果。
那我们看一下修复之后的 jetty 中的 QueuedThreadPool 是怎样的,这里我用的是 2015 年 10 月 6 日发布的一个包,也就是这个 BUG 爆出之后的最近的一个包:
里面对应的代码是这样的:
简单粗暴的用 CurrentHashSet 代替了 CLQ。
因为这个 BUG 在 JDK 中是已经修复了,出于好奇,我想看看 CLQ 还有没有机会重新站出来。
于是我看了一下今年发布的最新版本里面的代码:
既不是用的 CurrentHashSet ,也没有给 CLQ 机会。
而是 JDK 8 的 ConcurrentHashMap 里面的 newKeySet 方法,C 位出道:
这是一个小小的 jetty 线程池的演变过程。恭喜你,又学到了一个基本上不会用到的知识点。
回到 Greg 的回复中,这次的回复里面,他还给了一个修复的演示实例,下一小节我会针对这个实例进行解读。
在 23 分钟之后,他就提交代码修复完成了。
从第一次回复帖子,到定位问题,再到提交代码,用了 30 分钟的时间。