WeakHashMap和ThreadLocal内存泄漏中的弱引用运行原理

本文原创,如有引用,请指明出处。

WeakHashMap和ThreadLocal内存泄漏中的弱引用运行原理

WeakHashMap的内存泄漏问题

DefaultChannelPipeline中使用了WeakHashMap来作为缓存。事实上,WeakHashMap的设计理念与ThreadLocal很像。但是ThreadLocal重新设计了自己的实现,并没有直接使用WeakHashMap。同时,ThreadLocal存在着内存泄漏的问题。而网上关于WeakHashMap内存泄漏问题却谈的非常少。下面贴一个网上关于WeakHashMap内存泄漏问题的代码。出处在这里对WeakHashMap的使用不慎导致内存溢出分析

import java.util.WeakHashMap;


public class WeekTest {

    public static class Locker {
        private static WeakHashMap<String, Locker> lockerMap = new WeakHashMap<String, Locker>();
        private final String id;
        private Locker(String id) {
            this.id= id;
        }
        public synchronized static Locker acquire(String id) {
            Locker locker = lockerMap.get(id);
            if (locker == null) {
                locker = new Locker(id);
                //lockerMap.put(id, locker);  //问题代码,导致了entry.key == entry.value.id
                lockerMap.put(new String(id), locker);  //这是一种修改方式,保证了WeakHashMap中的key,没有被value直接或间接所引用
            }
            return locker;
        }
        public String getId() {
            return this.id;
        }
        public static int getSize() {
            return lockerMap.size();
        }
    }


    public static void main(String[] args) {
        for (int i = 0; i < 10000000; i++) {
            Locker.acquire("abc" + i);
            if (i % 10000 == 0) {
                System.gc();
                System.out.println(Locker.getSize());    //输出垃圾回收后的Map的Size
            }
        }

    }
}

读者可以自己运行上述代码。比较结果,很容易发现内存泄漏了。事实上,这里出现了许多疑问。当一个对象仅仅被弱引用指定时,它将被回收。因此,它被用来设计ThreadLocalMap和WeakHashMap。二者原理十分类似。二者内部数据结构十分类似。ThreadLocalMap元数据设计Entry代码如下:

/**
 * The entries in this hash map extend WeakReference, using
 * its main ref field as the key (which is always a
 * ThreadLocal object).  Note that null keys (i.e. entry.get()
 * == null) mean that the key is no longer referenced, so the
 * entry can be expunged from table.  Such entries are referred to
 * as "stale entries" in the code that follows.
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
      Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

下面是WeakHashMap的源代码。这里只比较数据结构,会发现两个内部类十分相似。但是,ThreadLocal会引发内存泄漏,而WeakHashMap很少引发内存泄漏。除非使用错误,WeakHashMap中的key被value直接或间接所引用。在使用正确的情况下,WeakHashMap中的数据在key没有被强引用的情况下,回收器可以正确回收整个Entry的内存;ThreadLocal则必须在当前线程停止后才可以,否则回收器将仅仅回收key(Threshold)内存,value内存无法被回收。

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
    V value;
    final int hash;
    Entry<K,V> next;
      /**
     * Creates new entry.
     */
    Entry(Object key, V value,
          ReferenceQueue<Object> queue,
          int hash, Entry<K,V> next) {
        super(key, queue);
        this.value = value;
        this.hash  = hash;
        this.next  = next;
    }
   
   //其他忽略

弱引用运行原理

为了理解这其中原因,我们需要查看三个类WeakReference,Reference,ReferenceQueue。WeakReference继承了Reference,

public class WeakReference<T> extends Reference<T> {

    /**
     * Creates a new weak reference that refers to the given object.  The new
     * reference is not registered with any queue.
     *
     * @param referent object the new weak reference will refer to
     */
    public WeakReference(T referent) {
        super(referent);
    }

    /**
     * Creates a new weak reference that refers to the given object and is
     * registered with the given queue.
     *
     * @param referent object the new weak reference will refer to
     * @param q the queue with which the reference is to be registered,
     *          or <tt>null</tt> if registration is not required
     */
    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }

}

整个WeakReference的代码十分简单。主要的逻辑由Reference实现。WeakReference仅实现了两个构造器。Reference的实现较晦涩,但原理十分简单。我们将结合例子讲解。如果我们想实现一个字符串变成弱引用。实现如下:

String String = new String("Random"); // 强引用
ReferenceQueue<String> StringQ = new ReferenceQueue<String>();// 引用队列
WeakReference<String> StringWRefernce = new WeakReference<String>(String, StringQ);//弱引用

构造器有两个十分重要的属性。这里构造器第一个参数是new String("Random")。在Reference中,回收器线程将对这个对象进行监视,从而决定是否将Reference放入ReferenceQueue中。而这个ReferenceQueue正是传递给构造器的第二个属性。Reference对象为了自己专门定义了四个内部状态:Active-Pending-Enqueued-Inactive。当它被传进来那一刻,Reference对象的状态为Active。回收器开始运作。当可达性发生变化,Reference对象状态将被转化为Pending(这部分是由JVM实现的)。同时,Reference对象将被加入一个Pending队列(源码中中实现为链表)。Reference类还启动一个守护线程ReferenceHandle。这个线程负责将Pending队列中的Reference对象加入ReferenceQueue队列中。可以说ReferenceQueue中的Reference对象已经是无用对象。ReferenceQueue可以看作是一个Reference链表。同时加入其中的Reference对象已经不可达,自动回收。示例代码如下:(改自Reference Queues


import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;

public class WeekTest {

    public static <T>boolean checkNotNull(T t,String fieldName){
        try {
            Field field=t.getClass().getDeclaredField(fieldName);
            field.setAccessible(true);
            Object result=field.get(t);
            return result!=null;
        } catch (NoSuchFieldException e) {
            System.out.println("不存在该字段");
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            System.out.println("");
            e.printStackTrace();
        }
        return false;
    }


    public static void main(String[] args) throws InterruptedException {


        String String = new String("Random"); // a strong object

        ReferenceQueue<String> StringQ = new ReferenceQueue<String>();// the ReferenceQueue
        WeakReference<String> StringWRefernce = new WeakReference<String>(String, StringQ);

        System.out.println("String created as a weak ref " + StringWRefernce);
        System.out.println("Is refQ head not null?"+checkNotNull(StringQ,"head"));
        Runtime.getRuntime().gc();
        System.out.println("Any weak references in Q ? " + (StringQ.poll() != null));
        String = null; // the only strong reference has been removed. The heap
        // object is now only weakly reachable

        System.out.println("Now to call gc...");
        Runtime.getRuntime().gc(); // the object will be cleared here - finalize will be called.
        System.out.println("Is refQ head not null?"+checkNotNull(StringQ,"head"));
        //这里改用remove()方法是因为poll()是无阻塞方法。一个对象标记不可达到加入ReferenceQueue是分别由GC和线程ReferenceHandler分别来实现的,因此是存在延迟的。而remove()是阻塞方法。
        System.out.println("Any weak references in Q ? " + (StringQ.remove() != null));
        System.out.println("Does the weak reference still hold the heap object ? " + (StringWRefernce.get() != null));
        System.out.println("Is the weak reference added to the ReferenceQ  ? " + (StringWRefernce.isEnqueued()));

    }
}

这里我们回到ThreadLocal中。ThreadLocal中的内部类Entry继承了WeakReference,传递给Reference监视的对象是ThreadLocal。value则是Entry内部的强引用。当外部ThreadLocal被设置为空时,回收器设置Reference为Pending,再加入ReferenceQueue,最后被回收。至始至终,我们传递给JVM监视并准备回收的都是ThreadLocal。Entry中的value并没有得到处理。因此如果线程一直没结束,那么就会存在Thread->ThreadLocalMap->Entry(null,value)的引用,造成内存泄漏。

为什么WeakHashMap能够回收value内存?很简单,WeakHashMap专门实现了一个方法

private void expungeStaleEntries() {
    for (Object x; (x = queue.poll()) != null; ) {
        synchronized (queue) {
            @SuppressWarnings("unchecked")
                Entry<K,V> e = (Entry<K,V>) x;
            int i = indexFor(e.hash, table.length);
              Entry<K,V> prev = table[i];
            Entry<K,V> p = prev;
            while (p != null) {
                Entry<K,V> next = p.next;
                if (p == e) {
                    if (prev == e)
                        table[i] = next;
                    else
                        prev.next = next;
                    // Must not null out e.next;
                    // stale entries may be in use by a HashIterator
                    e.value = null; // Help GC
                    size--;
                    break;
                }
                prev = p;
                p = next;
            }
        }
    }
}

WeakHashMap专门去翻找了ReferenceQueue。当发现了Reference中有某个key,说明它将被回收。那么e.value = null。直接将value置空,这样value也可以进行回收。

以上就是整个弱引用的运行原理。如果有兴趣,可以自己翻看jdk代码,对照本文。如果本文有误,欢迎指正。

补充

最后贴一个ReferenceQueue的使用代码

ReferenceQueue<Foo> fooQueue = new ReferenceQueue<Foo>();

class ReferenceWithCleanup extends WeakReference<Foo> {
  Bar bar;
  ReferenceWithCleanup(Foo foo, Bar bar) {
    super(foo, fooQueue);
    this.bar = bar;
  }
  public void cleanUp() {
    bar.cleanUp();
  }
}

public Thread cleanupThread = new Thread() {
  public void run() {
    while(true) {
      ReferenceWithCleanup ref = (ReferenceWithCleanup)fooQueue.remove();
      ref.cleanUp();
    }
  }
}

public void doStuff() {
  cleanupThread.start();
  Foo foo = new Foo();
  Bar bar = new Bar();
  ReferenceWithCleanup ref = new ReferenceWithCleanup(foo, bar);
  ... // From now on, once you release all non-weak references to foo,
      // then at some indeterminate point in the future, bar.cleanUp() will
      // be run. You can force it by calling ref.enqueue().
}

本文原创,如有引用,请指明出处。

posted @ 2018-06-02 00:47  SourMango  阅读(1787)  评论(0编辑  收藏  举报