Effective.Java 读书笔记(6)内存泄漏

6.Eliminate obsolete object reference

大意为 消除旧的对象引用

当你使用直接操作内存的语言,例如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);
    }
}

看上去好像没有什么明显的问题,测试起来也都可以通过对吧,但是注意到其中潜伏的一个问题,宽泛点说,这个程序有着内存泄漏的风险,这样的风险会导致垃圾回收的压力增大并且加大内存的开销从而降低整个程序性能,最严重的时候可能会产生OutOfMemoryError的错误,但是这样的错误比较少见

那么是那一部分内存泄漏呢,其实就是pop弹出栈操作中stack仍然保留着已经弹出的element的引用,那样垃圾回收机制并不会去回收,并且这样的一个旧的引用并不会被重引用,即使我们的stack没有所有element的引用了,垃圾回收机制也不会去回收,由于stack一直维持着一个旧的引用

内存泄漏在拥有垃圾回收机制(更加适合的说法是,无意的对象保留)的语言里面是十分阴险的,如果一个对象的引用无意间保留了下来,不仅仅这个对象不会被垃圾回收,那些被这个对象所引用的对象也不能被回收,链式效应会使得整个程序的性能极具下降

为了解决这样的一个问题,我们只需要简单地把那些引用置为null就可以了,比如在上述程序中,我们只要这样修改就好

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

只要你重引用这个元素,系统就会抛出一个空指针异常,这对于检测程序异常错误十分常见

当程序员第一次面临这个问题的时候,他们可能会在程序结束使用的时候过度去置空每一个对象的引用,这样是既没有必要又不被期望的,这样会使得代码变得杂乱,置空对象的引用应该视情况而定而不是规范式地照搬,关于消除旧引用的最好的办法就是让这些包含引用的变量越界,这经常在你在一个狭窄的范围内定义一个变量的时候会发生

那么,什么时候我们去置空这些引用呢?简单来说,当你的类中所拥有的变量的引用没有重用的可能并且你的类还继续拥有着这个引用的话,就把它置为空就可以了,如果不置为空,垃圾回收机制并不知道这个引用没有作用了,也就不会去回收了

总而言之,只要当一个类管理它自己的内存,程序员就应该注意一下内存泄漏的风险,当一个元素是free的时候,任何对这个元素的引用都应该被置空

另一个比较常见的可能造成内存泄漏的原因就是缓存了,一旦你把一个对象的引用放到缓存里面,很容易忘记它在缓存那里并且很容易就把它一直放在缓存那里知道它变得完全没有作用了,对于这类问题,有着一些解决方案,如果你足够幸运,实现的缓存的条目都是完全相关的并且只要对于键值存在缓存外部的引用,代表性的缓存例如WeakHashMap,一旦条目变过时了就会自动被移除,记住WeakHashMap,这个类很有用当缓存条目的生存时间取决于外部对于键值的引用,而不是值的引用的时候

更加常见的是,一个缓存条目的有用的生存时间很少被定义的很好,随着时间条目变得越来越没有价值,在这种情况下,缓存应该偶尔清一下那些不用了的条目,这可以利用后台线程来处理(可能是一个Timer 或者是一个ScheduledThreadPoolExecutor )或者添加新的条目的时候就会有这种作用,LinkedHashMap类使用了它的removeEldestEntry方法使得后一种方法更加简便,对于更加复杂的缓存,你可能需要直接的使用 java.lang.ref

第三种常见的内存泄漏的就是监听器和其他的回调,如果你实现了一个API,这个API是当用户注册回调但是并没有明确的解除注册,他们会积累起来除非你采取某些措施,最好的办法来保正这些回调被垃圾回收及时处理就是只储存weak reference(弱引用),对于实例,就利用WeakHashMap储存他们作为键

因为内存泄漏不会特别的明显地显示出来,他们可能在某个系统里面潜藏很久,只有十分仔细的代码或者debug工具(比如heap profiler)的辅助才能发现他们,因此,我们需要学习去在这些问题发生之前预测并且防止他们发生

你可能感兴趣的:(Effective.Java 读书笔记(6)内存泄漏)