【第7条】消除过期的对象引用

消除过期的对象引用

 

当你从手工管理内存的语言(比如C或C++)转换到具有垃圾收集功能的比如Java语言时,程序员的工作会变得更加容易,因为当你用完了对象之后,它们会被自动回收。当你第一次经历对象回收功能的时候,会觉得这简直有点不可思议。它很容易给你留下这样的印象,认为自己不再需要考虑内存管理的事情了,其实不然。

考虑以下简单的栈实现:

// Can you spot the "memory leak"? public class Stack {   private Object[] elements;   private int size = 0;   private static final int DEFAULT_INITIAL_CAPACITY = 16;   public Stack() {     elements = new Object[DEFAULT_INITIAL_CAPACITY];   }  public void push(Object e) {     ensureCapacity();     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);  } }

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

 

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

 

垃圾收集语言中的内存泄漏(更适当地称为无意的对象保留 unintentional object retentions)是隐蔽的。如果无意中保留了对象引用,那么不仅这个对象排除在垃圾回收之外,而且该对象引用的任何对象也是如此。即使只有少数对象引用被无意地保留下来,也可以阻止垃圾回收机制对许多对象的回收,这对性能产生很大的影响。

 

这类问题的解决方法很简单:一旦对象引用过期,将它们设置为 null。在我们的 Stack 类的情景下,只要从栈中弹出,元素的引用就设置为过期。pop 方法的修正版本如下所示:

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

取消过期引用的另一个好处是,如果它们随后被错误地引用,程序立即抛出NullPointerException 异常,而不是悄悄地做继续做错误的事情。尽可能快地发现程序中的错误是有好处的。

 

当程序员第一次被这个问题困扰时,他们可能会在程序结束后立即清空所有对象引用。这既不是必要的,也不是可取的;它不必要地搞乱了程序。清空对象引用应该是一种例外,而不是一种规范行为。消除过期引用的最好方法是让包含引用的变量超出范围。如果在最近的作用域范围内定义每个变量 (详见第 57 条),这种自然就会出现这种情况。

 

那么什么时候应该清空一个引用呢?Stack 类的哪个方面使它容易受到内存泄漏的影响?简单地说,它管理自己的内存。存储池(storage pool)由 elements 数组的元素组成(对象引用单元,而不是对象本身)。数组中活动部分的元素 (如前面定义的) 被分配,其余的元素都是空闲的。垃圾收集器没有办法知道这些;对于垃圾收集器来说, elements 数组中的所有对象引用都同样有效。只有程序员知道数组的非活动部分不重要。程序员可以向垃圾收集器传达这样一个事实,一旦数组中的元素变成非活动的一部分,就可以手动清空这些元素的引用。

 

一般来说,当一个类自己管理内存时,程序员应该警惕内存泄漏问题。每当一个元素被释放时,元素中包含的任何对象引用都应该被清除。

 

内存泄的另一个常见来源是存。一且你把对象引用放到缓存中,它就很容易被遗忘掉,从而使得它不再有用之后很长一段时间内仍然留在缓存中。对于这个问题,有几种可能的解决方案。如果你正好要实现这样的缓存:只要在缓存之外存在对某个项的键的引用该项就有意义,那么就可以用 WeakHashMap 代表缓存;当缓存中的项过期之后,它们就会自动被删除。记住只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap 才有用处。

 

更常见的情况是,缓存项有用的生命周期不太明确,随着时间的推移一些项变得越来越没有价值。在这种情况下,缓存应该偶尔清理掉已经废弃的项。这可以通过一个后台线程 (也许是ScheduledThreadPoolExecutor ) 或将新的项添加到缓存时顺便清理。LinkedHashMap 类使用它的removeEldestEntry 方法实现了后一种方案。对于更复杂的缓存,可能直接需要使用java.lang.ref 。

内存泄漏的第三个常见来源是监听器和其他回调。如果你实现了一个API,客户端在这个API中注册回调,却没有显式地取消注册,那么除非你采取某些动作,否则它们就会不断地堆积起来。确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用(weak reference),例如,只将它们保存成 WeakHashMap 中的键。

 

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

 

                                                                                   扫描二维码、获取更多内容

                                                             

感谢支持

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值