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

来源 | why技术


荒腔走板

大家好,我是 why,老规矩,先来一个简短的荒腔走板,给冰冷的技术文注入一丝色彩。

上面图片中这个正在奔跑的少年,是正在参加校运会的我,那一年我 18 岁,高三。

参加的项目是 3000 米长跑,那个时候长跑一向是大家都不喜欢的运动,但是我喜欢。

我是从小乡村出来的,小学、初中的时候操场就是用煤渣加泥土铺成的一圈仅 200 米的跑道。

在那个 200 米的跑道上,我从小学跑到初中,跑了不知道多少公里。

初中的时候参加了学校长跑队的集训。有一天集训完成之后,体育老师给我们说:

你们一定要刻苦训练,将来要到城里面去读书。你们知道吗?城里面的跑道一圈是 400 米,还是用塑胶铺成的跑道,跑上去不知道比这个煤渣舒服多少倍。我知道长跑很痛苦,但是如果你真的喜欢它,你就得享受这个痛苦的过程。不要放弃,特别是在最后冲刺阶段。因为,如果你不拼尽全力,等你到终点之后你总是会觉得自己能再快一点,可惜比赛已经结束了。

后来,我退出了集训队,但是老师的话我一直记得。

我也如愿进入了城里读高中,见到了传说中的“塑胶跑道”。还参加了开在塑胶跑道上的运动会。

前段时间我翻到这张照片的时候,我就想起了初中集训的一些点滴记忆,也想起了老师的话:享受痛苦,咬牙坚持,无愧结果。

这周,今年的高考也结束了。

其实这个道理放在高考,和高考之后的人生会面临的更多更复杂的挑战面前都可以。

人生嘛,无非就是:奔跑,跌倒,奔跑。

奔跑吧,骄傲的少年。前面有更多的机会和更难的挑战在等着你。

好了,说回文章。


从一个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 分钟的时间。

然后在凌晨 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:

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

程序运行 19 分 06 秒后,发生 OOM 异常:

那正常的走势图应该是怎么样的呢?

我们在 JDK 1.8.0_121 版本中(已经修复了 remove 方法),用相同的 JVM 参数(-Xmx20m)再跑一下:

首先从上面的日志中可以看出,时间间隔并没有递增,程序运行的非常的快。

然后用 VisualVM 检测内存,同样跑 19 分钟后截图如下:

可以看到堆内存的使用量并没有随着时间的推移而越来越高。但是还是有非常频繁的 GC 操作。

这个不难理解,因为 CLQ 的数据结构用的是链表。而链表又是由不同的 node 节点组成。

由于调用 remove 方法后,node 节点具备被回收的条件,所以频繁的调用 remove 方法对节点进行删除,会触发 JVM 的 min GC。

这种 JDK BUG 导致的内存泄漏其实挺让人崩溃的。首先你第一次感知到它是因为程序发生了 OOM。

也许你会先无脑的加大堆内存空间,恰好你的程序运行了一周之后又要上线了,所以涉及到重启应用。

然后很长一段时间内没有发生 OOM 了。你就想这个问题可能解决了。

但是它还是在继续发生着,很可能由于节假日前后不能上线,比如国庆七天,加上前后几天,大概有半个月的样子应用没有上线,所以没有重启,程序越来越慢,最终导致第二次 OOM 的出现。

这个时候,你觉得可能不是内存溢出这么简单了。

会不会是内存泄漏了?

然后你再次重启。这次重启之后,你开始时不时的 Dump 一下内存,拿出来分析分析。

突然发现,这个 node 怎么这么多呢?

最终,找到这个问题的原因。

原来是 JDK 的 BUG。

你就会发出和 Greg 一样的感叹:卧槽,震惊,这么牛皮!?

我这个运行了 60 多小时的程序到现在堆内存使用了 233m,但是我整个堆的大小是接近 2G。

通过 jmc 同时展示堆的整体大小和已经使用的堆大小你可以发现,距离内存泄漏可以说是道阻且长了:

我粗略的算了一下,这个程序大概还得运行 475 个小时左右,也就是 19 天之后才会出现由于内存泄漏,导致的 OOM。

我会尽量跑下去,但是听到我电脑嗡嗡嗡的风扇声,我不知道它还能不能顶得住。

如果它顶住了,我在后面的文章里面通知大家。

好了,图形化工具这一小节就到这里了。

我们只是展示了它们非常小的一个功能,合理的使用它们常常能达到事半功倍的作用。

如果你不太了解它们的功能,建议你看看《深入理解JVM虚拟机(第3版)》,里面有一章节专门讲这几个工具的。

最后说一句,

这是我昨天晚上写文章的时候拍的 ,女朋友说一眼望去感觉我是一个盯盘的人,在看股票走势图,这只股票太牛逼了。

要是股市的总体走势也像内存泄露那么单纯而直接就好了。

只要在 OOM 之前落袋为安就行。可惜有的人就是在 OOM 的前一刻满仓杀入,真是个悲伤的故事。

推荐阅读

  • 仅用2年过渡到自研ARM芯片,苹果的底气从何而来?

  • 开源巨头 SUSE 收购 Rancher Labs,云原生时代来临

  • 国内厂商 Onyx 违反 GPL 协议,中国开源何去何从?

  • 天上地下,马斯克和贝佐斯终有一战?

  • 空间-角度信息交互用于光场图像超分辨重构,性能达到最新SOTA | ECCV 2020

  • 赠书 | DeFi沉思录:历史、中国与未来

点分享点点赞点在看

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/517139.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Flutter+Serverless端到端研发架构实践

Serverless(无服务架构)被誉为下一代云计算,自概念推出以来,因为能带来研发交付速度提升与成本的降低在业内异常火爆。闲鱼客户端基于Flutter进行架构演进与创新,通过Flutter统一Android和iOS双端提升研发效能之后&…

Go 开发关键技术指南 | 为什么你要选择 GO?(内含超全知识大图)

导读:从问题本身出发,不局限于 Go 语言,探讨服务器中常常遇到的问题,最后回到 Go 如何解决这些问题,为大家提供 Go 开发的关键技术指南。我们将以系列文章的形式推出《Go 开发的关键技术指南》,共有 4 篇文…

小程序开发(10)-之热力图解决方案、手绘图

原本是用别人用canvas画的热力图的https://github.com/rover95/wxapp-heatmap,但是问题有点多,热力图的颜色,卡顿、叠加、渲染失败等,所以就弃用了,也找了好久,好像大家都没有更好的提议,自己也…

windows下mysql8.x配置远程连接

文章目录1. 现象2. 登录mysql3.先查看下当前的用户,具有什么权限4.创建新的用户之后再查权限5. 赋予权限6.刷新权限,然后就可远程访问了1. 现象 Host ‘192.168.0.103’ is not allowed to connect to this MySQL server mysql8.x配置远程连接 2. 登…

Spark整合Ray思路漫谈

什么是Ray 之前花了大概两到三天把Ray相关的论文,官网文档看了一遍,同时特意去找了一些中文资料看Ray当前在国内的发展情况(以及目前国内大部分人对Ray的认知程度)。 先来简单介绍下我对Ray的认知。 首先基因很重要&#xff0c…

建设数据中台之前,建议先看这份企业数据能力测评 | 大咖说中台

作者 | 耿立超来源 | 《大数据平台架构与原型实现:数据中台建设实战》“我的企业目前在数据应用上处于什么水平?接下来应该朝哪个方向努力?”本文试图帮助企业决策者和IT负责人解答这一问题。今天,数据之于企业的重要性已经勿须多…

如何让 python 处理速度翻倍?内含代码

阿里妹导读:作为在日常开发生产中非常实用的语言,有必要掌握一些python用法,比如爬虫、网络请求等场景,很是实用。但python是单线程的,如何提高python的处理速度,是一个很重要的问题,这个问题的…

Zipkin 存储追踪数据至 MySQL

下载zipkin-mysql数据库脚本 https://github.com/openzipkin/zipkin/tree/master/zipkin-storage/mysql-v1/src/main/resources 创建数据库名称为zipkin,字符集编码:utf8mb4 初始化脚本 -- -- Copyright 2015-2019 The OpenZipkin Authors -- -- Licen…

Spring Cloud Alibaba 新一代微服务解决方案

本篇是「跟我学 Spring Cloud Alibaba」系列的第一篇, 每期文章会在公众号「架构进化论」进行首发更新,欢迎关注。 1、Spring Cloud Alibaba 是什么 Spring Cloud Alibaba 是阿里巴巴提供的微服务开发一站式解决方案,是阿里巴巴开源中间件…

它估值25亿!被马云领投,是华为“老战友”,网友:也许股价能超茅台!

最近一条新闻被炒的沸沸扬扬:十年以来中国最大IPO,中芯国际将融资532亿元!何为IPO?翻译即为一家公司第一次向全社会公开售出它的股份。买的人越多,代表着社会对其信心越大。为什么2020年,能爆发这样1场最大…

RabbitMQ 最新版安装 (Linux环境)

文章目录一、Erlang1. Erlang下载2. Erlang 上传并解压3. 验证rabbitmq依赖是否安装4. 安装rabbitmq依赖5. Erlang 编译、安装6. Erlang 配置环境变量7. Erlang 验证二、RabbitMQ2.1. RabbitMQ 下载2.2. RabbitMQ 上传并解压2.3. RabbitMQ 配置2.4. 配置环境变2.5. 启动 Rabbit…

双11 背后的全链路可观测性:阿里巴巴鹰眼在“云原生时代”的全面升级

导读:作为一支深耕多年链路追踪技术 (Tracing) 与性能管理服务 (APM) 的团队,阿里巴巴中间件鹰眼团队的工程师们见证了阿里巴巴基础架构的多次升级,每一次的架构升级都会对系统可观测性能力 (Observability) 带来巨大挑战,而这次的…

一切转型始于数据和模型 | 2020 MATLAB EXPO 中国线上用户大会:即将上线

2020 MATLAB EXPO 中国线上用户大会一切转型始于数据和模型2020 年 7 月 21-24 日 | 线上直播MATLAB 和 Simulink,作为业界普遍使用的科学计算与模型仿真软件,已被全球的工程师和科学家们广泛应用于加快汽车、航空、电子、金融服务、生物医药以及其他行业…

Dubbo 如何成为连接异构微服务体系的最佳服务开发框架

从编程开发的角度来说,Apache Dubbo (以下简称 Dubbo )首先是一款 RPC 服务框架,它最大的优势在于提供了面向接口代理的服务编程模型,对开发者屏蔽了底层的远程通信细节。同时 Dubbo 也是一款服务治理框架,…

Zipkin 基于MQ存 储链路信息至 MySQL

RabbitMQ 最新版安装 (Linux环境) https://gblfy.blog.csdn.net/article/details/120498390 启动rabbitmq 队列是空的 数据库表是无数据的 启动nacos 应用集成rabbitMQ 父工程导入依赖 <!-- 消息队列通用依赖 --><dependency><groupId>org.springframewo…

标签编辑新工具:如何使用控制台标签编辑器(Tag editor)

创建阿里云资源时&#xff0c;您可以给资源绑定标签。已经创建的资源&#xff0c;也可以在资源列表页面或者通过API&#xff0c;批量的添加、更改和删除标签。当遇到如下更为复杂问题和场景&#xff0c;该如何快速解决标签问题呢&#xff1f; 资源跨度大&#xff0c;需要跨资源…

炸裂!这些大厂跪求的人才太牛了!

今年所有的互联网公司都在ALL in AI&#xff0c;百度、腾讯、阿里巴巴、京东等互联网巨头都在四处挖掘AI人才。AI的岗位需求很多&#xff0c;几乎每天都有数百个JD放出。而亿欧智库发布的《2020全球人工智能人才培养研究报告》提到&#xff0c;至今为止AI的人才储备仍跟不上需求…

60TB 数据量的作业从 Hive 迁移到 Spark 在 Facebook 的实践

Facebook 经常使用分析来进行数据驱动的决策。在过去的几年里&#xff0c;用户和产品都得到了增长&#xff0c;使得我们分析引擎中单个查询的数据量达到了数十TB。我们的一些批处理分析都是基于 Hive 平台&#xff08;Apache Hive 是 Facebook 在2009年贡献给社区的&#xff09…

阿里主管通知我试用期延期……

阿里妹导读&#xff1a;接下来的文章是一篇发布在阿里内网里的文章。花木是一位走出体制的博士&#xff0c;讲述自己Landing的经历。今天&#xff0c;她将这段经历分享给大家&#xff0c;告诉我们&#xff1a;脸先着地又怎样&#xff0c;哪有那么多坦途&#xff1b;最美的&…

使用hbuilder的maps模块调起百度地图导航

首先需要在百度地图开放平台&#xff0c;创建应用拿到appid&#xff0c;然后在hbuilder进行如下配置&#xff1a; hbuilder的manifest.json的配置如下&#xff1a; permissions下添加如下代码&#xff1a; "Maps": {"description": "地图"} 然…