1
// Can you spot the "memory leak"?
2
public class Stack {
3
private Object[] elements;
4
private int size = 0;
5
private static final int DEFAULT_INITIAL_CAPACITY = 16;
6
7
public Stack() {
8
elements = new Object[DEFAULT_INITIAL_CAPACITY];
9
}
10
11
public void push(Object e) {
12
ensureCapacity();
13
elements[size++] = e;
14
}
15
16
public Object pop() {
17
if (size == 0)
18
throw new EmptyStackException();
19
return elements[--size];
20
}
21
22
/**
23
* Ensure space for at least one more element, roughly doubling the capacity
24
* each time the array needs to grow.
25
*/
26
private void ensureCapacity() {
27
if (elements.length == size)
28
elements = Arrays.copyOf(elements, 2 * size + 1);
29
}
30
}
這段代碼看上去沒有什么錯,每次測試的時候都可以成功。但是這里有一個潛在的問題,泛義的講就是程序存在內存泄漏,由于不斷增加的垃圾回收器活動或者增加的內存訪問,而使性能降低被暴露出來。極端情況下,這種內存泄漏會導致磁盤頁面訪問甚至程序出現OutOfMemoryError的錯誤。但是這種錯誤是很少出現的。
2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

當stack增長和收縮時,即使程序不再使用它們,出棧的對象不會被垃圾收集。由于stack維護著這些對象的過時的引用。這種過時的引用是一種不再被重新引用的簡單引用。在這種情況下,任何出了活動區的元素引用都是過時的?;顒訁^包含著一個小于size的元素index。
具有垃圾回收器的語言,也稱為無意的對象保持,內存泄漏是潛在的。如果一個對象是無意的保持,不僅垃圾回收器不能包含他們,而且其他任何對象也不能引用它們。
修改這種錯誤的方法很簡單,一旦他們不再使用,設置它的引用為null。例如stack類,當元素出棧后就會變成不再使用的對性。正確的pop函數應該這樣寫:
1
public Object pop() {
2
if (size == 0)
3
throw new EmptyStackException();
4
Object result = elements[--size];
5
elements[size] = null; // Eliminate obsolete reference
6
return result;
7
}
設置過時的對象引用為null的另外一個好處是,如果它們被錯誤的重新引用,程序會立即拋出NullPointerException,而不是靜靜的做錯誤的事情。
2

3

4

5

6

7

當程序員第一次遇到這種問題后,他們可能會過度在每個對象引用后設置為null。這是不需要而且不可取的。設置對象引用為null應該是一個例外,而不是一個準則。消除這種過時對象引用的最好方法是讓包含引用的變量在范圍外訪問時發生錯誤。
設置引用為null的時機是什么?Stack的的那些方面容易遭受內存泄漏?簡單的說就是管理自己的內存。存儲池保持著一個元素列表,這個列表中處于活動區的元素是被分配的。列表中剩余的元素是空閑的。垃圾回收器不知道這些。對于垃圾回收器,所有列表中的元素都是有效的。只有程序員知道列表中不活動的部分是不重要的。程序員只有設置不活動的部分為null時,垃圾回收器才能感知到。
總的來說,任何時候,當一個類管理自己的內存,程序員應該警惕內存泄漏。當元素釋放的時候,包含元素的對象引用應該設置為null。
另外一個內存泄漏的例子是緩存cache。當你把一個對象引用放進cache,很容易遺忘掉在它變為不相關的時候它還在cache中。解決這種問題的方法是將cache表示成為一個WeakHashMap,入口點將會在他們過時的時候自動刪除。記住,WeakHashMap只在當一個期望的cache入口在被外面的引用到key,而不是值的時候才是有用的。
更為常見的是,cache的缺少好的有用生命周期定義。在這種情況下,cache應該在當entry沒有用的時候清除它們??梢酝ㄟ^后臺線程(Timer或者ScheduledThreadPoolExecutor)或者as a side effect of adding new entries to the cache來實現。LinkedHashMap類用removeEldestEntry方法實現了后者方式。對于好的cache,應該直接使用java.lang.ref。
第三種常見的內存泄漏的例子就是監聽器和一些回調listeners and other callbacks.如果你實現一個API,客戶注冊回調但是不顯式的注銷回調,它們就會堆積起來,除非你采取措施。確保回調被垃圾回收的最好方式是存儲weak reference給它們,例如將它們僅僅作為key存儲在WeakHashMap中。
因為內存泄漏是典型的沒有明顯的錯誤特征,它們可能在一個系統中保持數年。但是它們可以被仔細的代碼檢查或者heap profiler這樣的調試工具發現。因此,需要預知這些問題而且避免它們的發生。