码迷,mamicode.com
首页 > 其他好文 > 详细

WeakHashMap的Weakness

时间:2021-04-26 12:58:46      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:一个   nal   rar   exception   RoCE   col   guid   rom   循环调用   

我们在日常工作中,对于一些没有固定销毁时间点的对象,通常会考虑用WeakHashMap 来协助自动销毁对象。

举个例子,根据不同的request出错信息的key,自动找到相关的翻译内容。就是常说的国际化,I18n。软件出错的信息是不固定的,如果每次load完内容,就销毁又性能不高。所以通常想法是做个map,但是map的情况下,我没有办法决定什么时候销毁这个key,那么这个时候通常会用WeakHashMap。利用GC 来帮我保证map的内容不会膨胀,导致内存泄漏。

WeakHashMap的行为取决于垃圾回收器的动作。由于垃圾回收器是由jvm调度的,gc可以发生在WeakHashMap对象生命周期的任何时候,所以WeakHashMap的表现为,

  • 即使对 WeakHashMap 实例进行同步,并且没有调用任何赋值方法,在一段时间后 size 方法也可能返回较小的值。
  • 对于 isEmpty 方法连续调用两次,可能先返回true,然后返回true,
  • 对于给定的key,containsKey方法返回true,然后返回false。
  • 对于给定的key ,get方法调用两次, 可能第一次返回一个值,但接着返回null。

总而言之就是只要在垃圾回收器清除某个键的弱引用之后,该键才会自动移除。所以如果用WeakHashMap自己要有这方面的判断。比如上例,如果拿不到内容需要重新加载再放到WeakHashMap。

 

关于引用 

如果一个对象具有弱引用,在GC线程扫描内存区域的过程中,不管当前内存空间足够与否,都会回收内存,使用弱引用 构建非敏感数据的缓存。声明如下:

 

//WeakReference
WeakReference<Object> wf = new WeakReference<~>(new Object());

 

WeakHashMap与GC

WeakHashMap中Entry[]有着Entry->WeakReference->Reference这样一个继承结构,我们先来看下Reference

 

技术图片

字段
  • referent: 引用指向的对象
  • queue: ReferenceQueue,Reference构造的时候传入,其内部封装了单向链表的添加,删除和遍历等操作。用于Reference状态监听及管理
  • discovered:单向链表,由JVM维护
  • next:指向ReferenceQueue中下一个元素,ReferenceQueue链表指针
  • pending:discovered链表表头,在referent被回收后的reference
    将有JVM标记,等待入队处理
方法
  • static代码块:构造ReferenceHandler线程,循环执行tryHandlePending方法
  • tryHandlePending:循环处理pending链表头,维护discovered链表,如果pending不为空,则进行插入ReferenceQueue进行后续操作。(这里如果是cleaner,则先进行clean操作)
内部类
  • ReferenceHandler,线程实现类,run方法中循环调用Refreence的静态方法tryHandlePending
private static Lock lock = new Lock();
/* 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. The
 * list uses the discovered field to link its elements.
 */
private static Reference<Object> pending = null;
/* High-priority thread to enqueue pending References
 */
private static class ReferenceHandler extends Thread {
    private static void ensureClassInitialized(Class<?> clazz) {
        try {
            Class.forName(clazz.getName(), true, clazz.getClassLoader());
        } catch (ClassNotFoundException e) {
            throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
        }
    }
    static {
        // pre-load and initialize InterruptedException and Cleaner classes
        // so that we don‘t get into trouble later in the run loop if there‘s
        // memory shortage while loading/initializing them lazily.
        ensureClassInitialized(InterruptedException.class);
        ensureClassInitialized(Cleaner.class);
    }
    ReferenceHandler(ThreadGroup g, String name) {
        super(g, name);
    }
    public void run() {
        while (true) {
            tryHandlePending(true);
        }
    }
}

 

上面是Reference类的部分代码,可以看到Reference中有一个全局的锁对象:lock;有一个静态变量pending;在静态代码块中启动一个ReferenceHandler线程,启动完成后处于wait状态,它在一个Lock同步锁模块中等待。那么WeakHashMap中key/value如何自动回收跟这些有什么关系呢。

 我们假设JVM使用cms收集器(使用其他收集器对于弱引用的回收原理相同)。

  • JVM 在进行CMS GC的时候,会创建一个ConcurrentMarkSweepThread(简称CMST)线程去进行GC,ConcurrentMarkSweepThread线程被创建的同时会创建一个SurrogateLockerThread(简称SLT)线程并且启动它。
  • SLT启动之后,处于等待阶段。CMST开始GC时,会发一个消息给SLT让它去获取Java层Reference对象的全局锁:Lock。
  • 直到CMS GC完毕之后,JVM 会将WeakHashMap中所有被回收的对象所属的WeakReference容器对象放入到Reference 的pending属性当中(每次GC完毕之后,pending属性基本上都不会为null了)。
  • 然后通知SLT释放并且notify全局锁:Lock。此时激活了ReferenceHandler线程的run方法,使其脱离wait状态,开始工作了。
  • ReferenceHandler这个线程会将pending中的所有WeakReference对象都移动到它们各自的列队当中,比如当前这个WeakReference属于某个WeakHashMap对象,那么它就会被放入相应的ReferenceQueue列队里面(该列队是链表结构)。
  • 当我们下次从WeakHashMap对象里面get、put数据或者调用size方法的时候,WeakHashMap就会将ReferenceQueue列队中的WeakReference一一poll出来去和Entry[]数据做比较,如果发现相同的,则说明这个Entry所保存的对象已经被GC掉了,那么将Entry[]内的Entry对象剔除掉,这样就完成的key/value的自动回收。

 技术图片

 

这是WeakHashMap中的ReferenceQueue定义,注释就可以知道这是用来清除WeakEntries的

/**
    * Expunges stale entries from the table.
    */
   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;
               }
           }
       }
   }

大家可以看一段GC 的log,看看reference 是怎么回收的。

继续观察日志,可以发现,总共150ms的执行过程中,仅Ref Proc一步就花费了108ms。那么可以判断,多出来的时间,应该是和Ref Proc 有关了。Ref Proc这一步就来处理这些引用对象。默认是由单线程执行,如果这一步花费的时间较长,可以通过加参数-XX:+ParallelRefProcEnabled改为多线程处理。

技术图片

 

 

 

WeakHashMap的Weakness

使用WeakHashMap一般是全局变量,我们直接的想法就是把它应用到多线程中去。观察WeakHashMap源码可以发现,它是线程不安全的

 1 /**
 2     * Expunges stale entries from the table.
 3     */
 4    private void expungeStaleEntries() {
 5        for (Object x; (x = queue.poll()) != null; ) {
 6            synchronized (queue) {
 7                @SuppressWarnings("unchecked")
 8                    Entry<K,V> e = (Entry<K,V>) x;
 9                int i = indexFor(e.hash, table.length);
10                Entry<K,V> prev = table[i];
11                Entry<K,V> p = prev;
12                while (p != null) {
13                    Entry<K,V> next = p.next;
14                    if (p == e) {
15                        if (prev == e)
16                            table[i] = next;
17                        else
18                            prev.next = next;
19                        // Must not null out e.next;
20                        // stale entries may be in use by a HashIterator
21                        e.value = null; // Help GC
22                        size--;
23                        break;
24                    }
25                    prev = p;
26                    p = next;
27                }
28            }
29        }
30    }

 

 

 

 

下面指针交换过程,有可能把另外一个线程修改的指针改到当前线程指针, 导致Table的数组不正确。从而引起无限循环。

prev = p;
p = next;

 

这段代码当线程1 

pre =p 

p =next 时 

外一个线程可能

pre=p(此时 p 可能是线程1的 p.pre)

当线程1 p=next 付完值后, 线程2 又开始赋值 p=p.pre.next  所以p=p 就造成指针闭环。当你去调用get 方法是,由于有 while 条件和e =e.next就导致无限循环。

     */
    public V get(Object key) {
        Object k = maskNull(key);
        int h = hash(k);
        Entry<K,V>[] tab = getTable();
        int index = indexFor(h, tab.length);
        Entry<K,V> e = tab[index];
        while (e != null) {
            if (e.hash == h && eq(k, e.get()))
                return e.value;
            e = e.next;
        }
        return null;
    }

  

所以当我们使用WeakHashMap做temporary cache时, 通常都要如下写法

WeakHashMap<String, String> weakHashMapintsmaze=new WeakHashMap<String, String>();
Map<String, String> intsmaze=Collections.synchronizedMap(weakHashMapintsmaze)

  

 

WeakHashMap的Weakness

标签:一个   nal   rar   exception   RoCE   col   guid   rom   循环调用   

原文地址:https://www.cnblogs.com/developernotes/p/14696624.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!