如何避免java程式記憶體洩漏
雖然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. 總結
程式碼層面的檢查可以幫助發現部分記憶體洩漏的問題,但是生產環境中的記憶體洩漏往往不容易提前發現,因為很多問題是在大併發場景下才會出現。因此還需要通過壓力測試工具進行壓力測試,提前發現潛在的記憶體洩漏問題。