欢迎光临散文网 会员登陆 & 注册

Java十三篇:WeakHashMap

2023-03-03 00:03 作者:小刘Java之路  | 我要投稿

集合的最后一篇:WeakHashMap

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

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

WeakHashMap的key是“弱键”,即是WeakReference类型的;ReferenceQueue是一个队列,它会保存被GC回收的“弱键”。实现步骤是:

(01) 新建WeakHashMap,将“键值对”添加到WeakHashMap中。实际上,WeakHashMap是通过数组table保存Entry(键值对);每一个Entry实际上是一个单向链表,即Entry是键值对链表。

(02) 当某“弱键”不再被其它对象引用,并被GC回收时。在GC回收该“弱键”时,这个“弱键”也同时会被添加到ReferenceQueue(queue)队列中。

(03) 当下一次我们需要操作WeakHashMap时,会先同步table和queue。table中保存了全部的键值对,而queue中保存被GC回收的键值对;同步它们,就是删除table中被GC回收的键值对

这就是“弱键”如何被自动从WeakHashMap中删除的步骤了。

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

既然有WeakHashMap,那么有WeakHashSet吗?java collections包是没有直接提供WeakHashSet的。

WeakHashMap的这种特性比较适合实现类似本地、堆内缓存的存储机制——缓存的失效依赖于GC收集器的行为

WeakHashMap的定义如下:

publicclass WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>

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

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

它的特殊之处在于 WeakHashMap 里的entry可能会被GC自动删除,即使程序员没有调用remove()或者clear()方法。

更直观的说,当使用 WeakHashMap 时,即使没有显示的添加或删除任何元素,也可能发生如下情况:

调用两次size()方法返回不同的值;

两次调用isEmpty()方法,第一次返回false,第二次返回true;

两次调用containsKey()方法,第一次返回true,第二次返回false,尽管两次使用的是同一个key;

两次调用get()方法,第一次返回一个value,第二次返回null,尽管两次使用的是同一个对象。

遇到这么奇葩的现象,你是不是觉得使用者一定会疯掉?其实不然,WeekHashMap 的这个特点特别适用于需要缓存的场景。在缓存场景下,由于内存是有限的,不能缓存所有对象;对象缓存命中可以提高系统效率,但缓存MISS也不会造成错误,因为可以通过计算重新得到。

要明白 WeekHashMap 的工作原理,还需要引入一个概念:弱引用(WeakReference)。我们都知道Java中内存是通过GC自动管理的,GC会在程序运行过程中自动判断哪些对象是可以被回收的,并在合适的时机进行内存释放。GC判断某个对象是否可被回收的依据是,是否有有效的引用指向该对象。如果没有有效引用指向该对象(基本意味着不存在访问该对象的方式),那么该对象就是可回收的。这里的**“有效引用”并不包括弱引用**。也就是说,虽然弱引用可以用来访问对象,但进行垃圾回收时弱引用并不会被考虑在内,仅有弱引用指向的对象仍然会被GC回收。

WeakHashMap 内部是通过弱引用来管理entry的,弱引用的特性对应到 WeakHashMap 上意味着什么呢?将一对key, value放入到 WeakHashMap 里并不能避免该key值被GC回收,除非在 WeakHashMap 之外还有对该key的强引用。

关于强引用,弱引用等概念以后再具体讲解,这里只需要知道Java中引用也是分种类的,并且不同种类的引用对GC的影响不同就够了。

具体实现

Weak HashSet?

如果你看过前几篇关于 Map 和 Set 的讲解,一定会问:既然有 WeekHashMap,是否有 WeekHashSet 呢?答案是没有===不过Java Collections工具类给出了解决方案,Collections.newSetFromMap(Map<E,Boolean> map)方法可以将任何 Map包装成一个Set。通过如下方式可以快速得到一个 Weak HashSet:

// 将WeakHashMap包装成一个Set
Set<Object> weakHashSet = Collections.newSetFromMap(
       new WeakHashMap<Object, Boolean>());

不出你所料,newSetFromMap()方法只是对传入的 Map做了简单包装:

// Collections.newSetFromMap()用于将任何Map包装成一个Set
publicstatic <E> Set<E> newSetFromMap(Map<E, Boolean> map) {
   returnnew SetFromMap<>(map);
}

privatestaticclass SetFromMap<E> extends AbstractSet<E>
   implements Set<E>, Serializable
{
   privatefinal Map<E, Boolean> m;  // The backing map
   privatetransient Set<E> s;       // Its keySet
   SetFromMap(Map<E, Boolean> map) {
       if (!map.isEmpty())
           thrownew IllegalArgumentException("Map is non-empty");
       m = map;
       s = map.keySet();
   }
   public void clear()               {        m.clear(); }
   public int size()                 { return m.size(); }
   public boolean isEmpty()          { return m.isEmpty(); }
   public boolean contains(Object o) { return m.containsKey(o); }
   public boolean remove(Object o)   { return m.remove(o) != null; }
   public boolean add(E e) { return m.put(e, Boolean.TRUE) == null; }
   public Iterator<E> iterator()     { return s.iterator(); }
   public Object[] toArray()         { return s.toArray(); }
   public <T> T[] toArray(T[] a)     { return s.toArray(a); }
   public String toString()          { return s.toString(); }
   public int hashCode()             { return s.hashCode(); }
   public boolean equals(Object o)   { return o == this || s.equals(o); }
   public boolean containsAll(Collection<?> c) {return s.containsAll(c);}
   public boolean removeAll(Collection<?> c)   {return s.removeAll(c);}
   public boolean retainAll(Collection<?> c)   {return s.retainAll(c);}
   // addAll is the only inherited implementation
   ......
}

实例:

此例子中声明了两个Map对象,一个是HashMap,一个是WeakHashMap,同时向两个map中放入a、b两个对象,当HashMap remove掉a 并且将a、b都指向null时,WeakHashMap中的a将自动被回收掉。

出现这个状况的原因是,对于a对象而言,当HashMap remove掉并且将a指向null后,除了WeakHashMap中还保存a外已经没有指向a的指针了,所以WeakHashMap会自动舍弃掉a,而对于b对象虽然指向了null,但HashMap中还有指向b的指针。

弱引用(WeakReference)的特性是:当gc线程发现某个对象只有弱引用指向它,那么就会将其销毁并回收内存。

package com.sino.daily.code_2019_9_1;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.WeakHashMap;

/**
* create by 2020-04-28 22:45
*
* @author liulq
*/
publicclass WeekHash {
   public static void main(String[] args) {
       String a = new String("a");
       String b = new String("b");
       Map map = new HashMap();
       map.put(a, "aaa");
       map.put(b, "bbb");

       Map weakmap = new WeakHashMap();
       weakmap.put(a, "aaa");
       weakmap.put(b, "bbb");

       map.remove(a);
       a = null;
       b = null;

       System.gc();
       
       Iterator i = map.entrySet().iterator();
       while (i.hasNext()) {
           Map.Entry en = (Map.Entry) i.next();
           System.out.println("map:" + en.getKey() + ":" + en.getValue());
       }

       Iterator j = weakmap.entrySet().iterator();
       while (j.hasNext()) {
           Map.Entry en = (Map.Entry) j.next();
           System.out.println("weakmap:" + en.getKey() + ":" + en.getValue());

       }
   }
}

经典使用场景:tomcat两级缓存

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

package org.apache.tomcat.util.collections;

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

publicfinalclass ConcurrentCache<K,V> {

   privatefinalint size;

   privatefinal Map<K,V> eden;

   privatefinal Map<K,V> 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);
   }
}

源码中有edenlongterm的两个map,对jvm堆区有所了解的话,可以猜测出tomcat在这里是使用ConcurrentHashMapWeakHashMap做了分代的缓存

在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,不会造成内存空间的浪费。

总结:

    1. WeakHashMap还是集合,但是在开发中也很少用到这种集合,也只有底层源码才能用到这些。

    2. 知道四种引用:

    强引用

    如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。比如String str = "hello"这时候str就是一个强引用。

    软引用

    内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。

    弱引用

    如果一个对象具有弱引用,在垃圾回收时候,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。

    虚引用

    如果一个对象具有虚引用,就相当于没有引用,在任何时候都有可能被回收。使用虚引用的目的就是为了得知对象被GC的时机,所以可以利用虚引用来进行销毁前的一些操作,比如说资源释放等。

    我们的WeakHashMap是基于弱引用的,也就是说只要垃圾回收机制一开启,就直接开始了扫荡,看见了就清除。


    why?什么需要WeakHashMap:WeakHashMap正是由于使用的是弱引用,因此它的对象可能被随时回收。更直观的说,当使用 WeakHashMap 时,即使没有删除任何元素,它的尺寸、get方法也可能不一样,适用于缓存的场景

    3.这个集合虽然现在不是很能理解他,但是他的实现原理和设计原理值得我们学习,希望在我们技术的进阶中掌握这种设计和实现

    4.都是站在巨人的肩膀上前进,多学习,多总结,多自己总结经验,多敲代码,自律,锻炼身体,其他的都交给时间来检验成果吧!





Java十三篇:WeakHashMap的评论 (共 条)

分享到微博请遵守国家法律