轉自:https://my.oschina.net/hiease/blog/1613871
雖然jvm有垃圾回收機制,如果程序編寫不注意某些特定規則,仍然會導致java程序內存泄漏,最終可能出現OutOfMemory異常。
1.Java內存泄漏的原因
java中的對象從使用上分為2種類型,被引用(referenced)的和不被引用(unreferenced)的。垃圾回收只會回收不被引用的對象。被引用的對象,即使已經不再使用了,也不會被回收。因此如果程序中有大量的被引用的無用對象時,就是出現內存泄漏。
2.java堆內存(Heap)泄漏
jvm堆內存的大小是通過 -Xms 和 -Xmx兩個參數指定的。
2.1 對象被靜態成員引用
當大對象被靜態成員引用時,會造成內存泄漏。
示例:
private Random random = new Random(); public static final ArrayList<Double> list = new ArrayList<Double>(1000000); for (int i = 0; i < 1000000; i++) { list.add(random.nextDouble()); }
ArrayList是在堆上動態分配的對象,正常情況下使用完畢后,會被gc回收,但是在此示例中,由於被靜態成員list引用,而靜態成員是不會被回收的,所以會導致這個很大的ArrayList一直停留在堆內存中。
因此需要特別注意靜態成員的使用方式,避免靜態成員引用大對象或集合類型的對象(如ArrayList等)。
2.2 String的intern方法
在大字符串上調用String.intern() 方法,intern()會將String放在jvm的內存池中(PermGen ),而jvm的內存池是不會被gc的。因此如果大字符串調用intern()方法后,會產生大量的無法gc的內存,導致內存泄漏。
如果必須要使用大字符串的intern方法,應該通過-XX:MaxPermSize參數調整PermGen內存的大小。
2.3 讀取流后沒有關閉
開發中經常忘記關閉流,這樣會導致內存泄漏。因為每個流在操作系統層面都對應了打開的文件句柄,流沒有關閉,會導致操作系統的文件句柄一直處於打開狀態,而jvm會消耗內存來跟蹤操作系統打開的文件句柄。 示例:
BufferedReader br = new BufferedReader(new FileReader(path)); return br.readLine();
要解決這個問題,在java8之前的版本中可以在finally中加入關閉操作:
BufferedReader br = new BufferedReader(new FileReader(path)); try { return br.readLine(); } finally { if (br != null) br.close(); }
java8中可以使用try-with-resources語句:
try (BufferedReader br = new BufferedReader(new FileReader(path))) { return br.readLine(); }
對於網絡連接和數據庫連接等也要注意連接的關閉,如果采用了連接池,那關閉操作是由連接池負責的,程序中可以不用處理。
2.4 將沒有實現hashCode()和equals()方法的對象加入到HashSet中
這是一個簡單卻很常見的場景。正常情況下Set會過濾重復的對象,但是如果沒有hashCode() 和 equals()實現,重復對象會不斷被加入到Set中,並且再也沒有機會去移除。
因此給類都加上hashCode() 和 equals()方法的實現是一個好的編程習慣。可以通過Lombok的@EqualsAndHashCode很方便實現這種功能。
3. 查找內存泄漏的方法
3.1 記錄gc日志
通過在jvm參數中指定-verbose:gc,可以記錄每次gc的詳細情況,用於分析內存的使用。
3.2 進行profiling
通過Visual VM或jdk自帶的Java Mission Control,進行內存分析。
3.3 代碼審查
通過代碼審查和靜態代碼檢查,發現導致內存泄漏問題的錯誤代碼。
4. 總結
代碼層面的檢查可以幫助發現部分內存泄漏的問題,但是生產環境中的內存泄漏往往不容易提前發現,因為很多問題是在大並發場景下才會出現。因此還需要通過壓力測試工具進行壓力測試,提前發現潛在的內存泄漏問題。