一文搞定WeakHashMap

写在前面

在缓存场景下,由于内存是有限的,不能缓存所有对象,因此就需要一定的删除机制,淘汰掉一些对象。这个时候可能很快就想到了各种Cache数据过期策略,目前也有一些优秀的包提供了功能丰富的Cache,比如Google的Guava Cache,它支持数据定期过期、LRU、LFU等策略,但它仍然有可能会导致有用的数据被淘汰,没用的数据迟迟不淘汰(如果策略使用得当的情况下这都是小概率事件)。

现在有种机制,可以让Cache里不用的key数据自动清理掉,用的还留着,不会出现误删除。而WeakHashMap 就适用于这种缓存的场景,因为它有自清理机制!

如果让你手动实现一种自清理的HashMap,可以怎么做?首先肯定是想办法先知道某个Key肯定没有在用了,然后清理掉HashMap中没有在用的对应的K-V。在JVM里一个对象没用了是指没有任何其他有用对象直接或者间接执行它,具体点就是在GC过程中它是GCRoots不可达的。而某个弱引用对象所指向的对象如果被判定为垃圾对象,Jvm会将该弱引用对象放到一个ReferenceQueue里,只需要看下这个Queue里的内容就知道某个对象还有没有用了。

WeakHashMap概述

从WeakHashMap名字也可以知道,这是一个弱引用的Map,当进行GC回收时,弱引用指向的对象会被GC回收。

WeakHashMap正是由于使用的是弱引用,因此它的对象可能被随时回收。更直观的说,当使用 WeakHashMap 时,即使没有显示的添加或删除任何元素,也可能发生如下情况:

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

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

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

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

从上图可以看出:

  1. WeakHashMap继承于AbstractMap,并且实现了Map接口。

  2. WeakHashMap是哈希表,但是它的键是"弱键"。WeakHashMap中保护几个重要的成员变量:table, size, threshold, loadFactor, modCount, queue。

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

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

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

    • loadFactor就是加载因子。

    • modCount是用来实现fail-fast机制的

    • queue保存的是“已被GC清除”的“弱引用的键”。

基本用法

WeakHashMap < String, String > weakHashMap = new WeakHashMap < > (10);String key0 = new String("str1");
String key1 = new String("str2");
String key2 = new String("str3");// 存放元素
weakHashMap.put(key0, "data1");
weakHashMap.put(key1, "data2");
weakHashMap.put(key2, "data3");
System.out.printf("weakHashMap: %s\n", weakHashMap);// 是否包含某key
System.out.printf("contains key str1 : %s\n", weakHashMap.containsKey(key0));
System.out.printf("contains key str2 : %s\n", weakHashMap.containsKey(key1));// 移除key
weakHashMap.remove(key0);
System.out.printf("weakHashMap after remove: %s\n", weakHashMap);// 这意味着"弱键"key1再没有被其它对象引用,调用gc时会回收WeakHashMap中与key1对应的键值对
key1 = null;
// 内存回收,这里会回收WeakHashMap中与"key0"对应的键值对
System.gc();try {Thread.sleep(100);
} catch (InterruptedException e) {e.printStackTrace();
}// 遍历WeakHashMap
for (Map.Entry < String, String > m: weakHashMap.entrySet()) {System.out.printf("next : %s >>> %s\n", m.getKey(), m.getValue());
}
// 打印WeakHashMap的实际大小
System.out.printf("after gc WeakHashMap size: %s\n", weakHashMap.size());

底层源码

构造器

// 默认构造函数。
WeakHashMap()// 指定“容量大小”的构造函数
WeakHashMap(int capacity)// 指定“容量大小”和“加载因子”的构造函数
WeakHashMap(int capacity, float loadFactor)// 包含“子Map”的构造函数
WeakHashMap(Map<? extends K, ? extends V> map)

从WeakHashMap的继承关系上来看,可知其继承AbstractMap,实现了Map接口。其底层数据结构是Entry数组,Entry的数据结构如下:

从源码上可知,Entry的内部并没有存储key的值,而是通过调用父类的构造方法,传入key和ReferenceQueue(这里与ThreadLocal类似),最终key和queue会关联到Reference中,这里是GC时,清除key的关键,这里大致看下Reference的源码:

private static class ReferenceHandler extends Thread {private static void ensureClassInitialized(Class <? > clazz) {try {Class.forName(clazz.getName(), true, clazz.getClassLoader());} catch (ClassNotFoundException e) {throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);}}static {// pre-load and initialize InterruptedException and Cleaner classes// so that we don't get into trouble later in the run loop if there's// memory shortage while loading/initializing them lazily.ensureClassInitialized(InterruptedException.class);ensureClassInitialized(Cleaner.class);}ReferenceHandler(ThreadGroup g, String name) {super(g, name);}public void run() {// 注意这里为一个死循环while (true) {tryHandlePending(true);}}
}static boolean tryHandlePending(boolean waitForNotify) {Reference < Object > r;Cleaner c;try {synchronized(lock) {if (pending != null) {r = pending;// 'instanceof' might throw OutOfMemoryError sometimes// so do this before un-linking 'r' from the 'pending' chain...c = r instanceof Cleaner ? (Cleaner) r : null;// unlink 'r' from 'pending' chainpending = r.discovered;r.discovered = null;} else {// The waiting on the lock may cause an OutOfMemoryError// because it may try to allocate exception objects.if (waitForNotify) {lock.wait();}// retry if waitedreturn waitForNotify;}}} catch (OutOfMemoryError x) {// Give other threads CPU time so they hopefully drop some live references// and GC reclaims some space.// Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above// persistently throws OOME for some time...Thread.yield();// retryreturn true;} catch (InterruptedException x) {// retryreturn true;}// Fast path for cleanersif (c != null) {c.clean();return true;}// 加入对列ReferenceQueue <? super Object > q = r.queue;if (q != ReferenceQueue.NULL) q.enqueue(r);return true;
}static {ThreadGroup tg = Thread.currentThread().getThreadGroup();for (ThreadGroup tgn = tg; tgn != null; tg = tgn, tgn = tg.getParent());// 创建handlerThread handler = new ReferenceHandler(tg, "Reference Handler");/* If there were a special system-only priority greater than* MAX_PRIORITY, it would be used here*/// 线程优先级最大 handler.setPriority(Thread.MAX_PRIORITY);// 设置为守护线程handler.setDaemon(true);handler.start();// provide access in SharedSecretsSharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {@Overridepublic boolean tryHandlePendingReference() {return tryHandlePending(false);}});
}

put()

public V put(K key, V value) {// 确定key值,允许key为nullObject k = maskNull(key);// 获取器hash值int h = hash(k);// 获取tabEntry <K, V> [] tab = getTable();// 确定在tab中的位置 简单的&操作int i = indexFor(h, tab.length);// 遍历,是否要进行覆盖操作  for (Entry <K, V> e = tab[i]; e != null; e = e.next) {if (h == e.hash && eq(k, e.get())) {//已经存在,则覆盖旧值V oldValue = e.value;if (value != oldValue)e.value = value;return oldValue;}}//不是旧值,就新建Entry// 修改次数自增modCount++;// 取出i上的元素Entry <K, V> e = tab[i];// 构建链表,新元素在链表头tab[i] = new Entry <> (k, value, queue, h, e);// 检查是否需要扩容if (++size >= threshold)resize(tab.length * 2);return null;
}

WeakHashMap的put操作与HashMap相似,都会进行覆盖操作(相同key),但是注意插入新节点是放在链表头;注意这里和HashMap不太一样的地方,HashMap会在链表太长的时候会将链表转换为红黑树,防止极端情况下hashcode冲突导致的性能问题,但在WeakHashMap中没有树化。

上述代码中还要一个关键的函数getTable

resize操作

WeakHashMap的扩容操作:在size大于阈值的时候,WeakHashMap也对做resize的操作,也就是把tab扩大一倍。WeakHashMap中的resize比HashMap中的resize要简单好懂些,但没HashMap中的resize优雅。

WeakHashMap中resize有另外一个额外的操作,就是expungeStaleEntries(),因为key可能被GC掉,所以在扩容时也需要考虑这种情况

void resize(int newCapacity) {Entry<K,V>[] oldTable = getTable();// 原数组长度int oldCapacity = oldTable.length;if (oldCapacity == MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return;}// 创建新的数组  Entry<K,V>[] newTable = newTable(newCapacity);// 数据转移transfer(oldTable, newTable);table = newTable;/** If ignoring null elements and processing ref queue caused massive* shrinkage, then restore old table.  This should be rare, but avoids* unbounded expansion of garbage-filled tables.*/// 确定扩容阈值 if (size >= threshold / 2) {threshold = (int)(newCapacity * loadFactor);} else {// 清除被GC的valueexpungeStaleEntries();// 数组转移transfer(newTable, oldTable);table = oldTable;}}private void transfer(Entry<K,V>[] src, Entry<K,V>[] dest) {// 遍历原数组for (int j = 0; j < src.length; ++j) {// 取出元素Entry<K,V> e = src[j];src[j] = null;// 链式找元素while (e != null) {Entry<K,V> next = e.next;Object key = e.get();// key被回收的情况if (key == null) {e.next = null;  // Help GCe.value = null; //  "   "size--;} else {// 确定在新数组的位置int i = indexFor(e.hash, dest.length);// 插入元素 注意这里为头插法,会倒序e.next = dest[i];dest[i] = e;}e = next;}}}

过期元素(弱引用)清除

该函数的主要作用就是清除Entry的value,该Entry是在GC清除key的过程中入队的。

 private void expungeStaleEntries() {// 从队列中取出被GC的Entryfor (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);// 取出数组中的第一个元素 prev   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)// 将next直接挂在i位置table[i] = next;else// 进行截断 prev.next = next;// Must not null out e.next;// stale entries may be in use by a HashIteratore.value = null; // Help GCsize--;break;}// 更新prev和pprev = p;p = next;}}}}

当某个key失去所有强应用之后,其key对应的WeakReference对象会被放到queue里,有了queue就知道需要清理哪些Entry了。这里也是整个WeakHashMap里唯一加了同步的地方。

除了上文说的到resize中调用了expungeStaleEntries(),size()、getTable()中也调用了这个清理方法,这就意味着几乎所有其他方法都间接调用了清理。

总结

  1. WeakHashMap非同步,默认容量为16,扩容因子默认为0.75,底层数据结构为Entry数组**(数组+链表)**。
  2. WeakHashMap中的弱引用key会在下一次GC被清除,注意只会清除key,value会在每次调用expungeStaleEntries()的操作中清除。
  3. 注意:使用WeakHashMap做缓存时,如果只有它的key只有WeakHashMap本身在用,而在WeakHashMap之外没有对该key的强引用,那么GC时会回收这个key对应的entry。所以WeakHashMap不能用做主缓存,合适的用法应该是用它做二级的内存缓存,即过期缓存数据或者低频缓存数据

缺点

  • 非线程安全:关键修改方法没有提供任何同步,多线程环境下肯定会导致数据不一致的情况,所以使用时需要多注意。

  • 单纯作为Map没有HashMap好:HashMap在Jdk8做了好多优化,比如单链表在过长时会转化为红黑树,降低极端情况下的操作复杂度。但WeakHashMap没有相应的优化,有点像jdk8之前的HashMap版本。

  • 不能自定义ReferenceQueue:WeakHashMap构造方法中没法指定自定的ReferenceQueue,如果用户想用ReferenceQueue做一些额外的清理工作的话就行不通了。如果即想用WeakHashMap的功能,也想用ReferenceQueue,就得自己实现一套新的WeakHashMap了。

使用场景

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

  • 阿里Arthas:阿里开源的Java诊断工具中使用了WeakHashMap做类-字节码的缓存。

// 类-字节码缓存
private final static Map<Class<?>/*Class*/, byte[]/*bytes of Class*/> classBytesCache= new WeakHashMap<Class<?>, byte[]>();

关于作者

来自一线程序员Seven的探索与实践,持续学习迭代中~

本文已收录于我的个人博客:https://www.seven97.top

公众号:seven97,欢迎关注~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/426532.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

十八,Spring Boot 整合 MyBatis-Plus 的详细配置

十八&#xff0c;Spring Boot 整合 MyBatis-Plus 的详细配置 文章目录 十八&#xff0c;Spring Boot 整合 MyBatis-Plus 的详细配置1. MyBatis-Plus 的基本介绍2. Spring Boot 整合 MyBatis Plus 的详细配置3. Spring Boot 整合 MyBatis plus 注意事项和细节4. MyBatisx 插件的…

浅谈红外测温技术在变电站运维中的应用

0引言 随着市场经济的繁荣发展&#xff0c;社会对电力的需求持续增长。城市供电网络的规模和用电设备的总量也在不断扩大&#xff0c;这导致城市电力系统中潜在的网络安全隐患日益增多。作为电力系统核心组成部分的变压器&#xff0c;其安全、稳定的工作直接关系到电能的质量和…

总结拓展十:SAP开发计划(上)

第一节 功能开发说明书介绍 1、功能开发的基础分类 报表查询开发单据打印开发功能开发增强开发接口开发 2、屏幕元素介绍 ——程序屏幕是SAP系统与用户之间的桥梁&#xff0c;屏幕由各种不同元素布局组成 示例&#xff1a;选择屏幕界面 单选输入框 多选输入框 设定默认…

静态库 动态库

https://blog.csdn.net/mahoon411/article/details/113565482 库&#xff1a;可执行代码的二进制文件&#xff0c;里面有可以直接使用的函数&#xff0c;变量等&#xff1b;不能单独运行 因为 Linux 和 Win 的链接器、汇编器、编译器的不同&#xff0c;相同代码的库不同 Lin…

k8s介绍及部署

目录 一 Kubernetes 简介及部署方法 1.1 应用部署方式演变 1.2 容器编排应用 1.3 kubernetes 简介 1.4 K8S的设计架构 1.4.1 K8S各个组件用途 1.4.2 K8S 各组件之间的调用关系 1.4.3 K8S 的 常用名词感念 1.4.4 k8S的分层架构 二 K8S集群环境搭建 2.1 k8s中容器的管…

演示:基于WPF自绘的中国省份、城市、区县矢量地图

一、目的&#xff1a;演示一个基于WPF自绘的中国省份、城市、区县矢量地图 二、效果 国 省 市 三、功能 支持实际经纬度显示 支持平移&#xff0c;缩放等功能 显示中国地图 显示各个省份地图 显示各个省份地图&#xff08;包含在表格中&#xff0c;包含缩率图&#xff09; 显…

[数据集][目标检测]疟疾恶性疟原虫物种目标检测数据集VOC+YOLO格式948张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;948 标注数量(xml文件个数)&#xff1a;948 标注数量(txt文件个数)&#xff1a;948 标注类别…

MySQL系列—12.Undo log

1、概念 DML 操作导致数据变化 , 将变化前的记录写入 Undo 日志。 作用 用于记录更改前的一份 copy &#xff0c;在操作出错时&#xff0c;可以用于回滚、撤销还原&#xff0c;只将数据库 逻辑地恢复到原来的样子 你 插入一条记录时&#xff0c;至少要把这条记录的主键值记下来…

Elasticsearch基础(七):Logstash如何开启死信队列

文章目录 Logstash如何开启死信队列 一、确保 Elasticsearch 输出插件启用 DLQ 支持 二、配置 Logstash DLQ 设置 三、查看死信队列 四、排查 CSV 到 Elasticsearch 数据量不一致的问题 Logstash如何开启死信队列 在 Logstash 中&#xff0c;死信队列&#xff08;Dead Le…

典型BUCK电路学习和设计

手把手教你设计12V3Abuck降压电路-2-相关输入参数讲解_哔哩哔哩_bilibili 这里是输入电容&#xff0c;先过大电容&#xff08;电解电容&#xff09;再过小电容&#xff08;陶瓷贴片电容&#xff0c;高频率波&#xff09; 输出也可以同理 开关电源不能带负载的原因&#xff0c…

RocketMQ实战与集群架构详解

目录 一、MQ简介 MQ的作用主要有以下三个方面 二、RocketMQ产品特点 1、RocketMQ介绍 2、RocketMQ特点 三、RocketMQ实战 1、快速搭建RocketMQ服务 2、快速实现消息收发 1. 命令行快速实现消息收发 2. 搭建Maven客户端项目 3、搭建RocketMQ可视化管理服务 4、升级分…

MYSQL数据库基础篇——DDL

DDL&#xff1a;DDL是数据定义语言&#xff0c;用来定义数据库对象。 一.DDL操作数据库 1.查询 ①查询所有数据库 输入&#xff1b; 得到结果&#xff1a; ②查询当前数据库 输入&#xff1b; 例如执行下面语句&#xff1a; 2.创建 输入 然后展示数据库即可得到结果&…

linux第二课(docker的安装使用)

目录 一.关于docker (1)背景引入 (2)docker介绍 (3)功能 (4)Docker架构 二.docker的安装及相关的命令 (1)docker的安装 (2)docker的配置 (3)docker镜像命令 (4)容器命令 三.docker安装myaql ​编辑 四.数据卷挂载 1.数据卷挂载引入 2.数据卷挂载图解 3.数据卷的安装…

排序----数据结构

Comparable Integer Double 默认情况下都是按照升序排列的 string 按照字母再ASCII码表中对应的数字升序进行排列 冒泡排序 选择排序

实战16-RVP定义完成适配

新增文件 //设计搞总宽度 const DRAFT_WIDTH 360//将元素的设计搞大小转化为真机中的大小 export default function rvp(val: number) {/*计算元素真正的大小&#xff1b;* 元素在设计稿的大小 / 设计搞总宽度 x / 真机宽度 (保证元素在不同设备占比相同)x 元素在设计稿的大…

[数据集][目标检测]岩石种类检测数据集VOC+YOLO格式4766张9类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;4766 标注数量(xml文件个数)&#xff1a;4766 标注数量(txt文件个数)&#xff1a;4766 标注…

Cpp类和对象(上)(3)

文章目录 前言一、面向过程与面向对象初步认识二、类的引入三、类的定义四、类的访问限定符及类的封装类的访问限定符类的封装 五、类的作用域(类域)六、类的实例化七、类对象模型如何计算类对象的大小类对象的存储方式猜测 八、this指针this指针的引出this指针的特性 九、C语言…

实习项目|苍穹外卖|day11

Apache ECharts 前端技术。 营业额统计 还是比较简单的。 用户统计 订单统计 以上所有需求。难点在于对时间类的处理&#xff1a; // 接收格式 GetMapping("/turnoverStatistics")ApiOperation("营业额统计")public Result<TurnoverReportVO>…

二叉搜索树(Java实现)

博主主页: 码农派大星. 数据结构专栏:Java数据结构 数据库专栏:MySQL数据库 JavaEE专栏:JavaEE 关注博主带你了解更多数据结构知识 目录 1.概念 2.实现二叉搜索树 定义节点 查找元素 插入元素 删除元素 1.概念 二叉搜索树又称二叉排序树,或者它是一棵空树,或者是具有…

镀金引线---

一、沉金和镀金 沉金和镀金都是常见的PCB金手指处理方式&#xff0c;它们各有优劣势&#xff0c;选择哪种方式取决于具体的应用需求和预算。 沉金&#xff08;ENIG&#xff09;是一种常用的金手指处理方式&#xff0c;它通过在金手指表面沉积一层金层来提高接触性能和耐腐蚀性…