Effective-Java-6消除过期的对象引用

当你从手动管理内存的语言(比如C或者C++)转换到具有垃圾回收功能的语言时,工作会更加容易,因为当对象使用完毕后将会自动回收。但是,这并不代表着程序员再也不需要考虑内存管理的事情了。

考虑以下这个简单栈实现的例子:

1
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
// Can you spot the "memory leak"?
public class Stack {
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private Object[] elements;
private int size = 0;

public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}

public void push(Object e) {
elements[size++] = e;
}

public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}

/**
* Ensure space for at least one more element, roughly doubling the capacity each time the array needs to grow.
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}

这段程序(泛型版见26条)中并没有很明显的错错误。但其中隐藏着一个问题。不严格的讲,这段程序存在”内存泄漏”,随着内存的占用不断增加,程序的性能降低将会越来越明显。在极端的情况下,这种内存泄漏会导致磁盘交换(Disk Paging),甚至导致程序失败(OutOfMemoryError),虽然这种情况相对少见。

错误在于,如果一个栈先是增长,然后再收缩,那么,从栈中弹出来的对象将不会被当作垃圾回收,即时是使用栈的程序不在引用这些对象,它们也不会被回收。因为,栈内部维护着对这些对象的过期引用(obsolete reference)。所谓的过期引用,是指永远也不会在被解除的引用。在本示例中,凡是在elements数字的”活动部分(active protion)”之外的任何引用都是过期的。活动部分是指elements中下标小于size的那些元素。

在支持垃圾回收的语言中,内存泄漏是很隐蔽的(称这些内存泄漏为”无意识的对象保持(unintentional object retention)”更为恰当)。如果一个对象引用被无意识的保留起来了,也会有许许多多的对象被排除在垃圾回收机制之外,从而对性能造成潜在的重大影响。

这类问题的修复方法很简单:一旦对象引用已经过期,只需清空这些引用即可。对于上述例子中的Stack而言,只要有数据被弹出栈,指向它的引用就过期了。pop方法修改版如下:

1
2
3
4
5
6
7
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}

Ryuu: elements的中的元素本身就和size不相关,仅是返回个值栈末尾的元素当然不会被回收,要将elements对其元素的引用置空,让元素没有被指针指向,该元素才会被当作垃圾回收。

以下是 Ryuu 的示例代码,您可以打断点尝试,不置空元素确实不会被回收

1
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import java.util.Arrays;
import java.util.EmptyStackException;

public class Rule6 {
public static void main(String[] args) {
Stack stack = new Stack();
for (int i = 0; i < 10; i++)
stack.push("1");
for (int i = 0; i < 10; i++)
stack.pop(); // break point
System.out.println("complete"); // break point
}
}

// Can you spot the "memory leak"?
class Stack {
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private Object[] elements;
private int size = 0;

public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}

public void push(Object e) {
elements[size++] = e;
}

public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}

/**
* Ensure space for at least one more element, roughly doubling the capacity each time the array needs to grow.
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}



class Stack {
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private Object[] elements;
private int size = 0;

public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}

public void push(Object e) {
elements[size++] = e;
}

public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}

/**
* Ensure space for at least one more element, roughly doubling the capacity each time the array needs to grow.
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}

清空过期引用的另一个好处是,如果它们以后又被错误的引用,程序就会立即抛出NullPointerException异常,而不是悄悄地错误运行下去。尽快地检测出程序中的错误总是有益的。

当程序员第一次被类似这样的问题困扰的时候,它们往往会过分小心: 对于每一个对象引用,一旦程序不再用到它,就把他清空。其实这样做并没有必要,会把程序弄得很乱。清空对象引用应该是种例外,而不是一种规范行为。消除过期引用的最好的方法是让包含该引用的变量结束其生命周期。如果你是在最紧凑的作用范围内定义每一个变量(见第45条),这种情形就会自然而然地发生。

内存泄漏常见的三个来源

1. 自行管理内存类。

一般在这种情况下,程序员应警惕其可能的内存泄漏问题。一旦元素被释放掉,则该元素种包含的任何对象引用都应该被清空。

2. 缓存。

一旦将对象引用放到缓存中,很容易就会被遗忘掉,从而使得它不在有用之后很长一段时间内仍然留在缓存中。

3. 监听器和其他回调。

如果实现了一个API,客户端在这个API当中注册回调,却没有显式的取消注册 ,那么除非你采取某些行动,否则它们就会积聚。确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用(weak reference),例如,只将它们保存成WeakHashMap中的键。

由于内存泄漏通常不会表现成明显的失败,所以它们可以在一个系统中存在很多年。往往只有通过检查代码,或者借助于Heap剖析工具(Heap Profiler)才能发现内存泄漏问题。因此,如果能够在内存泄漏发生之前就知道如何预测此类问题,并阻止它们发生,那是最好不过了。