WeakHashMap工作原理

基本原理

WeakHashMap特点是,当除了自身有对key的引用外,此key没有其他引用,那么WeakHashMap会在下次对WeakHashMap进行增删改查操作时及时丢弃该键值对,节约内存使用,此特性使得WeakHashMap非常适合构建缓存系统。 
WeakHashMap是主要通过expungeStaleEntries函数的来实现移除其内部不用的entry从而达到的自动释放内存的目的。基本上只要对WeakHashMap的内容进行访问就会调用expungeStaleEntries函数,从而达到清除不再被外部引用的key对应的entry键值对。如果预先生成了WeakHashMap,而在GC以前又不曾访问该WeakHashMap,那么因为没有机会调用expungeStaleEntries函数,因此并不会回收不再被外部引用的key对应的entry。

Entry键值对

WeakHashMap的键值对Entry继承自WeakReference,并实现了Map.Entry

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>
  • 1

当弱引用指向的对象只能通过弱引用(没有强引用或弱引用)访问时,GC会清理掉该对象,之后,引用对象会被放到ReferenceQueue中。在Entry的构造函数中可以得知,通过super(key, queue)将key保存为弱引用,通过this.value = value将value保存为强引用。当key中的引用被gc掉之后,在下次访问WeakHashMap(调用expungeStaleEntries函数)时相应的entry也会自动被移除。

        /**
         * 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;
        } 
  

expungeStaleEntries():清除过期的条目

从ReferenceQueue中取出过期的entry,从WeakHashMap找到对应的entry,逐一删除。

expungeStaleEntries()清空table中无用键值对,原理如下:
1、当WeakHashMap中某个“弱引用的key”由于没有再被引用而被GC收回时,被回收的“该弱引用key”也被会被添加到"ReferenceQueue(queue)"中。
2、当我们执行expungeStaleEntries时,就遍历"ReferenceQueue(queue)"中的所有key后就在“WeakReference的table”中删除与“ReferenceQueue(queue)中key”对应的键值对

     /**
     * 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;
                }
            }
        }
    }

首先通过循环一直从queue中取过期entry直到取完为止

    for (Object x; (x = queue.poll()) != null; )

然后通过加锁queue进行删除过期entry的操作

    synchronized (queue) {
    ...
    }

在同步代码块中先把从queue中取出的Object类型的数据强制转化为Entry对象e,然后计算此entry在桶的位置(table数组的下标i),然后开始遍历entry链表,如果此entry是链表头,设置此entry的后继为新的链表头

              if (prev == e)
                  table[i] = next;

否则将此entry的前序节点的后继指针指向此entry的后继节点

               else
                   prev.next = next;

最后设置被删除的entry的value为null,加速垃圾回收,接着修改size

               e.value = null; // Help GC
               size--;

访问WeakHashMap时,对过期条目进行清除

WeakHashMap工作原理_第1张图片 
WeakHashMap工作原理_第2张图片 
可以看到对WeakHashMap的增删改查操作都会直接或者间接的调用expungeStaleEntries()方法,达到及时清除过期entry的目的。 

此特性会到导致两次调用size、get等方法可能会返回不一致的数据。

转载自:jdk源码分析之WeakHashMap

==================================

在tomcat的源码里,实现缓存时会用到WeakHashMap。

package org.apache.tomcat.util.collections;

import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;

public final class ConcurrentCache {

    private final int size;

    private final Map eden;

    private final Map longterm;

    public ConcurrentCache(int size) {
        this.size = size;
        this.eden = new ConcurrentHashMap<>(size);
        this.longterm = new WeakHashMap<>(size);
    }

    public V get(K k) {
        V v = this.eden.get(k);
        if (v == null) {
            synchronized (longterm) {
                v = this.longterm.get(k);
            }
            if (v != null) {
                this.eden.put(k, v);
            }
        }
        return v;
    }

    public void put(K k, V v) {
        if (this.eden.size() >= size) {
            synchronized (longterm) {
                this.longterm.putAll(this.eden);
            }
            this.eden.clear();
        }
        this.eden.put(k, v);
    }
}

  源码中有eden和longterm的两个map,对jvm堆区有所了解的话,可以猜测出tomcat在这里是使用ConcurrentHashMap和WeakHashMap做了分代的缓存。在put方法里,在插入一个k-v时,先检查eden缓存的容量是不是超了。没有超就直接放入eden缓存,如果超了则锁定longterm将eden中所有的k-v都放入longterm。再将eden清空并插入k-v。在get方法中,也是优先从eden中找对应的v,如果没有则进入longterm缓存中查找,找到后就加入eden缓存并返回。 
  经过这样的设计,相对常用的对象都能在eden缓存中找到,不常用(有可能被销毁的对象)的则进入longterm缓存。而longterm的key的实际对象没有其他引用指向它时,gc就会自动回收heap中该弱引用指向的实际对象,弱引用进入引用队列。longterm调用expungeStaleEntries()方法,遍历引用队列中的弱引用,并清除对应的Entry,不会造成内存空间的浪费。

参考:WeakHashMap的使用场景

===============================

今天一起来看下java.util包里的WeakHashMap工具类。

WeakHashMap的定义如下:

1
2
3
public class WeakHashMap
extends AbstractMap
implements Map

简单来说,WeakHashMap实现了Map接口,基于hash-table实现,在这种Map中,key的类型是WeakReference。如果对应的key被回收,则这个key指向的对象会被从Map容器中移除。

WeakHashMap跟普通的HashMap不同,WeakHashMap的行为一定程度上基于垃圾收集器的行为,因此一些Map数据结构对应的常识在WeakHashMap上会失效——size()方法的返回值会随着程序的运行变小,isEmpty()方法的返回值会从false变成true等等。

强引用、软引用和弱引用

“引用”,在Java中指的是一个对象对另一对象的使用(指向)。WeakHashMap中的键的类型是WeakReference,在Java中还有另外两种引用:强引用(Strong Reference)、软引用(Soft Reference)。

强引用

被强引用指向的对象,绝对不会被垃圾收集器回收。Integer prime = 1;,这个语句中prime对象就有一个强引用。

软引用

被SoftReference指向的对象可能会被垃圾收集器回收,但是只有在JVM内存不够的情况下才会回收;如下代码可以创建一个软引用:

1
2
3
Integer prime = 1
SoftReference soft = new SoftReference(prime);
prime = null ;

弱引用

当一个对象仅仅被WeakReference引用时,在下个垃圾收集周期时候该对象就会被回收。我们通过下面代码创建一个WeakReference

1
2
3
Integer prime = 1
WeakReference soft = new WeakReference(prime);
prime = null ;

当把prime赋值为null的时候,原prime对象会在下一个垃圾收集周期中被回收,因为已经没有强引用指向它。

利用WeakHashMap实现内存缓存

可以看出,WeakHashMap的这种特性比较适合实现类似本地、堆内缓存的存储机制——缓存的失效依赖于GC收集器的行为。假设一种应用场景:我们需要保存一批大的图片对象,其中values是图片的内容,key是图片的名字,这里我们需要选择一种合适的容器保存这些对象。

使用普通的HashMap并不是好的选择,这些大对象将会占用很多内存,并且还不会被GC回收,除非我们在对应的key废弃之前主动remove掉这些元素。WeakHashMap非常适合使用在这种场景下,下面的代码演示了具体的实现:

1
2
3
4
5
6
7
8
9
10
11
WeakHashMap map = new WeakHashMap<>();
BigImage bigImage = new BigImage( "image_id" );
UniqueImageName imageName = new UniqueImageName( "name_of_big_image" ); //强引用
 
map.put(imageName, bigImage);
assertTrue(map.containsKey(imageName));
 
imageName = null ; //map中的values对象成为弱引用对象
System.gc(); //主动触发一次GC
 
await().atMost( 10 , TimeUnit.SECONDS).until(map::isEmpty);

首先,创建一个WeakHashMap对象来存储BigImage实例,对应的key是UniqueImageName对象,保存到WeakHashMap里的时候,key是一个弱引用类型。

然后,我们将imageName设置为null,这样就没有其他强引用指向bigImage对象,按照WeakHashMap的规则,在下一次GC周期中会回收bigImage对象。

通过System.gc()主动触发一次GC过程,然后可以发现WeakHashMap成为空的了。

总结

本文从WeakHashMap的定义讲起,又通过介绍Java中的三种引用类型来理解WeakHashMap的工作原理,最后利用一个存储大对象的例子演示了WeakHashMap的应用场景。本文中提到的代码,可以在GitHub工程看到。

翻译自: Guide to WeakHashMap in Java

原文出处:  阿杜

你可能感兴趣的:(Java)