10.Java 集合 - WeakHashMap

基本概念

首先来看它的继承关系:

这里写图片描述

WeakHashMap 是以弱键实现的基于哈希表的 Map。在 WeakHashMap 中,当某个键不再正常使用时,将自动移除其条目。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。丢弃某个键时,其条目从映射中有效地移除,因此,该类的行为与其他的 Map 实现有所不同。

需要注意的地方:

  • null 值和 null 键都被支持。该类具有与 HashMap 类相似的性能特征,并具有相同的效能参数初始容量 和加载因子。

  • 像大多数 collection 类一样,该类是不同步的。可以使用 Collections.synchronizedMap 方法来构造同步的 WeakHashMap。


源码分析

由于 WeakHashMap 与 HashMap 的底层数据结构以及各方法实现原理大致相同,这里只分析不同点,细节就此略过。

1.节点

Entry(节点) 是 WeakHashMap 的一个静态内部类,用来保存 key-value。

  • 同 HashMap 一样,它也表示单向链表的一个节点。

  • 不同的是,它继承了 WeakReference类。key 采用了弱引用的方式。

// 相关成员变量
 private final ReferenceQueue<K> queue = new ReferenceQueue<K>();

// 节点
private static class Entry<K, V> extends WeakReference<K> implements Map.Entry<K, V>{

    private V value;
    private final int hash;
    private Entry<K, V> next;

    // 构造函数
    Entry(K key, V value, ReferenceQueue<K> queue, int hash, Entry<K, V> next) {

        // key 传给了父类 WeakReference,说明 key 为弱引用
        super(key, queue);

        // value 采用了强引用
        this.value = value;

        this.hash = hash;
        this.next = next;
    }

    public K getKey() {
        // 调用了 unmaskNull,因为当 key = null 时,用成员属性 NULL_KEY 表示
        // 为了区分无用节点(null-value),下面会具体讲到
        return WeakHashMap.<K> unmaskNull(get());
    }

    //...
}

2.弱引用

弱引用(Weak Reference) 是 Java 四种引用类型之一。当弱引用指向的对象没有存在引用时,GC 就会清理掉该对象。之后,该弱引用对象(即节点)会被放到 ReferenceQueue 中。

public static void main(String[] args) throws Exception {
    String str = new String("Hello World !");
    WeakReference<String> r = new WeakReference<String>(str);

    // 弱引用对象(str)存在引用
    System.out.println("①Before GC:"+r.get());

    System.gc();
    Thread.sleep(100);
    System.out.println("①After GC:"+r.get());

    // 将 str 置空,表示其不存在引用
    str = null;
    System.out.println("②Before GC:"+r.get());
    System.gc();
    Thread.sleep(100);
    System.out.println("②After GC:"+r.get());    
}

// 输出结果:
// ①Before GC:Hello World !
// ①After GC:Hello World !
// ②Before GC:Hello World !
// ②After GC:null

搞明白了弱引用的原理后,再回到 WeakHashMap 的 Entry。上面提到 key 采用了弱引用,因此当 key 不存在引用时,就有可能被 GC 回收,紧接着该弱引用对象(r)就会被放入 ReferenceQueue 中。

那么问题来了, WeakHashMap 中指定的节点(key-value)现在变成了(null-value),但是它还存在 。因此还要删除对应的无用节点。如何删除?请接着往下看…


3.expungeStaleEntries

在 WeakHashMap 中,expungeStaleEntries 这个方法主要用于消除无用的节点,即 (null -value)

当执行 expungeStaleEntries 时,会遍历 ReferenceQueue(即 queue) 中的所有 WeakReference 对象(该对象会被强制转换成 Entry),然后删除 WeakHashMap 中对应的节点。

private void expungeStaleEntries() {
    Entry<K, V> e;

    // 遍历 ReferenceQueue 的 WeakReference 对象
    while ((e = (Entry<K, V>) queue.poll()) != null) {
        int h = e.hash;
        int i = indexFor(h, table.length);

        Entry<K, V> prev = table[i];
        Entry<K, V> p = prev;

        // 遍历数组指定位置的单链表
        while (p != null) {
            Entry<K, V> next = p.next;

            // 找到与 queue 中 WeakReference 对象对应的节点 
            if (p == e) {
                // 判断 p 是不是单链表的首节点
                if (prev == e) {
                    // 删除节点 -> 修改首节点为其后继节点(数组中存放的是单链表的首节点)
                    table[i] = next;
                } else {
                    // 删除节点 -> 修改其前继节点的后指针
                    prev.next = next;
                }

                // 将 WeakReference 链表中的下一个对象置空???
                e.next = null; 

                // 把value 赋值为 null,来帮助 GC 回收强引用的 value
                e.value = null; 
                size--;
                break;
            }
            prev = p;
            p = next;
        }
    }
}

再来看看该类中 expungeStaleEntries 的调用,在 WeakHashMap 中几乎所有的操作(put,remove,get,size)都会涉及到消除操作,这也正体现了它的 weak

这里写图片描述


4.NULL_KEY

上面提到了 WeakHashMap 的成员变量 NULL_KEY,当 key = null 时,会被其代替。

private static final Object NULL_KEY = new Object();

private static Object maskNull(Object key) {
    return (key == null ? NULL_KEY : key);
}

key 是弱引用,在其被 GC 回收后,对应的节点就会从(key-value)变成(null-value);
当要新增一个 key = null 的节点时,即 put(null,key)为了避免其被当成无用节点。null 会被
NULL_KEY 代替,变成(NULL_KEY,key)以此来区分。

这里有个疑问:①当 key = null 时,调用 GC 后该节点不会被回收;②当 key = new Object( )时,节点就会被回收。然而 maskNull(null)返回的结果是 new Object( ),但是为什么结果会不一样,这里望大牛不吝赐教。

// ①
public static void main(String[] args) throws Exception {
    WeakHashMap<Object, String> wmap = new WeakHashMap<Object, String>();
    wmap.put(null, "a");
    System.gc();
    Thread.sleep(100);
    System.out.println(wmap.size());
    // 输出结果:1
}


// ②
public static void main(String[] args) throws Exception {
    WeakHashMap<Object, String> wmap = new WeakHashMap<Object, String>();
    wmap.put(new Object(), "a");
    System.gc();
    Thread.sleep(100);
    System.out.println(wmap.size());
    // 输出结果:0
}

实例探究

实例 ① ,在程序运行不久后就 OOF 了。实例 ② ,则能一直运行下去。观察代码,发现它们的区别仅仅是最后调用了 size 方法。原因就在这里,size 方法会调用 expungeStaleEntries 清除无用节点,防止内存不断增加。

//①
public static void main(String[] args) {
    List<WeakHashMap<byte[][], byte[][]>> maps = new ArrayList<WeakHashMap<byte[][], byte[][]>>();
    for (int i = 0;; i++) {
        WeakHashMap<byte[][], byte[][]> d = new WeakHashMap<byte[][], byte[][]>();
        d.put(new byte[10000][10000], new byte[10000][10000]);
        maps.add(d);
        System.gc();
        System.err.print(i+",");  

        // 输出结果:
        // 0,1,2,3,4,5,6,7,8,9,10,
        // Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at Test3.main(Test3.java:10)
    }
}


//②
public static void main(String[] args) {
    List<WeakHashMap<byte[][], byte[][]>> maps = new ArrayList<WeakHashMap<byte[][], byte[][]>>();
    for (int i = 0;; i++) {
        WeakHashMap<byte[][], byte[][]> d = new WeakHashMap<byte[][], byte[][]>();
        d.put(new byte[10000][10000], new byte[10000][10000]);
        maps.add(d);
        System.gc();
        // 关键 -> 区别:
        System.err.println(  "size = " + d.size());  

        // 输出结果:
        // size = 0
        // ...
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

oxf

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值