這將會是一篇比較邪惡的文章,當你想在某個人的生活中制造悲劇時你可能會去google搜索它。在Java的世界里,內存溢出僅僅只是你在這種情況下可能會引入的一種bug。你的受害者會在辦公室里度過幾天甚至是幾周的不眠之夜。
在這篇文章中我將會介紹兩種溢出方式,它們都是比較容易理解和重現的。并且它們都是來源現實項目的案例研究,但是為了讓你清晰地掌握,我把它們簡化了。
不過放心,在我們遇到和解決了很過溢出bug之后,類似的案例將會比你想象得更加普遍。
先來一個進入狀態的,在使用HashSet/HashMap時,所用鍵值沒有或者其equals()/hashCode()方法不正確,這會導致一個臭名昭著的錯誤。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
class KeylessEntry { static class Key { Integer id; Key(Integer id) { this .id = id; } @Override public int hashCode() { return id.hashCode(); } } public static void main(String[] args) { Map m = new HashMap(); while ( true ) for ( int i = 0 ; i < 10000 ; i++) if (!m.containsKey(i)) m.put( new Key(i), "Number:" + i); } } |
當你運行上面的代碼時,你可能會期望它運行起來永遠不會出問題,畢竟內置的緩存方案只會增加到10,000個元素,然后就不會再增加了,所有的key都已經出現在 HashMap中。然而,事情并非如此。元素將會一直增長, 因為Key這個類沒有在hashCode()后實現一個合適的equals()方法。
解決方法很簡單,只要和下面的示例一樣添加一個equals方法就可以了。但是在找到問題所在之前,你肯定已經花費了不少寶貴的腦細胞。
1
2
3
4
5
6
7
8
|
@Override public boolean equals(Object o) { boolean response = false ; if (o instanceof Key) { response = (((Key)o).id).equals( this .id); } return response; } |
下一個你得提醒朋友的是和String處理相關的操作。它的表現會很詭異,特別是結合JVM版本差異的時候。String的內部工作機制在 JDK 7u6中被改變了,所以如果你發現產品環境只是小版本號的區別,那么你已經準備好條件了。把類似下面的代碼給你的朋友調試,然后問他為什么這個bug只會在產品中出現。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
class Stringer { static final int MB = 1024 * 512 ; static String createLongString( int length){ StringBuilder sb = new StringBuilder(length); for ( int i= 0 ; i < length; i++) sb.append( 'a' ); sb.append(System.nanoTime()); return sb.toString(); } public static void main(String[] args){ List substrings = new ArrayList(); for ( int i= 0 ; i< 100 ; i++){ String longStr = createLongString(MB); String subStr = longStr.substring( 1 , 10 ); substrings.add(subStr); } } } |
上面的代碼出了什么問題呢?當它在JDK 7u6之前的版本上運行的時候,返回的字符串將會保存一個對那個1M左右大小的字符串的引用,如果你運行的時候設置為-Xmx100m,你會得到一個意想不到的oom錯誤。結合你實驗環境中平臺和版本的差異,傷腦經的事情就產生了。
現在如果你想掩蓋你的足跡,我們可以引進一些更加高級的概念。比如
- 在不同的類加載器中載入有破壞性的代碼,在加載的類被原始類加載器刪除后保持對它的引用,可以模擬一個類加載器溢出
- 把攻擊性的代碼隱藏在finalize方法中,使得程序表現變得不可預測
- 在一個長期運行的線程中加入棘手的組合,它可能在ThreadLocals中保存了一些可以被線程池訪問的東西,以便管理應用線程。
我希望我們給了你一些思考的原材料以及當你想修理某人時的一些素材。這將帶來無窮無盡的調試。除非你的朋友使用 Plumbr來查找溢出的所在地。