個人程序跑了60多小時,就是爲了讓你看一眼JDK的BUG致使的內存泄漏。

https://mp.weixin.qq.com/s/ppZlpL5Ip7DHG1GG09uT6g

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1在線客服java


荒腔走板



你們好,我是 why,歡迎來到我連續周更優質原創文章的第 56 篇。
老規矩,先來一個簡短的荒腔走板,給冰冷的技術文注入一絲色彩。
上面圖片中這個正在奔跑的少年,是正在參加校運會的我,那一年我 18 歲,高三。
參加的項目是 3000 米長跑,那個時候長跑一貫是你們都不喜歡的運動,可是我喜歡。
我是從小鄉村出來的,小學、初中的時候操場就是用煤渣加泥土鋪成的一圈僅 200 米的跑道。

在那個 200 米的跑道上,我從小學跑到初中,跑了不知道多少千米。

初中的時候參加了學校長跑隊的集訓。有一天集訓完成以後,體育老師給咱們說:
大家必定要刻苦訓練,未來要到城裏面去讀書。大家知道嗎?城裏面的跑道一圈是 400 米,仍是用塑膠鋪成的跑道,跑上去不知道比這個煤渣舒服多少倍。我知道長跑很痛苦,可是若是你真的喜歡它,你就得享受這個痛苦的過程。不要放棄,特別是在最後衝刺階段。由於,若是你不拼盡全力,等你到終點以後你老是會以爲本身能再快一點,惋惜比賽已經結束了。
後來,我退出了集訓隊,可是老師的話我一直記得。

我也如願進入了城裏讀高中,見到了傳說中的「塑膠跑道」。還參加了開在塑膠跑道上的運動會。

前段時間我翻到這張照片的時候,我就想起了初中集訓的一些點滴記憶,也想起了老師的話:享受痛苦,咬牙堅持,無愧結果。

這周,今年的高考也結束了。
其實這個道理放在高考,和高考以後的人生會面臨的更多更復雜的挑戰面前均可以。
人生嘛,無非就是:奔跑,跌倒,奔跑。
奔跑吧,驕傲的少年。前面有更多的機會和更難的挑戰在等着你。
好了,說迴文章。node


從一個BUG提及



前段時間翻到了一個 JDK 有點意思的 BUG,帶你們一塊兒瞅瞅。程序員


https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8137185


640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1memory leak,內存泄漏。
是誰致使的內存泄漏呢?

ConcurrentLinkedQueue,這個隊列。
這個 BUG 裏面說,在 jetty 項目裏面也爆出了這個 BUG:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1我看了一下,以爲 jetty 的這個寫的挺有意思的。

我按照 jetty 的這個講吧,反正都是同一個 JDK BUG 致使的。地址以下:web


https://bugs.eclipse.org/bugs/show_bug.cgi?id=477817


640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1我用我八級半的蹩腳英語給你們翻譯一下這個叫作 max 的同窗說了些什麼。
他說:在 Java 項目裏面,錯誤的使用 ConcurrentLinkedQueue(文章後面用縮寫 CLQ 代替)會致使內存泄漏的問題。

在 jetty 的 QueuedThreadPool 這個線程池裏面,使用了 CLQ 這個隊列,它會致使內存緩慢增加,最終引起內存泄漏。
雖然 QueuedThreadPool 僅僅使用了這個隊列的 add 方法和 remove 方法。但不幸的是,remove 方法不會把隊列的大小變小,只會使隊列裏面被刪除的 node 爲空。所以,該列表將增加到無窮大。
而後他給了一個附件,附件裏面是一段程序,能夠演示這個問題。
咱們先不看他的程序,後面咱們統一演示這個問題。
先給你們看一下 jetty 的 QueuedThreadPool 線程池。
看哪一個版本的 jetty 呢?640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
能夠看到這個 BUG 是在 2015 年 9 月 18 日被爆出來的。因此,咱們找一個這個日期以前的版本就行。

因而我找了一個 2015 年 9 月 3 日發佈的  maven 版本:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
在這個版本里面的 QueuedThreadPool 是這樣的:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1能夠看到,它確實使用了 CLQ 隊列。

而從這個對象全部被調用的地方來看,jetty 只使用了這個隊列的 size、add、remove(obj) 方法:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1和前面 max 同窗描述的一致。

而後這個 max 同窗給了幾張圖片,來佐證他的論點:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1主要關注我框起來的地方,就是說他展現了一張圖片。能夠從這圖片中看出內存泄漏的問題,而這個圖片的來源是他們真實的項目。

這個項目已經運行了大約兩天,每五分鐘就會有一個 web 請求過來。
下面是他給出的圖片:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1從他的這個圖片中,我就只看出了 CLQ 的 node 不少。

可是他說了,他這個項目請求量並不大,用的 jetty 框架也不該該建立這麼多的 node 出來。
好了,咱們前面分析了 max 同窗說的這個問題,接下來就是大佬出場,來解惑了:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1咱們先不看回答,先看看回答問題的人是誰。 
Greg Wilkins,何許人也?
我找到了他的領英地址:編程


https://www.linkedin.com/in/gregwilkins/?originalSubdomain=au


640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
jetty 項目的領導者,短短的幾個單詞,就足以讓你直呼牛逼。
高端的食材,每每只須要最簡單的烹飪。高端的人才,每每只須要寥寥數語的介紹。
大佬的簡歷就是這麼樸實無華,且枯燥。
並且,你看這個頭像。哎,酸了酸了。果真再次印證了這句話:變禿了,也變強了,並不適用於外國的神仙。640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1好了,咱們看一下這個 jetty 項目的領導者是怎麼回答這個問題的:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
首先他用 stupefied 表示了很是的震驚!而後,用到了 Ouch 語氣詞。至關於咱們常說的:
640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
他說:臥槽,我發現它不只致使內存泄漏,並且會隨着時間的推移,致使隊列愈來愈慢。太TM震驚了。
640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1這個問題必定會對使用大量線程的服務器產生影響......但願不是全部的服務器都會有影響。
但無論是否是全部的服務器都有這個問題,只要出現了這個問題,對於某些服務器來講,它必定是一個很是嚴重的 BUG。

而後他說了一個 Great catch!我理解這是一個語氣助詞。就相似於:太牛逼了。
這個很差翻譯,我貼一個例句,你們本身去體會一下吧:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1我也是沒想到,在技術文裏面還給你們教起了英文。

最後他說:我正在修復這個問題。
而後,在 7 分 37 秒以後, Greg 又回覆了一次:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1能夠看出,過了快 8 分鐘,他還在持續震驚。我懷疑這 8 分鐘裏面他一直在搖頭。

他說:我還在爲這個 BUG 搖頭,它怎麼這麼久都沒被發現呢!對於 jetty 來講修復起來很是的簡單,使用 set 結構代替 queue 隊列便可實現同樣的效果。
那咱們看一下修復以後的 jetty 中的 QueuedThreadPool 是怎樣的,這裏我用的是 2015 年 10 月 6 日發佈的一個包,也就是這個 BUG 爆出以後的最近的一個包:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1裏面對應的代碼是這樣的:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1簡單粗暴的用 CurrentHashSet 代替了 CLQ。
由於這個 BUG 在 JDK 中是已經修復了,出於好奇,我想看看 CLQ 還有沒有機會從新站出來。
640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
因而我看了一下今年發佈的最新版本里面的代碼:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1既不是用的 CurrentHashSet ,也沒有給 CLQ 機會。
而是 JDK 8 的 ConcurrentHashMap 裏面的 newKeySet 方法,C 位出道:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
這是一個小小的 jetty 線程池的演變過程。恭喜你,又學到了一個基本上不會用到的知識點。

回到 Greg 的回覆中,此次的回覆裏面,他還給了一個修復的演示實例,下一小節我會針對這個實例進行解讀。
在 23 分鐘以後,他就提交代碼修復完成了。
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1從第一次回覆帖子,到定位問題,再到提交代碼,用了 30 分鐘的時間。
640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
而後在凌晨 2 點 57 分(這個時間點,大佬都是不用睡覺的嗎?仍是說剛修完福報,下班了), max 回覆到:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1我不敢相信 CLQ 使用起來會有這樣的問題,他們至少應該在 API 文檔裏面說明一下。
這裏的他們,應該指的是 JDK 團隊的成員,特指 Doug Lea,畢竟是他老爺子的做品。640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
爲何沒有在 API 文檔裏面說明呢?
由於他們本身也不知道有這個 BUG 啊。
Greg 連着回覆了兩條,而且直接指出瞭解決方案:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1問題的緣由是 remove 方法的源碼裏面,有上圖中標號爲 ① 的這樣一行代碼。
這行代碼會去取消被移除的這個 node (其值已經被替換爲 null)和 list 之間的連接,而後可讓 GC 回收這個 node。
可是,當集合裏面只有一個元素的時候, next != null 這個判斷是不成立的。
因此就會出現這個須要移除的節點已經被置爲 null 了,但卻沒有取消和隊列之間的鏈接,致使 GC 線程不會回收這個節點。
他給出的解決方案也很簡單,就是標號爲②、③的地方。總之,只須要讓代碼執行 pred.casNext 方法就行。
總之一句話,致使內存泄漏的緣由是一個被置爲 null 的 node,因爲代碼問題,致使該 node 節點,既不會被使用,也不會被 GC 回收掉。
若是你還沒理解到這個 BUG 的緣由,說明你對 CLQ 這個隊列的結構還不太清晰。
那麼我建議你讀一下《Java併發編程的藝術》這一本書,裏面有一小節專門講這個隊列的,圖文並茂,寫的仍是很是清晰。640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1這個 BUG 在 jetty 裏面的前因後果算是說清楚了。

而後,咱們再回到 JDK BUG 的這個連接中去:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
他這裏寫的緣由就是我前面說的緣由,沒有 unlink,因此不能被回收。
並且他說到:這個 BUG 在最新的JDK 七、8和9版本中都存在。
他說的最新是指截止這個 BUG 被提出來以前:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1服務器


Demo跑起來



這一小節裏面,咱們跑一下 Greg 給的那個修復 Demo,親手去摸一下這個 BUG 的樣子。
數據結構


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


你能夠打開上面那個連接,直接複製粘貼到你的 IDEA 裏面去:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
注意第 13 行,由於 Greg 給的是修復 Demo,因此用的是 ConcurrentHashSet,因爲咱們要演示這個bug,因此使用 CLQ。
這個 Demo 就是在死循環裏面調用 queue 的 add(obj) 和 remove(obj) 方法。每循環 10000 次,就打印出時間間隔、隊列大小、最大內存、剩餘內存、總內存的值。

最終運行起來的效果是這樣的(JDK 版本是 1.7.0_71):
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
能夠看到每次打印 duration 這個時間間隔是愈來愈大,隊列大小始終爲 1。
後面三個內存相關的參數能夠先不關心,下一小節咱們用圖形化工具來看。
你知道上面這個程序,到我寫文章寫到這裏的時候,我跑了多久了嗎?
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=161 小時 32 分 53 秒。
最新一次循環 10000 次所須要的時間間隔是 575615ms,快接近 10 分鐘:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
這就是 Greg 說的:不只僅是內存泄漏,並且愈來愈慢。640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
可是,一樣的程序,我用 JDK 1.8.0_212 版本跑的時候狀況倒是這樣的:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
時間間隔很穩定,不會隨着時間的推移而增長。

說明這個版本是修復了這個 BUG 的,我帶你們看看源碼:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
JDK 1.8.0_212 版本的源碼裏面,在 CLQ 的 remove(obj) 方法的 502 行末尾註釋了一個 unlink。
官方的修復方法能夠看這裏:
併發


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


改動比較多,可是原理仍是和以前分析的同樣:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1我僅僅在兩個 JDK 版本中跑過示例代碼。

在 JDK 1.8.0_212 沒有發現內存泄漏的問題,我看了對應的 remove(obj) 方法的源碼確實是修復了。
在 JDK 1.7.0_71 中能夠看到內存泄漏的問題。
unlink,一個簡簡單單的詞,背後原來藏了這麼多故事。640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1app


jconsole、VisualVM、jmc



既然都說到內存泄漏了,那必須得介紹幾個可視化的故障排除工具。

前面說了,這個程序跑了 61 個小時了,給你們看一下這個時間段裏面堆內存的使用狀況:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1能夠看到整個堆內存的使用量是一個明顯的、緩慢的上升趨勢。
上面這個圖就是來自 jconsole。
結合程序,經過圖片咱們能夠分析出,這種狀況必定是內存泄漏了,這是一個很是經典的內存泄漏的走勢。
接下來,咱們再看一下 jmc 的監控狀況:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1上面展現的是已經使用的堆內存的大小,走勢和 jconsole 的走勢同樣。

而後再看看 VisualVM 的圖:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
VisualVM 的圖,我不知道怎麼看整個運行了 60 多小時的走勢圖,可是從上面的圖也是能看出是有上升趨勢的。

在 VisualVM 裏面,咱們能夠直接 Dump 堆,而後進行分析:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1能夠清楚的看到, CLQ 的 Node 的大小佔據了 94.2%。

可是,從咱們的程序來看,咱們根本就沒有用到這麼多 Node。咱們只是用了一個而已。
你說,這不是內存泄漏是什麼。
內存泄漏最終會致使 OOM。

因此當發生 OOM 的時候,咱們須要分析是否是有內存泄漏。也就是看內存裏面的對象到底應不該該存活,若是都應該存活那就不是內存泄漏,是內存不足了。須要檢查一下 JVM 的參數配置(-Xmx/-Xms),根據機器內存狀況,判斷是否還能再調大一點。

同時,也須要檢查一下代碼,是否存在生命週期過程的對象,是否有數據結構使用不合理的地方,儘可能減小程序運行期的內存消耗。
咱們能夠經過把堆內存設置的小一點,來模擬一下內存泄漏致使的 OOM。

仍是用以前的測試案例,可是咱們指定 -Xmx 爲 20m,即最大可用的堆大小爲 20m。

而後把代碼跑起來,同時經過 VisualVM 、jconsole、jmc 這三個工具監控起來,爲了咱們有足夠的時候準備好檢測工具,我在第 8 行加入休眠代碼,其餘的代碼和以前的同樣:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
加入 -Xmx20m 參數:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
運行起來以後,咱們同時經過工具來查看內存變化,下面三個圖從上到下的工具分別是 VisualVM、jconsole、jmc:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
從圖片的走勢來看,和咱們以前分析的是同樣的,內存一直在增加。
程序運行 19 分 06 秒後,發生 OOM 異常:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
那正常的走勢圖應該是怎麼樣的呢?
咱們在 JDK 1.8.0_121 版本中(已經修復了 remove 方法),用相同的 JVM 參數(-Xmx20m)再跑一下:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
首先從上面的日誌中能夠看出,時間間隔並無遞增,程序運行的很是的快。
而後用 VisualVM 檢測內存,一樣跑 19 分鐘後截圖以下:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1能夠看到堆內存的使用量並無隨着時間的推移而愈來愈高。可是仍是有很是頻繁的 GC 操做。

這個不難理解,由於 CLQ 的數據結構用的是鏈表。而鏈表又是由不一樣的 node 節點組成。
因爲調用 remove 方法後,node 節點具有被回收的條件,因此頻繁的調用 remove 方法對節點進行刪除,會觸發 JVM 的 min GC。

這種 JDK BUG 致使的內存泄漏其實挺讓人崩潰的。首先你第一次感知到它是由於程序發生了 OOM。
也許你會先無腦的加大堆內存空間,剛好你的程序運行了一週以後又要上線了,因此涉及到重啓應用。
而後很長一段時間內沒有發生 OOM 了。你就想這個問題可能解決了。
可是它仍是在繼續發生着,極可能因爲節假日先後不能上線,好比國慶七天,加上先後幾天,大概有半個月的樣子應用沒有上線,因此沒有重啓,程序愈來愈慢,最終致使第二次 OOM 的出現。
這個時候,你以爲可能不是內存溢出這麼簡單了。
會不會是內存泄漏了?
而後你再次重啓。此次重啓以後,你開始時不時的 Dump 一下內存,拿出來分析分析。

忽然發現,這個 node 怎麼這麼多呢?
最終,找到這個問題的緣由。
原來是 JDK 的 BUG。

你就會發出和 Greg 同樣的感嘆:臥槽,震驚,這麼牛皮!?640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1我這個運行了 60 多小時的程序到如今堆內存使用了 233m,可是我整個堆的大小是接近 2G。
經過 jmc 同時展現堆的總體大小和已經使用的堆大小你能夠發現,距離內存泄漏能夠說是道阻且長了:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1我粗略的算了一下,這個程序大概還得運行 475 個小時左右,也就是 19 天以後纔會出現因爲內存泄漏,致使的 OOM。

我會盡可能跑下去,可是聽到我電腦嗡嗡嗡的風扇聲,我不知道它還能不能頂得住。
640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1若是它頂住了,我在後面的文章裏面通知你們。

好了,圖形化工具這一小節就到這裏了。

咱們只是展現了它們很是小的一個功能,合理的使用它們經常能達到事半功倍的做用。

若是你不太瞭解它們的功能,建議你看看《深刻理解JVM虛擬機(第3版)》,裏面有一章節專門講這幾個工具的。640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1框架


最後說一句(求關注)


640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1這是我昨天晚上寫文章的時候拍的 ,女友說一眼望去感受我是一個盯盤的人,在看股票走勢圖,這隻股票太牛逼了。
要是股市的整體走勢也像內存泄露那麼單純而直接就行了。
只要在 OOM 以前落袋爲安就行。惋惜有的人就是在 OOM 的前一刻滿倉殺入,真是個悲傷的故事。
文中提到的兩本書,都是很是優秀的值得學習的書籍。做爲一個 Java 程序員,若是你尚未擁有這兩本書,我強烈建議你買來看看。

另外,我最近的閱讀量降低的很是厲害,正反饋持續減弱。
因此安排個「一鍵三連」(轉發、在看、點贊)吧,周更很累的,不要白嫖我,須要一點正反饋。image.png才疏學淺,不免會有紕漏,若是你發現了錯誤的地方,因爲本號沒有留言功能,還請你在後臺留言指出來,我對其加以修改。
感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。


640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1


我是 why,一個被代碼耽誤的文學創做者,不是大佬,可是喜歡分享,是一個又暖又有料的四川好男人。


還有,重要的事情說三遍:


歡迎關注我呀。歡迎關注我呀。歡迎關注我呀。640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1