聊聊內存泄露
1. 什么是內存泄露
看到網上有很多人都在問內存泄露與內存溢出的區別,而且后面還有一大堆的跟帖在用不同形式的語言予以解答,我看了以后思緒萬千啊。內存泄露是導致內存溢出的原因之一,說他們的區別純屬無稽之談。要解釋什么是內存泄露還真是個費事的活,我用一個例子來解釋下:
public class Test { public static void main(String[] args) { List<String> list = new ArrayList<String>(); while (true) { String test = new String("111"); list.add(test); } } }
上面的代碼會不停的往list中添加數據,當我們的堆空間不足時,就會報OOM的錯誤,如下:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.leak.Test.main(Test.java:11)
當堆空間不足的時候,JVM會進行垃圾回收,但是垃圾回收時卻發現這些對象都是有用的,不能回收。我們可能會考慮增大堆空間大小,可是這還是于是無補,因為我們有個死循環。這種情況其實就是內存泄露的一個簡單例子,簡而言之,如果是內存泄露引起的OOM,那肯定是我們代碼有問題,需要修正代碼。
2. 如何確定OOM是由于內存泄露引起的
在工作中,遇到OOM,你首先要確定他是由于什么原因引起的?是因為堆空間設置太小引起還是因為內存泄露引起。實際上,內存泄露的問題可以通過增大堆空間暫時得到解決,但是他不是長久之計。
我們可以通過對應用訪問峰值時堆空間利用率的分析來確定應用是否存在內存泄露,比如我們可以用JMeter來進行壓力測試,我們每次對應用加壓1000,一共加壓10次,第一次峰值時堆使用了100M,第二次峰值時使用了200M,第三次峰值時使用了300M….那這樣我們基本可以確定應用存在內存泄露。因為正常情況下,每次峰值時的堆占用率應該是差不多的,而上面的例子每次峰值時數據出入都比較大,而且是逐步增加,這不是一個正常的現象。
觀察內存的使用情況,你可以使用JConsole或者VisualVM等工具,我比較喜歡從GC的日志中得到我想要的信息,每次峰值時由于堆空間吃緊,肯定會觸發一次GC,我通過這幾次GC記錄可以明了的看到堆內存情況。我們可以通過配置JVM參數來啟用一些基本的GC日志,比如-verbose:gc、-XX:+PrintGCTimeStamps、-XX:+PrintGCDetails、-Xloggc:<file>。至于如何讀GC日志,我博客里有其他文章講解,Oracle官網也有比較好的例子。
總結一下,如何確定應用存在內存泄露問題,我們需要觀察峰值時的堆內存變化,比如堆的使用情況像下圖一樣,那肯定是存在Memory Leak了。
3. 如何定位引起內存泄露的代碼
首先我們可以看發生OOM時的代碼,比如上面的例子,我們大概可以知道在執行哪段代碼時發生了錯誤,然后重點看下這部分代碼。當然,那部分代碼不一定就是導致OOM的代碼。
接下來我們需要分析堆快照,可以為JVM配置發生OOM時出生堆快照文件(+XX:+HeapDumpOnOutOfMemoryError),或者使用jmap命令產生。注意生成堆快照文件時應用會停止運行,所以千萬不要在生產環境中這么搞。
拿到堆快照文件后,我們使用Mat或者VisualVM工具進行分析。借助這些工具,我們可以根據實例數、占用大小對目前堆中的所有實例進行排序,那排在前幾位的就是你要重點分析的。
前面講的方法很容易就能找出大范圍的Memory Leak代碼,但是對于一些小的內存溢出問題,我們可能就比較難發現了,我的經驗是先定位是哪些功能點引起的內存泄露,然后重點去壓這部分功能,放大他們的影響之后再去分析。