Skip to content

Commit 779d218

Browse files
committed
finished recording article 6 of Effective Java
1 parent 0f5a20e commit 779d218

File tree

1 file changed

+33
-2
lines changed

1 file changed

+33
-2
lines changed

docs/effective-java.md

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -402,9 +402,40 @@ public class Stack {
402402
}
403403

404404
}
405-
405+
406406
```
407407

408408
这段程序(它的泛型版本请见第 26 条)中并没有很明显的错误。但是这个程序中隐藏着一个问题。不严格地讲,这段程序有一个“内存泄漏”,随着垃圾回收器活动的增加,或者由于内存占用的不断增加,程序性能的降低会逐渐表现出来。在极端的情况下,这种内存泄漏会导致磁盘交换(`Disk Paging`),甚至导致程序失败(`OutOfMemoryError`错误),但是这种失败情形相对比较少见。
409409

410-
那么,程序中在哪里发生了泄漏呢?如果一个栈先是增长,然后再收缩,那么,从栈中弹出来的对象将不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,它们也不会被回收。这是因为,栈内部维护着对这些对象的过期引用(`Obsolete refence`)。所谓的过期引用,是指永远也不会再被解除的引用。在本例中,凡是在 elements 数组的“活动部分(`active portion`)”之外的任何引用都是过期的。活动部分是指 elements 中小标小于 size 的那些元素。
410+
那么,程序中在哪里发生了泄漏呢?如果一个栈先是增长,然后再收缩,那么,从栈中弹出来的对象将不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,它们也不会被回收。这是因为,栈内部维护着对这些对象的过期引用(`Obsolete refence`)。所谓的过期引用,是指永远也不会再被解除的引用。在本例中,凡是在 elements 数组的“活动部分(`active portion`)”之外的任何引用都是过期的。活动部分是指 elements 中小标小于 size 的那些元素。
411+
412+
在支持垃圾回收的语言中,内存泄漏是非常隐蔽的(称这类内存泄漏为“无意识的对象保持(`unintentional object retention`)“更为恰当)。如果一个对象引用被无意识的保留起来了,那么,垃圾回收机制不仅不会处理这个对象,而且也不会处理被这个对象所引用的所有其它对象。即使只有少量的几个对象被无意识的保留下来,也会有许许多多的对象排除在垃圾回收机制之外,从而对性能造成潜在的重大影响。
413+
414+
这类问题的修复方法很简单:一旦对象引用已经过期,只要清空这些引用即可。对于上述的 Stack 类而言,只要一个单元被弹出栈,指向它的引用就已经过期了。pop 方法的改进版如下:
415+
416+
```java
417+
418+
public Object pop() {
419+
if (size == 0)
420+
throw new EmptyStackException();
421+
elements[size] = null; // Eliminate obsolete reference
422+
return elements[--size];
423+
}
424+
425+
```
426+
427+
清空过期引用的另一个好处是,如果它们以后又被错误的解除引用,程序就会立即抛出 NullPointerException 异常,而不是悄悄地错误运行下去。尽快的检测出程序中的错误往往是有益的。
428+
429+
当程序员第一次被类似这样的问题困扰的时候,他们往往会过分小心:对于每一个对象引用,一旦程序不再用到它,就把它清空。其实这样做即没必要,也不是我们所期望的,因为这样做会把我们的程序弄的很乱。**清空对象引用应该是一种例外,而不是一种规范行为** 。消除过期引用最好的方法是让包含该引用的变量结束其生命周期。如果你是在最紧凑(最小)的作用域范围定义每一个变量(见第 45 条),这种情形就会自然而然的发生。
430+
431+
那么,何时应该清空引用呢?Stack 类的哪方面特性使它易于遭受内存泄漏的影响呢?简而言之,问题在于,Stack 类自己管理内存(`manage its own memory`)。存储池(`storage pool`)包含了 elements 数组(对象应用单元,而不是对象本身)的元素。数组活动区域(同前面的定义)中的元素是已分配的(`allocated`),而数组其余部分的元素则是自由的(`free`)。但是垃圾回收器并不知道这一点;对于垃圾回收器而言,elements 数组中所有的对象引用都同等有效。只有程序员知道数组的非活动部分是不重要的。程序员可以把这个情况告知垃圾回收器,做法很简单:一旦数组元素变成了非活动部分的一部分,程序员就手工清空这些数组元素。
432+
433+
一般而言,**只要类是自己管理内存,程序员就应该警惕内存泄漏问题**。一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。
434+
435+
**内存泄漏的另一个常见来源是缓存**。一旦你把对象引用放到缓存中,它就很容易被遗忘掉,从而使用它不再有用很长一段时间内仍然留在缓存中。对于这个问题,有几种可能的解决方案。如果你正好要实现这样的缓存:只要在缓存之外存在对某个项的键的引用,该项就有意义,那就可以用 WeakHashMap 代表缓存;当缓存中的项过期之后,它们就会自动被删除。记住只有当所要的缓存项的生命周期是由该键的外部引用而不是由键的值决定时候,WeakHashMap 才有用处。
436+
437+
更为常见的情形则是,“缓存项的生命周期是否有意义”并不是很容易确定,随着时间的推移,其中的项的价值变得越来越没有价值。在这种情况下,缓存应该时不时的清空掉无用的项。清除工作可以由一个后台线程(可能是 Timer 或者 ScheduledThreadPoolExecutor)来完成,或者也可以给缓存添加新数据的时候顺便进行清理。LinkedHashMap 利用其 removeEldestEntry 方法可以很容易地实现后一种方案。对于更加复杂的缓存,必须直接使用 java.lang.ref。
438+
439+
**内存泄漏的第三个常见来源是监听器和其它回调**。如果你实现了一个 API,客户端在这个 API 中注册回调,却没有显示地取消注册,那么除非你采取某些动作,否则它们就会聚集。确保回调立即被当作垃圾回收的最佳方法是只保存它们的软引用(`weak reference`),例如,只将它们保存成 WeakHashMap 中的键。
440+
441+
由于内存泄漏通常不会表现成明显的失败,所以它们可以在一个系统中存在很多年。往往只有通过代码检查,或者借助于 Heap 剖析工具(`Heap Profiler`)才可以发现内存泄漏问题。所以,如果我们能在内存泄漏发生之前就知道如何预测和分析此类问题,并预防和阻止它们发生,那是最好不过了。

0 commit comments

Comments
 (0)