WeakHashMap源码分析

WeakHashMap

WeakHashMap介绍

《WeakHashMap源码分析》 image.png

java.lang.Object
   ↳     java.util.AbstractMap<K, V>
         ↳     java.util.WeakHashMap<K, V>

public class WeakHashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V> {}

WeakHashMap继承于AbstractMap,实现了Map接口。和HashMap一样,WeakHashMap也是一个散列表,它存储的内容也是键值对(key-value)映射,而且键和值都可以是null。

不过WeakHashMap的键是“弱键”。在WeakHashMap中,当某个键不再正常使用时,会从WeakHashMap中被自动移除。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的回收,这就使该键成为可终止的,被终止,然后被回收。某个键被终止时,它对应的键值对也就从映射中有效地移除了。

和HashMap一样,WeakHashMap是不同步的。可以使用 Collections.synchronizedMap方法来构造同步的WeakHashMap。

WeakHashMap的构造函数

  • 默认构造函数,使用默认初始化容量:16,默认负载因子:0.75f

    WeakHashMap()

  • 指定“容量大小”的构造函数,使用默认负载因子:0.75f

    WeakHashMap(int capacity)

  • 指定“容量大小”和“负载因子”的构造函数

    WeakHashMap(int capacity, float loadFactor)

  • 包含“子Map”的构造函数

    WeakHashMap(Map<? extends K, ? extends V> map)

WeakHashMap的属性

  • Entry<K,V>[] table

table是一个Entry数组,而Entry实际上就是一个单向链表。哈希表的“key-value键值对”都是存储在Entry数组中的。

  • private int size

size是Hashtable的大小,它是Hashtable保存的键值对的数量。

  • private int threshold

threshold是阈值,用于判断是否需要调整容量。threshold=“容量*负载因子”。

  • private final float loadFactor

loadFactor就是负载因子,它是衡量哈希表能放多满的数值。

  • int modCount

modCount是用来实现fail-fast机制的,记录哈希表被修改的次数。

  • private final ReferenceQueue<Object> queue = new ReferenceQueue<>()

引用队列:当弱引用所引用的对象被垃圾回收器回收时,该弱引用将被垃圾回收器添加到与之关联的引用队列中。

WeakHashMap的关键实现

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

Entry继承自WeakReference(弱引用),那么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;
    }

从Entry的构造函数中可以看出:Entry通过传入key和queue调用了父类WeakReference的构造函数,那么key就成为了这个弱引用所引用的对象,并把这个弱引用注册到了引用队列上。

《WeakHashMap源码分析》 image.png

如果一个对象只具有弱引用,那就类似于可有可无的生活用品。只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

因为存储在Entry中的key只具有弱引用,所以并不能阻止垃圾回收线程对它进行回收,当发生垃圾回收时,Entry中的key被回收,java虚拟机就会把这个Entry添加到与之关联的queue中去。

通过上面的分析,存储在WeakHashMap中的key随时都会面临被回收的风险,因此每次查询WeakHashMap时,都要确认当前WeakHashMap是否已经有key被回收了。当key被回收时,引用这个key的Entry对象就会被添加到引用队列中去,所以只要查询引用队列是否有Entry对象,就可以确认是否有key被回收了。WeakHashMap通过调用expungeStaleEntries方法来清除已经被回收的key所关联的Entry对象。

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在调用putget方法之前,都会调用expungeStaleEntries方法来清除已经被回收的key所关联的Entry对象。因为Entry是弱引用,即使引用着key对象,但是依然不能阻止垃圾回收线程对key对象的回收。

如果存放在WeakHashMap中的key都存在强引用,那么WeakHashMap就会退化成HashMap。如果在系统中希望通过WeakHashMap自动清除数据,请尽量不要在系统的其他地方强引用WeakHashMap的key,否则,这些key就不会被回收,WeakHashMap也就无法正常释放它们所占用的表项。

案例应用

如果在一个普通的HashMap中存储一些比较大的值如下:

Map<Integer,Object> map = new HashMap<>();
for(int i=0;i<10000;i++)
{
    Integer ii = new Integer(i);
    map.put(ii, new byte[i]);
}

运行参数:-Xmx5M
运行结果:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at collections.WeakHashMapTest.main(WeakHashMapTest.java:39)

如果我们将HashMap换成WeakHashMap其余都不变:

Map<Integer,Object> map = new WeakHashMap<>();
for(int i=0;i<10000;i++)
{
    Integer ii = new Integer(i);
    map.put(ii, new byte[i]);
}

运行结果:(无任何报错)

这两段代码比较可以看到WeakHashMap的功效,如果在系统中需要一张很大的Map表,Map中的表项作为缓存使用,这也意味着即使没能从该Map中取得相应的数据,系统也可以通过候选方案获取这些数据。虽然这样会消耗更多的时间,但是不影响系统的正常运行。

在这种场景下,使用WeakHashMap是最合适的。因为WeakHashMap会在系统内存范围内,保存所有表项,而一旦内存不够,在GC时,没有被引用的表项又会很快被清除掉,从而避免系统内存溢出。

我们这里稍微改变一下上面的代码(加了一个List):

Map<Integer,Object> map = new WeakHashMap<>();
List<Integer> list = new ArrayList<>();
for(int i=0;i<10000;i++)
{
    Integer ii = new Integer(i);
    list.add(ii);
    map.put(ii, new byte[i]);
}   

运行结果:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at collections.WeakHashMapTest.main(WeakHashMapTest.java:43)

如果存放在WeakHashMap中的key都存在强引用,那么WeakHashMap就会退化成HashMap。如果在系统中希望通过WeakHashMap自动清除数据,请尽量不要在系统的其他地方强引用WeakHashMap的key,否则,这些key就不会被回收,WeakHashMap也就无法正常释放它们所占用的表项。

要想WeakHashMap能够释放掉被回收的key关联的value对象,要尽可能的多调用下put/size/get等操作,因为这些方法会调用expungeStaleEntries方法,expungeStaleEntries方法是关键,而如果不操作WeakHashMap,以企图WeakHashMap“自动”释放内存是不可取的,这里的“自动”是指譬如:map.put(obj, new byte[10M]);之后obj=null了,之后再也没调用过map的任何方法,那么new出来的10M空间是不会释放的。

注意

WeakHashMap的key可以为null,那么当put一个key为null,value为一个很大对象的时候,这个很大的对象怎么采用WeakHashMap的自带功能自动释放呢?

代码如下:

Map<Object,Object> map = new WeakHashMap<>();
map.put(null,new byte[5*1024*928]);
int i = 1;
while(true)
{
    System.out.println();
    TimeUnit.SECONDS.sleep(2);
    System.out.println(map.size());
    System.gc();
    System.out.println("==================第"+i+++"次GC结束====================");
}

运行参数:-Xmx5M -XX:+PrintGCDetails
运行结果:

1
[GC [PSYoungGen: 680K->504K(2560K)] 5320K->5240K(7680K), 0.0035741 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 504K->403K(2560K)] [ParOldGen: 4736K->4719K(5120K)] 5240K->5123K(7680K) [PSPermGen: 2518K->2517K(21504K)], 0.0254473 secs] [Times: user=0.06 sys=0.00, real=0.03 secs] 
==================第1次GC结束====================

1
[Full GC [PSYoungGen: 526K->0K(2560K)] [ParOldGen: 4719K->5112K(5120K)] 5246K->5112K(7680K) [PSPermGen: 2520K->2520K(21504K)], 0.0172785 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
==================第2次GC结束====================

1
[Full GC [PSYoungGen: 41K->0K(2560K)] [ParOldGen: 5112K->5112K(5120K)] 5153K->5112K(7680K) [PSPermGen: 2520K->2520K(21504K)], 0.0178421 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] 
==================第3次GC结束====================

1
[Full GC [PSYoungGen: 41K->0K(2560K)] [ParOldGen: 5112K->5112K(5120K)] 5153K->5112K(7680K) [PSPermGen: 2520K->2520K(21504K)], 0.0164874 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
==================第4次GC结束====================

1
[Full GC [PSYoungGen: 41K->0K(2560K)] [ParOldGen: 5112K->5112K(5120K)] 5153K->5112K(7680K) [PSPermGen: 2520K->2520K(21504K)], 0.0191096 secs] [Times: user=0.05 sys=0.00, real=0.02 secs] 
==================第5次GC结束====================
(一直循环下去)

可以看到在map.put(null, new byte[5*1024*928]);之后,相应的内存一直没有得到释放。

通过显式的调用map.remove(null)可以将内存释放掉,如下代码所示:

Map<Integer,Object> map = new WeakHashMap<>();
System.gc();
System.out.println("===========gc:1=============");
map.put(null,new byte[4*1024*1024]);
TimeUnit.SECONDS.sleep(5);
System.gc();
System.out.println("===========gc:2=============");
TimeUnit.SECONDS.sleep(5);
System.gc();
System.out.println("===========gc:3=============");
map.remove(null);
TimeUnit.SECONDS.sleep(5);
System.gc();
System.out.println("===========gc:4=============");

运行参数:-Xmx5M -XX:+PrintGCDetails
运行结果:

[GC [PSYoungGen: 720K->504K(2560K)] 720K->544K(6144K), 0.0023652 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 504K->0K(2560K)] [ParOldGen: 40K->480K(3584K)] 544K->480K(6144K) [PSPermGen: 2486K->2485K(21504K)], 0.0198023 secs] [Times: user=0.11 sys=0.00, real=0.02 secs] 
===========gc:1=============
[GC [PSYoungGen: 123K->32K(2560K)] 4699K->4608K(7680K), 0.0026722 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 32K->0K(2560K)] [ParOldGen: 4576K->4578K(5120K)] 4608K->4578K(7680K) [PSPermGen: 2519K->2519K(21504K)], 0.0145734 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 
===========gc:2=============
[GC [PSYoungGen: 40K->32K(2560K)] 4619K->4610K(7680K), 0.0013068 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 32K->0K(2560K)] [ParOldGen: 4578K->4568K(5120K)] 4610K->4568K(7680K) [PSPermGen: 2519K->2519K(21504K)], 0.0189642 secs] [Times: user=0.06 sys=0.00, real=0.02 secs] 
===========gc:3=============
[GC [PSYoungGen: 40K->32K(2560K)] 4609K->4600K(7680K), 0.0011742 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 32K->0K(2560K)] [ParOldGen: 4568K->472K(5120K)] 4600K->472K(7680K) [PSPermGen: 2519K->2519K(21504K)], 0.0175907 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
===========gc:4=============
Heap
 PSYoungGen      total 2560K, used 82K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 4% used [0x00000000ffd00000,0x00000000ffd14820,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 5120K, used 472K [0x00000000ff800000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 5120K, 9% used [0x00000000ff800000,0x00000000ff876128,0x00000000ffd00000)
 PSPermGen       total 21504K, used 2526K [0x00000000fa600000, 0x00000000fbb00000, 0x00000000ff800000)
  object space 21504K, 11% used [0x00000000fa600000,0x00000000fa8778f8,0x00000000fbb00000)

分析:

1、在WeakHashMap中,put的key为null时,放入的是NULL_KEY,即:private static final Object NULL_KEY = new Object(),是一个静态常量。
2、在WeakHashMap中,由于传给WeakReference的只有key和queue,即gc只回收里面的KEY,而不会动value,value的清除则是在expungeStaleEntries这个私有方法进行的。
3、而static的就不在gc之列,所以key也就不会被gc,所以它的大值value,也就不会被设为null,不会被回收。
4、通过调用remove方法,最终table[k]设为null,此时大对象游离所以被回收。

只有通过remove方法才能删除null键所关联的value,建议在使用WeakHashMap的时候尽量避免使用null作为键。

    原文作者:史路比
    原文地址: https://www.jianshu.com/p/9e47ff490365
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞