JDK的BUG致使的內存溢出!反正我是沒想到還能有續集。

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

image.png

荒腔走板


你們好,我是 why,歡迎來到我連續周更優質原創文章的第 57 篇。java


老規矩,先來一個簡短的荒腔走板,給冰冷的技術文注入一絲色彩。node


上面這個圖是個人第一臺筆記本電腦,從上面的標籤能夠看到,是購於 2012 年 6 月 10 日,那一天是高考完的次日。
程序員


我讀高三的時候媽媽承諾我,考完以後就能夠擁有一臺本身的筆記本電腦。那個時候媽媽和我都不太懂電腦,我記得很清楚,是姑父開車帶我去買的,花了 3000 多。web


一晃 8 年多的時間過去了,這臺電腦也跟着我從老家到成都再到北京再回成都。
算法


其實我大三實習的時候就想本身換個 Mac,而後一想可能要存錢去北京,因而想着再等等吧。
bootstrap


到北京後,發現每月剩下的錢很少很多,這電腦也能用,那能省則省了吧,再等等吧。安全


這一等,就是 5 年。說來慚愧,我到如今都還沒擁有一臺本身的 Mac。微信


上週的監控圖就是用這臺電腦跑的,我不是說預計還須要跑 19 天才可能會看到 OOM 嗎。
數據結構


其實我挺擔憂跑着跑着就藍屏了,可是發完文章後不到一小時,小區就停電了。多線程


因而,這場停電,終止了這個監控,也拯救了這個電腦。從某種角度上來講,這場停電也拯救了個人錢包。


好了,說迴文章。

BUG究竟是怎麼修復的?


上週《個人程序跑了60多小時,就是爲了讓你看一眼JDK的BUG致使的內存泄漏》這篇文章發佈後。


有好幾個同窗都來問了我一些相關的問題。


好比這樣的:


640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1寫上篇文章的時候,個人側重點主要在 ConcurrentLinkedQueue(下文統一縮寫 CLQ)存在過一個可能會致使內存泄漏的 BUG ,這個 BUG 的前因後果是怎樣的,以及怎麼經過可視化工具讓咱們感覺到這個 BUG 的存在。

其實對於 BUG 在源碼裏面具體是怎樣體現的,以及修改以後爲何就不會內存泄漏了並無進行詳細的解讀。

開始的想法是,告訴你們有這個事情,若是有興趣的能夠直接去調試分析一下。
可是有的同窗反映調試也看不明白啊。一個方法,在斷點處一臉懵逼的進來,又一臉懵逼的出去。640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
苦思冥想沒搞清楚,而後就來問我。
我發現一兩句話也說不太清楚,因而把 Debug 的關鍵截圖放到文檔裏面配以文字說明,才勉強能說的比較清楚一點,也不知道這位同窗看明白了沒。
但就拿這個文檔來講:真的是暖男石錘了。640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
因此,文本主要是分享一個我本身調試的奇淫技巧,最後再作個 remove 方法的解讀。
可是若是要深入理解 CLQ 這個十分優秀、十分有想法的基於非阻塞方法實現的線程安全的隊列,你們須要去看的是 offer、poll 方法。而後一個狀況一個狀況的去分析,本身拿着草稿本在上面寫寫畫畫。
我也妄想過經過這篇文章給大家把它講的明明白白的,後來我發現這對我而言難度有點大。
最後再說一下若是你用 IDEA 調試時,大機率會碰到的一個巨坑。
好了,先把以前的這個坑給填上。
修復以後的 JDK8 到底怎麼就避免了內存泄漏的問題了?


自定義CLQ



咱們先看一下 CLQ 的數據結構。

CLQ 的 Node 裏面有一個 item(放的是存儲的對象),還有一個 next 節點(指向的是當前 Node 的下一個節點)。

從數據結構來看,也知道這是一個單向鏈表了。640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1Java 程序員,就靠日誌活着的。
因此我想經過日誌的方式直接輸出鏈表結構,這應該是最簡單的演示方式了。
爲了經過日誌體現出數據變化的過程,咱們先來一個自定義的 CLQ。
方法很簡單,直接把 JDK 8 的 CLQ 複製出來一份,而後修更名稱就能夠,咱們這裏的名稱是 whyConcurrentLinkedQueue(下文簡寫爲 WhyCLQ):
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
而後你會發現報錯了:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
這個錯誤是關於 Unsafe 操做的,在代碼的第 931 行:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
getUnsafe 方法的源碼是這樣的:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
而這個方法裏面就是判斷當前類的類加載器是否是爲 null:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
這裏拋出異常了,說明不是 null。也就會說當前類的加載器不是啓動類加載器 BootstrapClassLoader。
咱們知道,rt.jar 包下的類是須要 bootstrap 類加載器加載的。
誒,巧了。這個類就位於 rt.jar 包裏面: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
若是咱們自定義了一個 CLQ ,那麼這個類的類加載器是什麼類加載器呢?

咱們驗證一下:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1從 Debug 的截圖能夠看出當前類 WhyCLQ 的類加載器是 AppClassLoader。其父類加載器(parent)是 ExtClassLoader 類加載器。

不是 BootstrapClassLoader ,因此咱們這裏拋出了異常。
在介紹怎麼解決這個異常以前,先簡單的說一下 Unsafe。
這個類名稱一聽就是很是牛逼的。Unsafe,不安全。
感受像是在釣魚執法,表面上瘋狂的在那給你擺手,說:別靠近我,別使用我,我很不安全。
實際上心裏是這樣的:640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
做爲一個正常的男人,看到這個東西誰不想去調用一下,看看究竟是怎麼不安全的呢?

咱們看一下《美團點評 2019 技術年貨》裏面是怎麼描述的:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
同時,看一下它相關的 API:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
因爲 Unsafe 不是文本重點,我就不展開說明了。若是你對 Unsafe 這個類掌握的還不深入,建議你好好了解一下。若是你清楚的知道這個類的威力,在某些場景下能夠達到意想不到的效果,它就是一枚銀彈般的存在。

《美團點評 2019 技術年貨》裏面有一小節是專門分享這個類的,有興趣的朋友能夠查看文末獲取方式。
好了,知道拋出問題的緣由了,咱們自定義的 CLQ 就不能用了嗎?
固然不是,別忘了,咱們還有極其「流氓」的反射方法可使用:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
這樣,咱們自定義的 CLQ 就可使用了。免費附贈你一個 Unsafe 的知識,不用謝。
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
因此在咱們自定義的 CLQ 裏面加一個打印鏈表結構的方法640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
而後給咱們的 remove 方法增長一個循環次數的入參,並在操做隊列以前和以後調用咱們打印鏈表結構的方法,就像下面這樣式兒的:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
其中的 printWhyCLQ 方法以下:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
有的朋友確定注意到了,我這個方法名稱是 removeJDK8 。這個方法裏面的邏輯就是 JDK8 的 CLQ 的 remove 方法。
你能夠這麼理解:我就是把 JDK8 的 CLQ 的 remove 方法的名稱變成了 removeJDK8 
爲何這樣命名呢?
由於我要把 JDK7 對應的 remove 方法直接拿過來,放在同一個類裏面方便調用,操做和上面的 JDK8 方法一致:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
這樣,咱們就有一個自定義的 CLQ,裏面包含 JDK7 和 JDK8 對應的 CLQ 的 remove 方法。
萬事俱備,就差個 Demo 跑起來了:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
在下面一小節中,咱們對比一下修復前(JDK7)和修復後(JDK8)的輸出日誌,一切就會很是的明瞭。


修復前 vs 修復後



咱們把 Demo 跑起來,看輸出結果,進行對比:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
你仔細品這個輸出結果,還須要我給你分析個啥玩意?
640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1和 JDK8 的方法比起來,上面 JDK7 的方法執行完成後鏈表長度都長了一些。

JDK8 的方法執行完成後,鏈表長度最長也沒有超過 3 個。

咱們再看 JDK7,我拿一次循環出來分析:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
這就是我上篇文章中說到的:一個節點中的 item 對象被置爲 null 了,可是該節點,因爲代碼問題,並無從鏈表中取下來,致使不能被回收。
而上篇文章中提到的「愈來愈慢」,因爲能夠直接的看到鏈表結構了,因此也很好解釋了:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
好比,我把 Demo 中 for 循環的次數修改成 100,運行以後,咱們看最後一次循環的結果爲:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
remove 方法是從鏈表的頭結點開始遍歷鏈表,而咱們每次須要移除的實際上是最後一個節點,因爲鏈表愈來愈長,因此遍歷鏈表的時間愈來愈長。
因此致使咱們上一篇的案例中每循環 10000 次,時間都會增長。

image.png

源碼導讀



接下來咱們看一下 JDK8 的源碼中的 remove(obj) 方法究竟是怎麼樣工做的。
這個方法的目的就是從頭結點開始遍歷鏈表,而後判斷每一個 Node 裏面的 item 是否是須要被刪除的這個,若是是則刪除,若是不是則繼續遍歷。

我想了好久這個地方怎麼能把代碼的執行流程說清楚呢?
除了 Debug 以外,由於 Debug 須要截很是多的圖纔可能說的清楚。
只有瘋狂的輸出日誌了。

咱們先看簡單的分析一下 JDK8 對應的源碼:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
490 行是在對象被移除以前,咱們能夠在這裏加一行輸出語句打印當前的鏈表結構。

505 行是在對象被移除以後,咱們能夠在這裏加一行輸出語句打印刪除操做完成以後的鏈表結構。

縱觀整個方法,只有我標註的兩個地方會去修改鏈表結構。因此,咱們分別在這兩處地方的先後輸出相關日誌,而後分析日誌,就能夠知道這個方法的工做流程了。
知道它的工做流程了,再返回去看代碼,那還不是易如反掌的事兒?

這就是傳說中的蛇皮走位,反向操做。

image.png

因此,按照咱們上面的分析,在自定義的 CLQ 裏面加入輸出語句以下:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
其中的 sortName 方法是爲了把 java.lang.Object@xxx 截取爲 @xxx,精簡輸出:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
上面的 removeJDK8 方法除了輸出語句以外,其餘的代碼邏輯和 JDK8 的對應方法如出一轍。
咱們仍是用這個示例代碼: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
日誌不少,可是細細分析下來流程很是的清晰,你能夠在草稿本上畫一畫。
我帶着你們分析前兩個循環,一共 10 行日誌,咱們一行行的分析,注意咱們下面畫的圖僅體現了 node 裏面的 item 元素:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1


第【0】次循環,【移除以前】,鏈表item對象指向 = @723279cf->@10f87f48->


從測試代碼中能夠知道,被刪除以前咱們確實是有兩個節點:
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


第【0】次循環,【修改節點item爲null】被修改的p節點的item爲(@10f87f48),即須要被刪除的節點


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


第【0】次循環,【修改節點item爲null以後】,鏈表item對象指向 = @723279cf->null->


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


第【0】次循環,【移除以後】,鏈表item對象指向 = @723279cf->null->


其實移除以後,就是把節點的 item 修改成 null 以後,因此結構和上面仍是同樣的:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
第【0】次循環就分析完了。能夠看到如今的鏈表裏面有一個 item 爲 null 的元素,它還在鏈上,因此不會被回收。

接下來,咱們分析一下第【1】次循環。


第【1】次循環,【移除以前】,鏈表item對象指向 = @723279cf->null->@10f87f48->


因爲進入下次循環,因此會先執行 add 方法,因此如今的鏈表結構變成了這樣:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1


第【1】次循環,【處理null節點】把item爲(@723279cf)的pred節點的next節點,從item爲(null)的p節點修改成item爲(@10f87f48)的next節點



pred 節點裏面的 item 就是 @723297cf。
p 節點裏面的 item 就是 null。

next 節點裏面的 item 就是 @10f87f48。640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1


第【1】次循環,【處理null節點以後】,鏈表item對象指向 = @723279cf->@10f87f48->


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


第【1】次循環,【修改節點item爲null】被修改的p節點的item爲(@10f87f48),即須要被刪除的節點


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


第【1】次循環,【修改節點item爲null以後】,鏈表item對象指向 = @723279cf->null->


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


第【1】次循環,【移除以後】,鏈表item對象指向 = @723279cf->null->


640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
第【1】次循環完成後又回到了第【0】次循環完後的樣子。

中間的那個 item 爲 null 的節點去哪了呢?

由於這是個單向鏈表,從頭節點已經不能遍歷到這個節點了。因此等待它的命運將是被回收,因此也就不會內存溢出了。
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1到這裏,我以爲這個問題算是回答清楚了吧?
關於 remove(obj) 我就分享到這裏。
實話實話,這個方法對於 CLQ 並非很是的重要,咱們通常使用場景也比較少。
我寫這節主要是兩個目的。
一是回答讀者的提問,由於畢竟是看了個人文章引起出來的問題,我有義務回答。
二是分享一下這種本身 copy 一個類出來,而後只加入輸出語句的調試方式。這個調試方法老讀者確定知道了,我在寫 ArrayList 的時候也用過,寫 Dubbo 負載均衡算法的時候也用過。當你被一步步 debug 帶暈的時候,你能夠試一試這種方式,先總體再局部。好比本文的 CLQ,多線程調試 CLQ 的狀況下,我以爲日誌的輸出對於你理解它的精髓很是的有幫助。
仍是以前說過的,若是要深入理解 CLQ 這個十分優秀、十分有想法的基於非阻塞方法實現的線程安全的隊列,你們須要去看的是 offer、poll 方法。而後一個狀況一個狀況的去分析,看看它是怎麼避免頻繁 CAS 的,本身拿着草稿本在上面寫寫畫畫。
我也妄想過經過這篇文章給大家把它講的明明白白的,後來我發現這對我而言難度有點大。
我在這裏給你們指個路,看哪幾種狀況:


  1. 單線程下的 offer。

  2. 單線程下的 poll。

  3. 多線程下的一個線程 offer ,一個線程 poll。offer 比 poll 快。

  4. 多線程下的一個線程 offer ,一個線程 poll。offer 比 poll 慢。



就這四種狀況,玩去吧。
一種很是優秀的思想,很是牛逼的實現,我但願你能靜下心來堅持過半小時。640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1


IDEA DEBUG 模式的巨坑



640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
看了我上面的介紹,準備靜下心來看第一種狀況:單線程下的 offer。
若是你用 IDEA 的 Debug 調試 CLQ 的 offer 方法,半個小時後你心態應該就會炸裂: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很是簡單,在隊列裏面添加一個元素。

因爲初始化的狀況下 head=tail=new Node<E>(null):640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
因此在 add 方法被調用以後的鏈表結構裏面的 item 指向應該是這樣的:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
咱們在 offer 方法裏面加入幾個輸出語句:
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
爲何最後一行輸出,【offer以後】輸出的日誌不是 null->@723279cf 呢?

由於這個方法裏面會調用 first 方法,獲取真正的頭節點,即 item 不爲 null 的節點:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
到這裏都一切正常。可是,當你用 debug 模式操做的時候就不太同樣了:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
頭節點的 item 不爲 null 了!而頭節點的下一個節點爲 null,因此拋出空指針異常。
單線程的狀況下代碼直接運行的結果和 Debug 運行的結果不一致!這不是遇到鬼了嗎。
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
我在網上查了一圈,發現遇到鬼的網友還很多。
最終找到了這個地方:


https://stackoverflow.com/questions/55889152/why-my-object-has-been-changed-by-intellij-ideas-debugger-soundlessly



這個哥們遇到的問題和咱們如出一轍:
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
你知道回答這個問題的哥們是誰嗎?640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1IDEA 的產品經理,獻上個人 respect。
640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
最後的解決方案就是關閉 IDEA 的這兩個配置:
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
由於 IDEA 在 Debug 模式下會主動的幫咱們調用一次 toString 方法,而 toString 方法裏面,會去調用迭代器。

而 CLQ 的迭代器,會觸發 first 方法,這個裏面和以前說的,會修改 head 元素:image.png

一切,都真相大白了。

以前,我認爲是玄學。而如今,沒有什麼是玄學,咱們要相信科學。
我身邊也有朋友碰到過這個問題,若是不知道這個坑,很是的摳腦袋,很容易就「懷疑人生」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=1640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

☜ 滑 動 查 看 更 多 圖 片


最後說一句



文章中提到的《美團點評 2019 技術年貨》是公衆號【美團技術團隊 2019 年出品的後臺技術文章集合,內容很是的豐富:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1若是你有興趣,能夠在公衆號後臺回覆關鍵字【java】便可得到 PDF 的下載連接。

若是你以爲麻煩了,那你也能夠直接加我微信,備註【PDF】,我直接發給你:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1放心,若是你不主動找我聊天,我也是不會主動和你搭話的。靜靜的躺在朋友圈裏,作個點贊之交。畢竟說出來你可能不信,我也是有輕微的社交恐懼症的。
最後,你們安排個「一鍵三連」(轉發、在看、點贊)吧,周更很累的,不要白嫖我,須要一點正反饋。640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1才疏學淺,不免會有紕漏,若是你發現了錯誤的地方,因爲本號沒有留言功能,還請你在後臺留言指出來,我對其加以修改。
感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。


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

相關文章
相關標籤/搜索