喜歡用Map卻從未遭遇記憶體泄露的Java程式設計師上輩子都是神仙

前言

點進來這篇文章的大概有兩種人,一種是喜歡用Map的想看看自己是不是有可能也會踩雷,一種是不喜歡用Map的想進來看看那些喜歡用的人是怎麼踩雷的。

那你要失望了,我只是單純把公司最近程式碼審查時一個關於Map的小故事講出來罷了。

如果這樣用過的,可以收手了,沒用過的,引以為鑒。

故事背景

我所在的是一個醫療行業互聯網公司,有些地方比較嚴謹,比如架構和安全這塊,有些地方又十分鬆散,就是沒有好的編碼規範,也沒有程式碼審查,各自為之,自由發揮,所以工作中維護項目時經常能讀到一些爛程式碼。

久而久之也就容易形成下面這張很有名的圖描述的生態:

000.png

所以上個月開始,公司的主管決定開始做程式碼審查,而且每周五開一次審查會,熱烈討論和幫助成長(公開處刑)。

而上周開會有意思的就是一段Map用法相關的程式碼,被特別拎出來做了討論,我就專門把裡面的片段提取出來做了簡化,給大家分享一下問題以及優化方法。

錯誤用法

因為源程式碼涉及的業務內容較多,直接拎出來不便於理解,所以我專門做了簡化,把核心的那塊拿出來展示。

大概的業務是這樣:患者去挂號,我們要查詢未繳費的挂號記錄集合,然後拿到裡面的患者ID和挂號編碼,用這兩個值再去查詢每個挂號下面所開的處方列表,也就是檢查、藥品等等之類的。

好了,下面就是被處刑的一段使用Map的簡化版程式碼:

首先,是查詢未繳費挂號記錄,然後用查到的值再去查詢對應的處方。

111.jpg

看著還好,問題就是第一段返回Map那裡:

222.jpg

獲取了挂號列表,然後進行了一次遍歷,把裡面自己需要的兩個值專門放到Map裡面再返回。

OK,其實很好理解,可能有人看了就會有疑問了,為啥不直接返回列表就好了,還要用一下Map?

根據程式碼本人的解釋是這樣的:因為他要返回的這個對象裡面屬性實在太多了,大概是下面這樣。

(PS:param是我為了核心欄位的保密而修改的,源程式碼里該對象共有幾十個業務欄位。)

777.jpg

因為他嫌棄太多,就用Map只保留了自己需要的那兩個,也算解釋的過去……

問題解析

1、問題到底在哪裡

案例擺出來了,那麼問題是什麼?

就是因為Map你放在了循環里,這是大忌啊!

眾所周知,生產環境查詢的數據往往是動態的,比如案例中的查詢挂號列表,某個時刻也許是1個,也有可能是10個,流量洪峰期更有可能百多個,你想想Map裡面一次性裝了多少這鬼玩意兒。

2、Map最直接作用

Map網上有非常多的文章解析和原理解析,我就不專門解釋了,我這裡給你說其中一個最簡單直接的特點,說白了你可以把它當成項目運行時的快取。

比如項目啟動後,你可以往裡面存一些靜態不會變的配置資訊,這樣項目運行時你直接可以GET到,而不必做多餘的查詢,這也是在服務商模式的支付場景中很常用的一個手段,PUT多個子商戶的配置資訊。

3、形象地看待Map

在Java中Map是一種特殊的工具,它一旦產生就很難清理,會在記憶體中直接開闢一塊空間,你可以想像成建了一個小倉庫,當你PUT鍵值對的時候,其實就是往這個小倉庫中放雜物。

而怪就怪在當你想要把Map清空的時候,結果只是把倉庫里的雜物給丟掉了,小倉庫還在。

很多初學者甚至包括已經工作幾年的Java工程師都不清楚這一點,要麼以為會自動釋放記憶體,要麼以為調用empty()就可以清理掉佔用記憶體,實則不然。

明白這一點以後,你再回頭想一想這個案例,一個患者查詢挂號資訊的時候開闢了1個小倉庫,這個小倉庫裡面又循環存放了N個雜物,算算一個三甲醫院一天估計會有多少個患者看病,每個患者都開闢1個小倉庫存放一堆雜物,你覺得記憶體的心理陰影面積有多大。

4、真會記憶體泄露嗎

這也是為什麼我們的程式碼評審會專門把這位道友的片段撈出來與民同樂的原因,當你無法預估一段程式碼中Map生產量的時候,就是埋下記憶體泄露隱患的時候。

我工作至今,只有在廣州工作期間遇到過一次,而且不是互聯網項目,是電力行業的系統,用戶量也就幾萬人,就因為Map使用不當導致某一天系統忽然跑不動了,一直轉啊轉啊轉,跟著我左手右手一個慢動作……

以2014年當時的一些技術底蘊,很多中小企業並沒有可靠的監控措施,純粹靠人力巡檢,所以出了這種生產環境的問題大家都傻了,然後各種分析各種討論,好在經過大家的同心協力,最後重啟一下好了

後來知道,就是記憶體泄露了,並且找到了核心業務中動態產生Map的程式碼,當時給我驚呆了,還有這種寫法,還好不是我。

其實這些年換了幾家公司,和不同的同事合作過,不分經驗長短,寫這種程式碼的程式設計師在中小企業比你想像的多,但是我遇到的出現記憶體泄露的就那一次,哪怕這次程式碼審查撈出來的也是沒有在線上出現問題的程式碼,只是他被審查了而已。

所以,記憶體泄露沒有你想的那麼容易出現,伺服器水平也和以前大不相同,動輒8核16G什麼的,但不能因為這樣就抱有僥倖的心態,以後沒把握還是別用什麼Map了,萬一你走了把別人坑了呢,就當樂於助人吧。

正確用法

說下正確的用法吧,其實很簡單,遇事不決List,圍繞List來解決一般都沒啥問題。

1、直接List

上面的案例說了,雖然欄位多,但其實省事一點就直接List列表全部返回也沒啥,總比你對著Map瘋狂輸出要強。

333.jpg

就直接返回整個List列表,別整些奇奇怪怪的,在實際工作中,安全比方便重要,亘古不變。

444.jpg

2、拼接字元串

如果你實在不想要那麼多欄位,畢竟有大幾十個,我就只想要我的兩個小可愛。

那也簡單,直接把兩個小可愛做成人體蜈蚣,然後放List返回就行。

獲取挂號列表的時候,直接用一個分隔符比如@,把兩個你要的值拼接起來放入List返回就行。這裡只是拋磚引玉,只要思路是這樣方法有很多,比如你不嫌麻煩的話還可以單獨創建一個DTO就傳輸你想要的幾個欄位也行。

666.jpg

然後查處方時再把兩個小可愛拆開即可

555.jpg

總結

Map原理解析的文章多如牛毛,你可能看過就忘了,也可能看的很煩,沒關係,那些都是造核彈用的,我們就擰螺絲不幹別的,工作時就注意一下我總結的幾點即可高枕無憂。

1、Map最直接的特點是可以作為項目運行時的快取,多用於項目啟動後存放靜態數據,比如配置資訊;

2、Map的產生會開闢一塊記憶體空間,而這塊記憶體空間的數據很好清理,但佔用的記憶體很難清理,往往要重啟後才會徹底釋放;

3、在循環中使用Map是大忌,因為線上環境它的生產量難以預估;

4、查詢時一旦牽扯到動態數據,千萬不要使用Map,圍繞List來做替代。

最後就好奇問下,您也是神仙轉世嗎?


原創文章純手打,一個一個字敲出來的,如果覺得有幫助麻煩點個推薦吧~

本人致力於分享工作中的經驗及趣事,喜歡的話可以進主頁關注一下哦~