標籤:
Q:剛才我參加了面試,面試官問我如何寫出會發生記憶體泄露的Java代碼。這個問題我一點思路都沒有,好囧。
A1:通過以下步驟可以很容易產生記憶體泄露(程式碼不能訪問到某些對象,但是它們仍然儲存在記憶體中):
- 應用程式建立一個長時間啟動並執行線程(或者使用線程池,會更快地發生記憶體泄露)。
- 線程通過某個類載入器(可以自訂)載入一個類。
- 該類分配了大塊記憶體(比如new byte[1000000]),在某個靜態變數儲存一個強引用,然後在ThreadLocal中儲存它自身的引用。分配額外的記憶體new byte[1000000]是可選的(類執行個體泄露已經足夠了),但是這樣會使記憶體泄露更快。
- 線程清理自訂的類或者載入該類的類載入器。
- 重複以上步驟。
由於沒有了對類和類載入器的引用,ThreadLocal中的儲存就不能被訪問到。ThreadLocal持有該對象的引用,它也就持有了這個類及其類載入器的引用,類載入器持有它所載入的類的所有引用,這樣GC無法回收ThreadLocal中儲存的記憶體。在很多JVM的實現中Java類和類載入器直接分配到permgen地區不執行GC,這樣導致了更嚴重的記憶體泄露。
這種泄露模式的變種之一就是如果你經常重新部署以任何形式使用了ThreadLocal的應用程式、應用程式容器(比如Tomcat)會很容易發生記憶體泄露(由於應用程式容器使用了如前所述的線程,每次重新部署應用時將使用新的類載入器)。
A2:
靜態變數引用對象
2 |
static final ArrayList list = new ArrayList( 100 ); |
調用長字串的String.intern()
1 |
String str=readString(); // read lengthy string any source db,textbox/jsp etc.. |
2 |
// This will place the string in memory pool from which you cant remove |
未關閉已開啟流(檔案,網路等)
2 |
BufferedReader br = new BufferedReader( new FileReader(inputFile)); |
5 |
} catch (Exception e) { |
未關閉串連
2 |
Connection conn = ConnectionFactory.getConnection(); |
5 |
} catch (Exception e) { |
JVM的GC不可達地區
比如通過native方法分配的記憶體。
web應用在application範圍的對象,應用未重啟或者沒有顯式移除
getServletContext().setAttribute("SOME_MAP", map);
web應用在session範圍的對象,未失效或者沒有顯式移除
session.setAttribute("SOME_MAP", map);
不正確或者不合適的JVM選項
比如IBM JDK的noclassgc阻止了無用類的記憶體回收
A3:如果HashSet未正確實現(或者未實現)hashCode()或者equals(),會導致集合中持續增加“副本”。如果集合不能地忽略掉它應該忽略的元素,它的大小就只能持續增長,而且不能刪除這些元素。
如果你想要建置錯誤的索引值對,可以像下面這樣做:
2 |
// no hashCode or equals(); |
3 |
public final String key; |
4 |
public BadKey(String key) { this .key = key; } |
7 |
Map map = System.getProperties(); |
8 |
map.put( new BadKey( "key" ), "value" ); // Memory leak even if your threads die. |
A4:除了被遺忘的監聽器,靜態引用,hashmap中key錯誤/被修改或者線程阻塞不能結束生命週期等典型記憶體泄露情境,下面介紹一些不太明顯的Java發生記憶體泄露的情況,主要是線程相關的。
- Runtime.addShutdownHook後沒有移除,即使使用了removeShutdownHook,由於ThreadGroup類對於未啟動線程的bug,它可能不被回收,導致ThreadGroup發生記憶體泄露。
- 建立但未啟動線程,與上面的情形相同
- 建立繼承了ContextClassLoader和AccessControlContext的線程,ThreadGroup和InheritedThreadLocal的使用,所有這些引用都是潛在的泄露,以及所有被類載入器載入的類和所有靜態引用等等。這對ThreadFactory介面作為重要組成元素整個j.u.c.Executor架構(java.util.concurrent)的影響非常明顯,很多開發人員沒有注意到它潛在的危險。而且很多庫都會按照請求啟動線程。
- ThreadLocal緩衝,很多情況下不是好的做法。有很多基於ThreadLocal的簡單緩衝的實現,但是如果線程在它的期望生命週期外繼續運行ContextClassLoader將發生泄露。除非真正必要不要使用ThreadLocal緩衝。
- 當ThreadGroup自身沒有線程但是仍然有子線程組時調用ThreadGroup.destroy()。發生記憶體泄露將導致該線程組不能從它的父線程組移除,不能枚舉子線程組。
- 使用WeakHashMap,value直接(間接)引用key,這是個很難發現的情形。這也適用於繼承Weak/SoftReference的類可能持有對被保護對象的強引用。
- 使用http(s)協議的java.net.URL下載資源。KeepAliveCache在系統ThreadGroup建立新線程,導致當前線程的上下文類載入器記憶體泄露。沒有存活線程時線程在第一次請求時建立,所以很有可能發生泄露。(在Java7中已經修正了,建立線程的代碼合理地移除了上下文類載入器。)
- 使用InflaterInputStream在建構函式(比如PNGImageDecoder)中傳遞new java.util.zip.Inflater(),不調用inflater的end()。僅僅是new的話非常安全,但如果自己建立該類作為建構函式參數時調用流的close()不能關閉inflater,可能發生記憶體泄露。這並不是真正的記憶體泄露因為它會被finalizer釋放。但這消耗了很多native記憶體,導致linux的oom_killer殺掉進程。所以這給我們的教訓是:儘可能早地釋放native資源。
- java.util.zip.Deflater也一樣,它的情況更加嚴重。好的地方可能是很少用到Deflater。如果自己建立了Deflater或者Inflater記住必須調用end()。
原文: http://www.open-open.com/bbs/view/1409626364853
如何用Java編寫一段代碼引發記憶體泄露