[关闭]
@XingdingCAO 2017-11-16T13:07:58.000000Z 字数 3306 阅读 1655

Effective Java(第2版):item 6 —— Eliminate obsolete object reference.

reference Java


前言

在从像C、C++等这样手动回收内存的语言,转到Java这样的自动回收内存的语言时,我们可能会感叹编程语言的人性化发展。但是,Java的GC(Garbage Collection)机制也还没有足够智能到让我们忘记内存管理这回事。

一个例子

  1. public class Stack{
  2. private static int DEFAULT_INITIAL_CAPACITY=16;
  3. private Object[] elements;
  4. prvate int size=0;
  5. public Stack(){elements=new Object[DEFAULT_INITIAL_CAPACITY];
  6. public void push(Object obj){
  7. ensureCapacity();
  8. elements[size++]=obj;
  9. }
  10. public Object pop{
  11. if(size==0)
  12. throw new StackEmptyException();
  13. return elements[--size];
  14. }
  15. private void ensureCapacity(){
  16. if(size==elements.length)
  17. elements=Array.copyOf(elements,2*size+1);
  18. }
  19. }

解决方法

将过时的引用置空

上面的例子仅需要将数组中无用的引用置空即可使GC正确回收内存,解决内存泄露问题。这也是解决Java中内存泄露的方法——将过时的引用置空。

  1. public Object pop(){
  2. if(size==0)
  3. throw new StackEmptyException();
  4. Object ret = elements[size-1];
  5. elements[--size] = null;//消除过时的引用,将引用置空
  6. return ret;
  7. }

额外的好处

将引用置空除了可以释放不必占用着的内存,还会在错误访问应被弃置的对象时抛出NullPointerException,保证了程序的稳定性与正确性。
例如:在上例中Stack类内方法假如错误地访问已经被pop出去的对象时,就会抛出异常。

何时将引用置空?

常态

消除过时的引用除了将引用置空,还可以通过指定变量的作用域来实现。变量在结束其生命周期后便被回收,整个回收过程都由GC完成,无需我们插手。因此。我们应该仔细考量变量的作用域,将其放置在最小的作用域

例外

  1. 必须置空引用的情形通常发生在自我维系内存池的程序中,例如:上例中Stack类自己维系着若干个Object对象的引用,极易造成无意的内存泄露。对于此种情况,程序编写者必须时刻提醒自己,在对象没有用处时,及时置空,防止内存泄漏。

  2. 还需要注意的就是在对Cache(缓存)的使用时,我们时常在无需使用缓存时忘记释放对应内存。

    • 例如:将一个图片的名称作为key(键),比特流作为value(值),存入一个HashMap中作为缓存。但是,这样做最大的问题就是,图片比特流通常占用着大量的内存,而HashMap不会自动释放一对keyvalue,当我们进行多项操作后忘却内存的释放时,悲剧就发生了。(悲惨的教训——永远不要相信使用者,尽量交给代码去完成)
      解决方法:
    • 使用WeakHaspMap这类使用了Weak Reference的类,作为缓存的存储容器。
      Weak Reference在这里就是指:在将key置空后,下一轮GC操作就会回收这个key对应的“键-值对”。

      有关WeakHaspMap的详细情况可参考:http://www.baeldung.com/java-weakhashmap

      1. String key = "img_name";
      2. String value = "img_bits";
      3. Map<String,String> cache = new WeakHashMap<>();
      4. cache.put(key,value);
      5. //JUnit中的方法 —— assertTrue()
      6. assertTrue(map.containsKey(key));//无输出,说明包含key
      7. key = null;
      8. System.gc();
      9. //阻塞直到内存被GC回收
      10. await().atMost(10, TimeUnit.SECONDS).until(() -> map.size() == 0);
      11. assertTrue(map.containsKey(key));//有输出,说明不包含key
    • 使用LinkedHaspMap这类自动回收内存的类作为缓存的容器。
      关于自动回收内存可以通过后台线程,如TimerScheduledThreadPoolExcutor;还可以在添加新的“键-值对”时进行。

      1. //LinkedHaspMap源码 JDK8
      2. void afterNodeInsertion(boolean evict) { // possibly remove eldest
      3. LinkedHashMap.Entry<K,V> first;
      4. if (evict && (first = head) != null && removeEldestEntry(first)) {
      5. K key = first.key;
      6. removeNode(hash(key), key, null, false, true);
      7. }
      8. }
  3. 此外,另外一个常见的情况就是,为Listerner设置Callback
    当你继承某个API,为某个Listener注册了Callback回调,但是却没有释放。这样会使Callback的内存占用逐渐提升,造成内存泄漏。
    解决方法:使用Weak Reference的类来存储Callback,如WeakHaspMap

后语

内存泄漏并不能被显式地检测到,仅能依靠编码者的代码走查,然后配合工具检测是否发生了内存泄漏的现象。可悲的是,现在运行的程序就在发生着内存泄漏,却无法被用户感知,知道内存被挤爆。所以,我们应该培养足够的编程素养,尽量防止这种问题的发生。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注