概述

WeakHashMap也是Map接口的一个实现类,它与HashMap相似,也是一个哈希表,存储key-value pair,而且也是非线程安全的。不过WeakHashMap并没有引入红黑树来尽量规避哈希冲突带来的影响,内部实现只是数组+单链表。此外,WeakHashMap与HashMap最大的不同之处在于,WeakHashMap的key是“弱键”(weak keys),即当一个key不再正常使用时,key对应的key-value pair将自动从WeakHashMap中删除,在这种情况下,即使key对应的key-value pair的存在,这个key依然会被GC回收,如此以来,它对应的key-value pair也就被从map中有效地删除了。

Java的四种引用

在正式进入WeakHashMap源码之前,我们需要先对“弱引用”有一个基本的认识,为此这里先介绍一下JDK 1.2开始推出的四种引用:

  • 强引用(Strong Reference) 强引用是指在程序代码之中普遍存在的,类似Objective obj = new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
  • 软引用(Soft Reference) 软引用是用来描述一些还有用但并非必需的对象,对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。
  • 弱引用(Weak Reference) 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一点,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。
  • 虚引用(PhantomReference) 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

我们说WeakHashMap的key是weak-keys,即是说这个Map实现类的key值都是弱引用。

底层实现

先来看一下WeakHashMap的定义:

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

与HashMap一样继承自AbstractMap类,并特地标注了Map接口,另外,WeakHashMap并没有实现Cloneable接口和Serializable接口。

再来看一下WeakHashMap的重要静态内部类Entry:

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next; /** * Creates new entry. */
Entry(Object key, V value,
ReferenceQueue<Object> queue,
int hash, Entry<K,V> next) {
//把key传给父类WeakReference的构造函数,说明Entry的key是弱引用
//同时key值会进入引用队列queue中,等待被处理
super(key, queue);
//显示定义了value,说明了Entry的value是强引用
this.value = value;
this.hash = hash;
this.next = next;
} @SuppressWarnings("unchecked")
public K getKey() {//在获取key时需要unmaskNull,因为对于null的key,是用WeakHashMap的内部成员属性来表示的
return (K) WeakHashMap.unmaskNull(get());
} public V getValue() {
return value;
} public V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
} public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
K k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
V v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
} public int hashCode() {
K k = getKey();
V v = getValue();
return Objects.hashCode(k) ^ Objects.hashCode(v);
} public String toString() {
return getKey() + "=" + getValue();
}
}

这里用到了unmaskNull()方法,它的作用是用一个空的Object对象来表示null值得key。其源码如下:

private static final Object NULL_KEY = new Object();
/** * 当key为null时,使用NULL_KEY表示key */
private static Object maskNull(Object key) {
return (key == null) ? NULL_KEY : key;
}

可以看到Entry<K,V>继承自WeakReference类,那么对于Entry类中需要定义为弱引用的字段,直接传入父类的构造函数即可,如代码中看到的key,这也实现了我们前面说的“弱键”。而value值则赋予了强引用,但这并不影响,我们在稍后会介绍一个WeakHashMap.expungeStaleEntries方法,该方法会把弱键对应的key-value整个赋为null,以帮助GC将其回收。

再来看一下WeakHashMap的重要字段:

 /** * 存储键值对的数组,一般是2的幂 */
Entry<K,V>[] table;
/** * 键值对的实际个数 */
private int size;
/** * 扩容的临界值,通过capacity * load factor可以计算出来。超过这个值HashMap将进行扩容 * @serial */
private int threshold;
/** * 负载因子 */
private final float loadFactor;
/** * 引用队列,用于保存会被GC回收的弱键 */
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
/** * 记录HashMap被修改结构的次数。 * 修改包括改变键值对的个数或者修改内部结构,比如rehash * 这个域被用作HashMap的迭代器的fail-fast机制中(参考ConcurrentModificationException) */
int modCount;

与HashMap相比,WeakHashMap少了entrySet字段,而多了一个引用队列queue。

WeakHashMap定义的静态全局变量如下:

 private static final int DEFAULT_INITIAL_CAPACITY = 16;

    /** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. */
private static final int MAXIMUM_CAPACITY = 1 << 30; /** * The load factor used when none specified in constructor. */
private static final float DEFAULT_LOAD_FACTOR = 0.75f;

与HashMap相比,缺少了TREEIFY_THRESHOLD、UNTREEIFY_THRESHOLD、MIN_TREEIFY_CAPACITY三个静态全局变量,它们是用来进行单链表和红黑树转换的,在WeakHashMap中没有实现红黑树,因此不需要这三个变量。

下面再来看一下与弱键实现有关的重要方法expungeStaleEntries

/** * 从哈希表中删除被回收的引用 */
private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;//e为要清理的Entry
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; // 把value赋值为null,帮助GC回收强引用的value
size--;
break;
}
prev = p;
p = next;
}
}
}
}

从WeakHashMap.Entry的源码中我们看到,weak keys的值会被保存到引用队列中,该方法就说将引用队列中保存的弱键对应的Entry从单链表中删除,即删除哈希表中被GC回收了的键值对。在WeakHashMap定义的增、删、改、查方法中,都要调用该方法。

应用场景

缓存

缓存是内存泄漏的一个常见来源,一旦你把对象引用放到缓存中,它就很容易被遗忘掉,从而使得它不再有用之后很长一段时间内仍然留在缓存中。

对于这个问题,有几种可能的解决方案。如果你正好要实现这样的缓存:只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap代表缓存。当缓存中的项过期之后,它们就会自动被删除(要注意的是,只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处)。

Tomcat就用WeakHashMap实现了它的并发缓存ConcurrentCache,源码如下:

package org.apache.tomcat.util.collections;

import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap; public final class ConcurrentCache<K,V> { private final int size; private final Map<K,V> eden; private final 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);
}
}

监听器和其他回调

内存泄漏的另一个常见来源是监听器和其他回调。如果你在实现的是客户端注册回调却没有显式地取消注册的API,除非你采取某些动作,否则它们就会积聚。确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用(weak reference),可以只将它们保存成WeakHashMap中的键。

------------------------推荐阅读------------------------

2019年JVM最新面试题,必须收藏它

最全面的阿里多线程面试题,你能回答几个?

Java面试题:Java中的集合及其继承关系

花了近十年的时间,整理出史上最全面Java面试题

WeakHashMap,源码解读的更多相关文章

  1. WeakHashMap源码解读

    1. 简介 本文基于JDK8u111的源码分析WeakHashMap的一些主要方法的实现. 2. 数据结构 就数据结构来说WeakHashMap与HashMap原理差不多,都是拉链法来解决哈希冲突. ...

  2. jdk1.8.0_45源码解读——Map接口和AbstractMap抽象类的实现

    jdk1.8.0_45源码解读——Map接口和AbstractMap抽象类的实现 一. Map架构 如上图:(01) Map 是映射接口,Map中存储的内容是键值对(key-value).(02) A ...

  3. SDWebImage源码解读之SDWebImageDownloaderOperation

    第七篇 前言 本篇文章主要讲解下载操作的相关知识,SDWebImageDownloaderOperation的主要任务是把一张图片从服务器下载到内存中.下载数据并不难,如何对下载这一系列的任务进行设计 ...

  4. SDWebImage源码解读 之 NSData+ImageContentType

    第一篇 前言 从今天开始,我将开启一段源码解读的旅途了.在这里先暂时不透露具体解读的源码到底是哪些?因为也可能随着解读的进行会更改计划.但能够肯定的是,这一系列之中肯定会有Swift版本的代码. 说说 ...

  5. SDWebImage源码解读 之 UIImage+GIF

    第二篇 前言 本篇是和GIF相关的一个UIImage的分类.主要提供了三个方法: + (UIImage *)sd_animatedGIFNamed:(NSString *)name ----- 根据名 ...

  6. SDWebImage源码解读 之 SDWebImageCompat

    第三篇 前言 本篇主要解读SDWebImage的配置文件.正如compat的定义,该配置文件主要是兼容Apple的其他设备.也许我们真实的开发平台只有一个,但考虑各个平台的兼容性,对于框架有着很重要的 ...

  7. SDWebImage源码解读_之SDWebImageDecoder

    第四篇 前言 首先,我们要弄明白一个问题? 为什么要对UIImage进行解码呢?难道不能直接使用吗? 其实不解码也是可以使用的,假如说我们通过imageNamed:来加载image,系统默认会在主线程 ...

  8. SDWebImage源码解读之SDWebImageCache(上)

    第五篇 前言 本篇主要讲解图片缓存类的知识,虽然只涉及了图片方面的缓存的设计,但思想同样适用于别的方面的设计.在架构上来说,缓存算是存储设计的一部分.我们把各种不同的存储内容按照功能进行切割后,图片缓 ...

  9. SDWebImage源码解读之SDWebImageCache(下)

    第六篇 前言 我们在SDWebImageCache(上)中了解了这个缓存类大概的功能是什么?那么接下来就要看看这些功能是如何实现的? 再次强调,不管是图片的缓存还是其他各种不同形式的缓存,在原理上都极 ...

随机推荐

  1. Django之web框架原理

    Web框架原理 我们可以这样理解:所有的Web应用本质上就是一个socket服务端,而用户的浏览器就是一个socket客户端. 这样我们就可以自己实现Web框架了. 先写一个 原始的web框架 imp ...

  2. C# webclient progresschanged downlodfileCompleted

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...

  3. abp去掉AbpUser中的Name,Surname,去掉姓和名分离

    abp是国外的框架,默认的框架中的AbpUser表中的Name和Surname是分开的,这不符合国情:可以先去掉 1. 在User类中重写Name和Surname,并设置为私有 2. 在DbConte ...

  4. MyBatis框架之第二篇

    1.高级参数映射和返回值映射(重点) a)Pojo包装pojo的参数映射 b)当结果集列名与pojo属性名不一致的返回值映射 2.动态sql(重点) 3.关联查询结果(重点) a)一对一关联结果 b) ...

  5. .WrongArgumentException: Malformed database URL, failed to parse the connection string near ';characterEncoding=UTF-8&;serverTimezone=Asia/Shanghai'.)

    连接mysql库报的异常信息: org.springframework.transaction.CannotCreateTransactionException: Could not open JDB ...

  6. textarea中文本高亮选中

    最近在实现原文/译文句段高亮对比显示,和有道翻译类似,如下图所示: 最初的解决方案是采用富文本编辑器,把所有句段信息都用HTML标签包裹,操作空间比较大,页面上需要的功能几乎都可以实现,但是由此带来了 ...

  7. ABP入门教程4 - 初始化运行

    点这里进入ABP入门教程目录 编译解决方案 重新生成解决方案,确保生成成功. 连接数据库 打开JD.CRS.Web.Host / appsettings.json,修改数据库连接设置Connectio ...

  8. sshd 启动后 出现:Could not load host key: /etc/ssh/ssh_host_rsa_key

    今天在启动sshd时,出现了如下问题,导致客户端不能成功连接服务端,不能建立ssh连接: 有两个提示 Could not load host key: /etc/ssh/ssh_host_rsa_ke ...

  9. Django—使用后台管理Models

    后台的配置 1.创建后台管理员 [root@localhost study_django]# python manage.py createsuperuser [root@localhost stud ...

  10. Linux系统学习 六、网络基础—Linux的IP地址的配置

    1.ifconfig命令临时配置IP地址 2.setup工具永久配置IP地址 3.修改网络配置文件 4.图形界面配置IP地址 ifconfig命令临时配置IP地址 主要的作用是查看网络信息,也可以临时 ...