Java 程序佔用記憶體過多,幕後元凶原來是執行緒太多
那天中午吃飯,一個同事說,那個專案組的人快氣死我了,程式有問題,早晨在群裡@了他們,到中午才回訊息,然後竟然還說他們的程式沒有問題,是我們這邊呼叫的太頻繁了。
簡直想笑。
背景說明
我們當前這個系統和很多的第三方系統做了整合,出問題的就是其中一個三方系統。其實很簡單,他們的系統會產生一些個人待辦任務,然後待辦任務的個數需要推送到我們的 APP 上,作為圖示的角標顯示。
使用者資料已經打通,其實很簡單的需求,角標通知也不要求實時,10分鐘刷一次就可以。這個場景非常典型,用訊息佇列再合適不過了。他們把資料推到訊息佇列,我們去訊息佇列取,完美。
可現實並不是這樣,他們說系統是產品化,不支援訊息佇列,只能把待辦任務介面開放出來。好的(微笑臉),你們是產品你們有理。可能有待辦任務的使用者不多,300多個,那就每隔 10 分鐘請求 300 多次請求唄。也沒用多執行緒,就是簡單的迴圈 300 多次請求,每次耗時差不多的1分鐘。
也可以,那就這樣唄。
順便說一下,這個服務的 JDK 是 1.6 版本,據說由於歷史原因,現在已經不敢升級了。而且,服務要部署在 windows 上。(你說神奇不神奇)
花明柳暗
那就這樣唄,做個定時任務,10分鐘咔咔請求個 300 來次,也挺過癮,也挺省心。
但是好景不長,天不遂人願,伺服器不遂程式設計師願。
以下是同事的經歷,我轉述以下。
就在定時任務跑起來後的第二個晚上,那本來該是一個平常的晚上,可是告警郵件擾人清夢。一看日誌,記憶體使用空間過高,撐爆了,導致機器自動重啟了。windows 就這點好啊,還會自動重啟(尷尬臉)。然後手動上去把服務啟動起來,解決。
隔了一天,還是晚上,又報警了,伺服器又自動重啟了,又是記憶體使用空間過高。又手動上去把服務啟動了。
於是他反饋給這個服務的開發人員,結果得到的回覆是:“我們的服務沒有問題,肯定是你們的呼叫有問題,你們把定時任務停掉肯定就好了,所以是你們的問題”。
於是,他過來找我,跟我說明情況,問我可能會是什麼問題。
我:你確定定時服務是 10 分鐘一次,沒有出現死迴圈嗎?
同事:確定。
我:那他們的服務有使用 redis 之類的外部快取嗎?
同事:不知道。
我:。。。 既然你確定你呼叫的沒問題,那肯定是他們程式出現問題把記憶體撐爆了呀,這有什麼好懷疑的,讓他們改吧。
同事:他們現在說自己沒問題啊。
挖出真凶
好吧,既然他們說沒問題,那我就來幫他把問題找出來吧。於是,遠端進了那臺 windows 伺服器。
這時候已經把定時任務已經跑了兩天了,16G 的記憶體已經用掉 15G 多了,眼看隨時有可能崩潰,然後把定時任務停掉,記憶體使用量也並不會下來。
我開始懷疑是不是用了 redis 之類的外部快取,結果進伺服器一查 redis 、memcached 之類的壓根兒就沒裝,所以基本排除外部快取。
並且登入上去之後檢視程序記憶體佔用,確實就是一個 Java 程序佔了這麼多記憶體。
那既然不是外部快取,那肯定出在 JVM 上了,要不然就是用了 JVM 快取,要不然就是記憶體洩漏什麼的。於是我想用 jinfo -flags
看一下 JVM 初始引數,JDK 6 竟然還不支援 -flags 。
然後我不知道是不是嘗試了 jmap -heap
還是就看了一眼 jmap -help
以為不支援 jamp -heap,反正最後我是通過 jconsole
來觀察的 JVM。一看 JVM 引數明顯就是預設沒特殊設定過,並且奇怪的是對記憶體一共採用了 700 多M。700M 和 15G 比,差哪兒去了,沒道理啊,問題沒出在堆上。
然後我嘗試執行 GC 操作,然而並沒有任何改善。直到這裡,我嚴重懷疑是出現了記憶體洩漏了。
於是我執行了 jmap -dump
,把堆、執行緒資訊 dump 下來,然後拉到本地分析。不看不知道,一看嚇一跳,執行緒多到令人窒息。
不得不說,有一點他們做的非常好,竟然貼心的給執行緒編了號,沒錯,就是有這麼多執行緒 10萬多個。於是我們算了一下假設 10分鐘請求 300 次,那就是 300 個執行緒,一小時就是 30 x 6=1800,一天24小時就是1800 x 24=43200,兩天多的時間 10萬多個執行緒那就正好對上了,好牛x的樣子。
一個執行緒預設佔用空間大小 1M,10萬多個執行緒那就是 10個多G,加上堆記憶體佔用和機器上其他服務的記憶體佔用,記憶體飆到 15G 就對的上了。
誰的問題誰處理
有問題就找問題就這麼難嗎,不承認自己的程式有問題是怎麼想的呢。
好啊,你們自己不查,我幫你找到問題原因了,滿意了吧。
於是,同事理直氣壯的把上面那張截圖發給他們,但是沒有額外說一句話。
下午,微信群裡對方發來訊息,問題已修改,可以再試試。
然後,好多天過去了,問題沒有再出現。
規避問題
有的同學問了,系統能建立10萬多個執行緒嗎,有可能的。這篇文章是「你假笨」大神寫的 Linux 系統下能建立多少個執行緒的原始碼分析 https://club.perfma.com/article/244079,有興趣可以上去看一看。
這個問題產生的原因就是執行緒建立了但是沒有銷燬,估計是銷燬邏輯寫的有些問題吧。
拋開邏輯錯誤不說,使用執行緒的正確做法是使用執行緒池,以免帶來不必要的效能損耗和這種未加控制、未及時銷燬帶來的執行緒無止境建立的問題。