WeakReference和WeakHashMap

今天听人分享了关于threadLocal的内存泄露知识
ref:http://blog.csdn.net/wudiyong22/article/details/52141608 深入分析 ThreadLocal 内存泄漏问题
ref:话说ReferenceQueue 前半部分不错,后半部分也是模模糊糊,不看也罢
ref:Java引用类型原理剖析 这个是我目前查到唯一讲到referent到底怎么被置为null的文章
ref:gc过程中reference对象的处理

一、前置知识

根源在于内存是宝贵的,因此我们需要合理使用弱引用。由于java的GC策略帮我们已经完成了很多优化,大部分情况下,我们自己无需使用弱引用,但还是要理解这个概念

1.1 弱引用类 WeakReference

想看最基本的使用方法

productA = new Product(...);
WeakReference weakProductA = new WeakReference<>(productA);

但基本没见过直接使用的,Java类库为我们提供了WeakHashMap类。
weakhashmap首先是一个hashmap,外层是一个Entry[k,v] table,table[i]是一个链表,每一个entry指向next entry
那么它相比于普通的HashMap区别在哪?

最特殊的就是Map的这个 Entry是继承自WeakRenferenced的,super(key,queue)

此处最难以理解,容我自己理理,我也被绕了很久

 private static class Entry extends WeakReference implements Map.Entry {
        V value;
        final int hash;
        Entry next;

        /**
         * Creates new entry.
         */
        Entry(Object key, V value,
              ReferenceQueue queue,
              int hash, Entry next) {
            super(key, queue);
            this.value = value;
            this.hash  = hash;
            this.next  = next;
        }


  • WeakReference这个基类到底有什么特性呢?
public class WeakReference extends Reference {
    public WeakReference(T referent) {
        super(referent);
    }
    public WeakReference(T referent, ReferenceQueue q) {
        super(referent, q);
    }
}

其实就简单的有一个引用队列ReferenceQueue,有一个引用referent。

但我一开始困惑的原因就是,我想当然认为entry是WeakReference的子类,所以这个引用是Entry

!错了

Entry显式调用了super(key, queue);
Object Key才是整整的referent

1.2 引用类 Reference

继续看WeakReference
先看看ReferenceQueue在Java中的描述:

Reference queues, to which registered reference objects are 
appended by the garbage collector after the appropriate reachability changes are detected. 
中文JavaDoc的描述:引用队列,在检测到适当的可到达性更改后,垃圾回收器将已注册的引用对象添加到该队列中

查看源代码会发现它很简单,实现了一个队列的入队(enqueue)和出队(poll还有remove)操作,内部元素就是泛型的Reference,并且Queue的实现,是由Reference自身的链表结构所实现的。

敲黑板,前文提到的注册queue的意义就在这里

这个queue是通过构造函数传入的,表示创建一个Reference时,要将其注册到那个queue上。

Queue的另一个作用是可以区分不同状态的Reference。Reference有4种状态,不同状态的reference其queue也不同:

Active:
 queue = ReferenceQueue with which instance is registered,
 or ReferenceQueue.NULL if it was not registered with a queue; next = null.

Pending:
 queue = ReferenceQueue with which instance is registered;
 next = Following instance in queue, or this if at end of list.

Enqueued:
 queue = ReferenceQueue.ENQUEUED; next = Following instance 
 in queue, or this if at end of list.

Inactive:
 queue = ReferenceQueue.NULL; next = this.

1)pending和 discovered成员:都和GC有关

先看pending对象

/* List of References waiting to be enqueued.  The collector adds
 * References to this list, while the Reference-handler thread removes
 * them.  This list is protected by the above lock object.
 */
private static Reference pending = null;

//这个对象,定义为private,并且全局没有任何给它赋值的地方,
//根据它上面的注释,我们了解到这个变量是和垃圾回收期打交道的。
再看discovered,同样为private,上下文也没有任何地方使用它

transient private Reference discovered;    /* used by VM */

//看到了它的注释也明确写着是给VM用的。
上面两个变量对应在VM中的调用,可以参考openjdk中的hotspot源码,在hotspot/src/share/vm/memory/referenceProcessor.cpp 的ReferenceProcessor::discover_reference 方法。(根据此方法的注释由了解到虚拟机在对Reference的处理有ReferenceBasedDiscovery和RefeferentBasedDiscovery两种策略)

2)ReferenceHandler线程

这个线程在Reference类的static构造块中启动,并且被设置为高优先级和daemon状态。此线程要做的事情,是不断的检查pending 是否为null,如果pending不为null,则将pending进行enqueue,否则线程进入wait状态。

通过这2点,我们来看整个过程:

! pending内对象的放入是由jvm 的 GC 来完成

1.垃圾收集会对各类引用进行清理,具体可以参考Java引用类型原理剖析
这里就讲WeakHashMap关联的wearReference发生了什么
2.ReferenceHandler线程要做的是将pending对象enqueue,但默认我们所提供的queue,也就是从构造函数传入的是null,实际是使用了ReferenceQueue.NULL,Handler线程判断queue为ReferenceQueue.NULL则不进行操作,只有非ReferenceQueue.NULL的queue才会将Reference进行enqueue。
ReferenceQueue.NULL相当于我们提供了一个空的Queue去监听垃圾回收器给我们的反馈,并且对这种反馈不做任何处理。要处理反馈,则必须要提供一个非ReferenceQueue.NULL的queue。

1.3 到此我们来看Young GC的时候,到底发生了什么?

主要分为Native层和Java层两个部分。

  • Native层在GC时将需要被回收的Reference对象加入到DiscoveredList中(代码在referenceProcessor.cppprocess_discovered_references方法),然后将DiscoveredList的元素移动到PendingList中(代码在referenceProcessor.cppenqueue_discovered_ref_helper方法),PendingList的队首就是Reference类中的pending对象
  • 敲黑板,重点java中ReferenceQueue的poll方法
    Java层流程比较简单:就是源源不断的从PendingList中提取出元素,然后将其加入到ReferenceQueue中去,开发者可以通过从ReferenceQueue中poll元素感知到对象被回收的事件。
/**
     * Polls this queue to see if a reference object is available.  If one is
     * available without further delay then it is removed from the queue and
     * returned.  Otherwise this method immediately returns null.
     *
     * @return  A reference object, if one was immediately available,
     *          otherwise null
     */
    public Reference poll() {
        if (head == null)
            return null;
        synchronized (lock) {
            return reallyPoll();
        }
    }

可以看到,当你可达时,会从queue中remove并返回,但如果不可达,会返回null,后面会用到
总流程
reference-JVM->pending-JAVA->referenceQueue

以WeakReference进行分析
1.GC refs_lists中存放了本次GC发现的某种引用类型(虚引用、软引用、弱引用等)
2.process_discovered_reflist方法的作用就是将不需要被回收的对象从refs_lists移除掉,refs_lists最后剩下的元素全是需要被回收的元素,最后会将其第一个元素赋值给上文提到过的Reference.java#pending字段。
3.WeakReference在Java层只是继承了Reference,没有做任何的改动。那referent字段是什么时候被置为null的呢?要搞清楚这个问题我们再看下上文提到过的process_discovered_reflist方法:

 // Phase 3:
  // 根据clear_referent的值决定是否将不存活对象回收
  if (mt_processing) {
    RefProcPhase3Task phase3(*this, refs_lists, clear_referent, true /*marks_oops_alive*/);
    task_executor->execute(phase3);
  } else {
    for (uint i = 0; i < _max_num_q; i++) {
      process_phase3(refs_lists[i], clear_referent,
                     is_alive, keep_alive, complete_gc);
    }
  }

  return total_list_count;
}

void
ReferenceProcessor::process_phase3(DiscoveredList&    refs_list,
                                   bool               clear_referent,
                                   BoolObjectClosure* is_alive,
                                   OopClosure*        keep_alive,
                                   VoidClosure*       complete_gc) {
  ResourceMark rm;
  DiscoveredListIterator iter(refs_list, keep_alive, is_alive);
  while (iter.has_next()) {
    iter.update_discovered();
    iter.load_ptrs(DEBUG_ONLY(false /* allow_null_referent */));
接下去是重点,看到没有,是把referent变为了null!!!总算可以确定了
clear_referent这个值,各种不同引用是不同的
    if (clear_referent) {
      // NULL out referent pointer
      //将Reference的referent字段置为null,之后会被GC回收
      iter.clear_referent();
    } else {
      // keep the referent around
      //标记引用的对象为存活,该对象在这次GC将不会被回收
      iter.make_referent_alive();
    }
    ...
  }
    ...
}
二、WeakHashMap是如何实现的?

有了以上的知识,让我们从头到晚重新理一下WeakHashMap到底如何实现的

2.1 WeakHashMap.put 一个对象

1.首先 weakHashMap有一个属性 ReferenceQueue,用处上文已经讲过,它的目的是最终用于new Entry时作为queue

tab[i] = new Entry<>(k, value, queue, h, e);
 Entry(Object key, V value,
              ReferenceQueue queue,
              int hash, Entry next) {
            super(key, queue);
            this.value = value;
            this.hash  = hash;
            this.next  = next;
        }

这里的k就是Object key,会作为referent注册到queue中

上文已经分析过,wakeReference会清除其中的referent,那么entry本质上是weakreference,它也有ReferenceQueue,它也会清除其中的referent,这里的referent是什么,就是key,所以weakhashmap的entry的key会被清除

2.2 JVM发生GC

JVM发生GC,对所有的WeakReference进行分析,那么Entry会被回收,按基础知识中的逻辑,我们知道GC会把WeakReference的referent置为null,由于Entry的构造函数调用了super,所以GC把Entry的referent也就是key置为null

注意!然后把Entry放到了reference 的pending中,因为Entry才是真正的WeakReference啊

到了这步位置,其实说穿了,只是把key清除了

2.3 Java的ReferenceHandler

Java的ReferenceHandler将pending enqueue到了referenceQueue中

2.4 OK,接下去就是把Entry当中的Value回收部分了,这部分比较简单好理解了

  • WeakHashMap的expungeStaleEntries方法
此方法会在3个地方进行调用
getTable(),这个getTable基本上会在所有的get,put等方法中进行调用
resize()
size()

顾名思义,这个方法就是删除陈旧的数据。前面ReferenceQueue中提到了,对象不可达时,poll方法会返回null,但这返回null只是返回,真正目前虚拟机中,只有key为显式置为null了,后面还要把value显示置为null
NICE,总算全部通了!

 /**
     * Expunges stale entries from the table.
     */
    private void expungeStaleEntries() {
        for (Object x; (x = queue.poll()) != null; ) {
            synchronized (queue) {
                @SuppressWarnings("unchecked")
                    Entry e = (Entry) x;
                int i = indexFor(e.hash, table.length);

                Entry prev = table[i];
                Entry p = prev;
                while (p != null) {
                    Entry 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;
                }
            }
        }
    }

你可能感兴趣的:(WeakReference和WeakHashMap)