1. 程式人生 > >記憶體溢位和洩露

記憶體溢位和洩露

一、記憶體溢位和記憶體洩露

一種通俗的說法。
1、記憶體溢位:你申請了10個位元組的空間,但是你在這個空間寫入11或以上位元組的資料,出現溢位。
2、記憶體洩漏:你用new申請了一塊記憶體,後來很長時間都不再使用了(按理應該釋放),但是因為一直被某個或某些例項所持有導致 GC 不能回收,也就是該被釋放的物件沒有釋放。

下面具體介紹。

1.1 記憶體溢位

java.lang.OutOfMemoryError,是指程式在申請記憶體時,沒有足夠的記憶體空間供其使用,出現OutOfMemoryError。
產生原因
產生該錯誤的原因主要包括:

JVM記憶體過小。
程式不嚴密,產生了過多的垃圾。
程式體現
一般情況下,在程式上的體現為:

記憶體中載入的資料量過於龐大,如一次從資料庫取出過多資料。
集合類中有對物件的引用,使用完後未清空,使得JVM不能回收。
程式碼中存在死迴圈或迴圈產生過多重複的物件實體。
使用的第三方軟體中的BUG。
啟動引數記憶體值設定的過小。
錯誤提示
此錯誤常見的錯誤提示:
tomcat:java.lang.OutOfMemoryError: PermGen space
tomcat:java.lang.OutOfMemoryError: Java heap space
weblogic:Root cause of ServletException java.lang.OutOfMemoryError
resin:java.lang.OutOfMemoryError
java:java.lang.OutOfMemoryError

解決方法

增加JVM的記憶體大小
對於tomcat容器,找到tomcat在電腦中的安裝目錄,進入這個目錄,然後進入bin目錄中,在window環境下找到bin目錄中的catalina.bat,在linux環境下找到catalina.sh。
編輯catalina.bat檔案,找到JAVA_OPTS(具體來說是 set "JAVA_OPTS=%JAVA_OPTS% %LOGGING_MANAGER%")這個選項的位置,這個引數是Java啟動的時候,需要的啟動引數。
也可以在作業系統的環境變數中對JAVA_OPTS進行設定,因為tomcat在啟動的時候,也會讀取作業系統中的環境變數的值,進行載入。
如果是修改了作業系統的環境變數,需要重啟機器,再重啟tomcat,如果修改的是tomcat配置檔案,需要將配置檔案儲存,然後重啟tomcat,設定就能生效了。
優化程式,釋放垃圾
主要思路就是避免程式體現上出現的情況。避免死迴圈,防止一次載入太多的資料,提高程式健壯型及時釋放。因此,從根本上解決Java記憶體溢位的唯一方法就是修改程式,及時地釋放沒用的物件,釋放記憶體空間。
1.2 記憶體洩露

Memory Leak,是指程式在申請記憶體後,無法釋放已申請的記憶體空間,一次記憶體洩露危害可以忽略,但記憶體洩露堆積後果很嚴重,無論多少記憶體,遲早會被佔光。
在Java中,記憶體洩漏就是存在一些被分配的物件,這些物件有下面兩個特點:
1)首先,這些物件是可達的,即在有向圖中,存在通路可以與其相連;
2)其次,這些物件是無用的,即程式以後不會再使用這些物件。
如果物件滿足這兩個條件,這些物件就可以判定為Java中的記憶體洩漏,這些物件不會被GC所回收,然而它卻佔用記憶體。

關於記憶體洩露的處理頁就是提高程式的健壯型,因為記憶體洩露是純程式碼層面的問題。

1.3 記憶體溢位和記憶體洩露的聯絡

記憶體洩露會最終會導致記憶體溢位。
相同點:都會導致應用程式執行出現問題,效能下降或掛起。
不同點:1) 記憶體洩露是導致記憶體溢位的原因之一,記憶體洩露積累起來將導致記憶體溢位。2) 記憶體洩露可以通過完善程式碼來避免,記憶體溢位可以通過調整配置來減少發生頻率,但無法徹底避免。

二、一個Java記憶體洩漏的排查案例

某個業務系統在一段時間突然變慢,我們懷疑是因為出現記憶體洩露問題導致的,於是踏上排查之路。

2.1 確定頻繁Full GC現象

首先通過“虛擬機器程序狀況工具:jps”找出正在執行的虛擬機器程序,最主要是找出這個程序在本地虛擬機器的唯一ID(LVMID,Local Virtual Machine Identifier),因為在後面的排查過程中都是需要這個LVMID來確定要監控的是哪一個虛擬機器程序。
同時,對於本地虛擬機器程序來說,LVMID與作業系統的程序ID(PID,Process Identifier)是一致的,使用Windows的工作管理員或Unix的ps命令也可以查詢到虛擬機器程序的LVMID。
jps命令格式為:
jps [ options ] [ hostid ]
使用命令如下:
使用jps:jps -l
使用ps:ps aux | grep tomat

找到你需要監控的ID(假設為20954),再利用“虛擬機器統計資訊監視工具:jstat”監視虛擬機器各種執行狀態資訊。
jstat命令格式為:
jstat [ option vmid [interval[s|ms] [count]] ]
使用命令如下:
jstat -gcutil 20954 1000
意思是每1000毫秒查詢一次,一直查。gcutil的意思是已使用空間站總空間的百分比。
結果如下圖:

 

jstat執行結果
查詢結果表明:這臺伺服器的新生代Eden區(E,表示Eden)使用了28.30%(最後)的空間,兩個Survivor區(S0、S1,表示Survivor0、Survivor1)分別是0和8.93%,老年代(O,表示Old)使用了87.33%。程式執行以來共發生Minor GC(YGC,表示Young GC)101次,總耗時1.961秒,發生Full GC(FGC,表示Full GC)7次,Full GC總耗時3.022秒,總的耗時(GCT,表示GC Time)為4.983秒。

2.2 找出導致頻繁Full GC的原因

分析方法通常有兩種:
1)把堆dump下來再用MAT等工具進行分析,但dump堆要花較長的時間,並且檔案巨大,再從伺服器上拖回本地匯入工具,這個過程有些折騰,不到萬不得已最好別這麼幹。
2)更輕量級的線上分析,使用“Java記憶體影像工具:jmap”生成堆轉儲快照(一般稱為headdump或dump檔案)。
jmap命令格式:
jmap [ option ] vmid
使用命令如下:
jmap -histo:live 20954
檢視存活的物件情況,如下圖所示:

 

存活物件
按照一位IT友的說法,資料不正常,十有八九就是洩露的。在我這個圖上物件還是挺正常的。

我在網上找了一位博友的不正常資料,如下:

 

image.png
可以看出HashTable中的元素有5000多萬,佔用記憶體大約1.5G的樣子。這肯定不正常。

2.3 定位到程式碼

定位帶程式碼,有很多種方法,比如前面提到的通過MAT檢視Histogram即可找出是哪塊程式碼。——我以前是使用這個方法。 也可以使用BTrace,我沒有使用過。

舉例:

一臺生產環境機器每次執行幾天之後就會莫名其妙的宕機,分析日誌之後發現在tomcat剛啟動的時候記憶體佔用比較少,但是執行個幾天之後記憶體佔用越來越大,通過jmap命令可以查詢到一些大物件引用沒有被及時GC,這裡就要求解決記憶體洩露的問題。

 

Java的記憶體洩露多半是因為物件存在無效的引用,物件得不到釋放,如果發現Java應用程式佔用的記憶體出現了洩露的跡象,那麼我們一般採用下面的步驟分析:
1. 用工具生成java應用程式的heap dump(如jmap)
2. 使用Java heap分析工具(如MAT),找出記憶體佔用超出預期的嫌疑物件
3. 根據情況,分析嫌疑物件和其他物件的引用關係。
4. 分析程式的原始碼,找出嫌疑物件數量過多的原因。

 下面是具體的案例:

1. 登入linux伺服器,獲取tomcat的pid,命令:

ps -ef|grep java  

 2. 利用jmap初步分析記憶體對映,命令:

jmap -histo:live 3514 | head -7  

第2行是我們業務系統的物件,通過這個物件的引用可以初步分析出到底是哪裡出現了引用未被垃圾回收收集,通知開發人員優化相關程式碼。

 

3. 如果上面一步還無法定位到關鍵資訊,那麼需要拿到heap dump,生成離線檔案,做進一步分析,命令:

jmap -dump:live,format=b,file=heap.hprof 3514 

  

4. 拿到heap dump檔案,利用eclipse外掛MAT來分析heap profile。

a. 安裝MAT外掛

b. 在IDEA裡切換到Memory Analysis檢視

c. 用MAT開啟heap profile檔案。

 

 

直接看到下面Action視窗,有4種Action來分析heap profile,介紹其中最常用的2種:

- Histogram:這個使用的最多,跟上面的jmap -histo 命令類似,只是在MAT裡面可以用GUI來展示應用系統各個類產生的例項。

 

 

Shllow Heap排序後發現 Cms_Organization 這個類佔用的記憶體比較多(沒有得到及時GC),檢視引用: 

 

分析引用棧,找到無效引用,開啟原始碼: