我網站不知道什麼時候,開始內存飆升,從 Tomcat 啓動後,初始內存佔用4%~5%
左右,到20%、40%
最後服務器卡死,SSH都連不上服務器,不得不重啓。但是我知道是我程序的問題。然後分析問題,解決問題。陸陸續續持續了一個多月,下面分享解決思路。
一、定位造成內存溢出可能存在的問題
- io流操作文檔沒關閉流。
- 往一個靜態集合變量裏一直壓棧。
- 連接沒釋放。
- Java隊列沒消耗。
- Ehcache緩存使用量過大。
- 頻繁IO操作大文件。
- Session過期時間太久。
- 等等.....
我定位有可能造成的原因是以上原因,針對本站的特點在做細排查,有可能出現的問題。
- io流操作文檔沒關閉流。(有可能)
- 往一個靜態集合變量裏一直壓棧。(沒有這個問題)
- 鏈接沒釋放。(有可能,因爲本站有大量的HttpClient請求)
- Java隊列沒消耗。(有可能,因爲本站使用上了)
- Ehcache緩存使用量過大。(沒使用)
- 頻繁IO操作大文件。(沒有)
- Session過期時間太久。(可能有)
- 等等.....
二、採用Memory Analyzer Tool(MAT)分析Java內存
採用 jmap 命令(Java Memory Map)導出內存轉儲快照(Dump);
首先查詢到你對應的 Tomcat 的pid
ps -aux|grep xxx-tomcat
然後執行jmap命令:
jmap -dump:format=b,file=73630.hprof 16706
導出完畢。down下來用 Eclipse ,或者 MyEclipse 查看,但是 MyEclipse
或者 Eclipse
要先安裝工具,自行百度。然後以openFile
的方式打開。如圖:
可能有點看不懂,自行解決,點擊Histogram
,可以看到內存中的詳細信息。
可以看到char[]
、byte[]
佔用的是最多,而且不是多一點點。這明顯不正常。就是一些IO流相關的信息。Memory Analyzer
工具還是有很多功能的,我也不太會用。具體可以多看看相關的博客。下面來排查問題。
三、問題逐一排查,由容易到複雜
3.1 Session檢查
從配置文件web.xml
查看,發現 Session 超時配置了900
分鐘。。。醉了,回想起來,是當時因爲有權限校驗(防止攻擊)模塊利用
Session 來實現,所以才出此下策。改成30
分鐘,重啓後效果有一點點。繼續排查。
3.2 IO流操作沒關閉檢查(嚴重)
全局搜索各種InputStream
、OutputStream
,各種Buffer
等等,然後各種修改關閉。尤其是本站的 HTTP
模擬請求工具,一天的用量非常大。如下IO
流在finally
裏try...catch
各種關閉。
try{
//......
}catch(Exception e){
//......
}finally{
realUrl = null;
try {
if(null != conn)conn.disconnect();
conn = null;
} catch (Exception e2) {
LoggerUtils.fmtError(HttpManager.class, e2, "請求完畢關閉流出現異常,可以忽略![%s]", url);
}
try {
if(null != outStream)outStream.close();
outStream = null;
} catch (Exception e2) {
LoggerUtils.fmtError(HttpManager.class, e2, "請求完畢關閉流出現異常,可以忽略![%s]", url);
}
try {
if(null != out)out.close();
out = null;
} catch (Exception e2) {
LoggerUtils.fmtError(HttpManager.class, e2, "請求完畢關閉流出現異常,可以忽略![%s]", url);
}
try {
if(null != inStream)inStream.close();
inStream = null;
} catch (Exception e2) {
LoggerUtils.fmtError(HttpManager.class, e2, "請求完畢關閉流出現異常,可以忽略![%s]", url);
}
try {
if(null != in)in.close();
in = null;
} catch (Exception e2) {
LoggerUtils.fmtError(HttpManager.class, e2, "請求完畢關閉流出現異常,可以忽略![%s]", url);
}
double end = System.currentTimeMillis();
map.put("time", (end - begin) / 1000);
//大對象用完賦值null
bo = null;//促進回收
}
結論:全部加好後重啓,過一段時間再看。效果不明顯。其實是我在平時代碼嚴謹上這個錯誤沒有出現,但是從經驗角度來說,如果這個沒處理好,這個是最容易出現 內存溢出 的。
ps:關於後面有一段代碼,bo=null;//促進回收
,我個人是這麼理解,不知道有沒毛病。主要是針對局部變量的大變量。可以用完後賦值爲null
。
3.3 HttpClient請求鏈接釋放問題(嚴重)
Httpclent 請求鏈接不主動關閉,這個問題也是個大問題,但是對內存的影響,看從什麼角度,佔用最大的應該還是響應鏈接,把鏈接用完了,新的鏈接就進不來,我們知道 Tomcat 默認配置一共好像才150
個,一會就用完了,如果不用完關閉,那麼會造成鏈接釋放慢,甚至不釋放。如果不釋放,請求得到的responseBody
那麼有可能一直沒有釋放了。
HttpClient怎麼釋放?
其實百度一下有很多答案,我這裏順便帶一下。
1.請求頭增加關閉Head信息。
//添加頭信息
HttpURLConnection conn = null;
URL realUrl = new URL(url);
// 打開和URL之間的連接
conn = (HttpURLConnection) realUrl.openConnection();
//省略部分代碼
//增加請求完畢後關閉鏈接的頭信息
conn.setRequestProperty("Connection", "close");
2.用完Httpclent後手動關閉
//添加頭信息
HttpURLConnection conn = null;
URL realUrl = new URL(url);
// 打開和URL之間的連接
conn = (HttpURLConnection) realUrl.openConnection();
//省略部分代碼
//手動釋放
if(null != conn)conn.disconnect();
3.通過線程的方式掃描關閉。
這個方法其實類似啓動一個守護線程(一直啓動着),來掃描有沒有關閉的請求。這個方法比較雞肋,用的好就很好,用的不好就蛋痛了。推薦使用方法一、方法二,爲了保險起見你可以兩種一起使用,並不會有問題。
總結: Httpclent 鏈接請求完畢一定要關閉。有的人可能會看了瀏覽器裏的請求頭信息爲:Connection:keep-alive
,這個回頭我會詳細說明,但是這個是瀏覽器需要的,因爲你還要繼續加載css、js、image
等等,大概是這個意思,而你的 Httpclent 只需要加載一次,所以直接close
即可。
3.3 Java隊列(最終問題定位)
昨晚把 隊列 換成了阿里的隊列,問題解決了,幾個小時過去了,還是5.6%
。
換成了阿里的 隊列 ,我把隊列用於本地計算機跑,線上跑網站,把所有隊列的錯誤信息,以及執行情況看了一下,發現是之前邏輯寫的有問題,導致隊列異常,隊列異常沒有catch及時處理。故導致了這個現象的最大的罪魁禍首。