也是几年前写的,在内部邮件列表里发过,在这里保存一下。

看到了这篇帖子: 《WeakHashMap的神话》http://www.javaeye.com/topic/587995

因为Javaeye回帖还要先做个论坛小测验,所以懒得在上面回复了,在这里说下。

以前设计缓存时也曾过用WeakHashMap来实现,对Java的Reference稍做过一些了解,其实这个问题,归根到底,是个Java
GC
的问题,由垃圾回收器与ReferenceQueue的交互方式决定的。WeakHashMap的实现也是通过ReferenceQueue这个“监听器”来优雅的实现自动删除那些引用不可达的key的。

先看看ReferenceQueue在Java中的描述:

Reference queues, to which registered reference objects are appended by the garbage collector after the appropriate reachability changes are detected. 

中文JavaDoc的描述:引用队列,在检测到适当的可到达性更改后,垃圾回收器将已注册的引用对象添加到该队列中

查看源代码会发现它很简单,实现了一个队列的入队(enqueue)和出队(poll还有remove)操作,内部元素就是泛型的Reference,并且Queue的实现,是由Reference自身的链表结构所实现的。

再来看 Reference类的代码,注意,javadoc中有一句,提到了它与GC是紧密相关的:

Because reference objects are implemented in close cooperation with the garbage collector, this class may not be subclassed directly.

从数据结构上看,Reference链表结构内部主要的成员有

private T referent; //就是它所指引的
Reference next; //指向下一个;

另一个比较重要的内部数据是:

ReferenceQueue<? super T> queue;

这个queue是通过构造函数传入的,表示创建一个Reference时,要将其注册到那个queue上。

Queue的另一个作用是可以区分不同状态的Reference。Reference有4种状态,不同状态的reference其queue也不同:

  1. Active:

     queue = ReferenceQueue with which instance is registered,
    or ReferenceQueue.NULL if it was not registered with a queue; next = null.
  2. Pending:

     queue = ReferenceQueue with which instance is registered;
    next = Following instance in queue, or this if at end of list.
  3. Enqueued:

     queue = ReferenceQueue.ENQUEUED; next = Following instance
    in queue, or this if at end of list.
  4. Inactive:

     queue = ReferenceQueue.NULL; next = this.

那么,当我们创建了一个WeakReference,并且将其referent改变后,究竟发生了什么?先看一段代码:

// eg1
public static void test() throws Exception{
Object o = new Object();
// 默认的构造函数,会使用ReferenceQueue.NULL 作为queue
WeakReference<Object> wr = new WeakReference<Object>(o);
System.out.println(wr.get() == null);
o = null;
System.gc();
System.out.println(wr.get() == null);
}

结果大家都知道,但其内部是怎么实现的,还需重新看Reference的源码,内部有两点需要注意:

1)pending和 discovered成员:

先看pending对象

/* List of References waiting to be enqueued.  The collector adds
* References to this list, while the Reference-handler thread removes
* them. This list is protected by the above lock object.
*/
private static Reference pending = null; //这个对象,定义为private,并且全局没有任何给它赋值的地方,
//根据它上面的注释,我们了解到这个变量是和垃圾回收期打交道的。

再看discovered,同样为private,上下文也没有任何地方使用它

transient private Reference<T> discovered;    /* used by VM */
//看到了它的注释也明确写着是给VM用的。

上面两个变量对应在VM中的调用,可以参考openjdk中的hotspot源码,在hotspot/src/share/vm/memory/referenceProcessor.cpp 的ReferenceProcessor::discover_reference 方法。(根据此方法的注释由了解到虚拟机在对Reference的处理有ReferenceBasedDiscoveryRefeferentBasedDiscovery两种策略)

2)ReferenceHandler线程

这个线程在Reference类的static构造块中启动,并且被设置为高优先级和daemon状态。此线程要做的事情,是不断的检查pending 是否为null,如果pending不为null,则将pending进行enqueue,否则线程进入wait状态。

通过这2点,我们来看整个过程:

pending是由jvm来赋值的,当Reference内部的referent对象的可达状态改变时,jvm会将Reference对象放入pending链表。

结合代码eg1中的 o = null; 这一句,它使得o对象满足垃圾回收的条件,并且在后边显式的调用了System.gc(),垃圾收集进行的时候会标记WeakReference所referent的对象o为不可达(使得wr.get()==null),并且通过
赋值给pending,触发ReferenceHandler线程处理pending

ReferenceHandler线程要做的是将pending对象enqueue,但默认我们所提供的queue,也就是从构造函数传入的是null,实际是使用了ReferenceQueue.NULLHandler线程判断queue为ReferenceQueue.NULL则不进行操作,只有非ReferenceQueue.NULL的queue才会将Reference进行enqueue。

ReferenceQueue.NULL相当于我们提供了一个空的Queue去监听垃圾回收器给我们的反馈,并且对这种反馈不做任何处理。要处理反馈,则必须要提供一个非ReferenceQueue.NULL的queue。

WeakHashMap则在内部提供了一个非NULL的ReferenceQueue

private final ReferenceQueue<K> queue = new ReferenceQueue<K>();

在 WeakHashMap 添加一个元素时,会使用 此queue来做监听器。见put方法中的下面一句:

    tab[i] = new Entry<K,V>(k, value, queue, h, e);

这里Entry是一个内部类,继承了WeakReference

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

WeakHashMap的 put, size, clear 都会间接或直接的调用到 expungeStaleEntries()方法。

expungeStaleEntries顾名思义,此方法的作用就是将 queue中陈旧的Reference进行删除,因为其内部的referent都已经不可达了。所以也将这个WeakReference包装的key从map中删除。

个人认为:ReferenceQueue是作为 JVM GC与上层Reference对象管理之间的一个消息传递方式,它使得我们可以对所监听的对象引用可达发生变化时做一些处理,WeakHashMap正是利用此来实现的。用图来大致表示如下:

现在,我们再回到那个帖子的问题:http://www.javaeye.com/topic/587995

他开始的测试写法为:

List<WeakHashMap<byte[][], byte[][]>> maps = new ArrayList<WeakHashMap<byte[][], byte[][]>>();
for (int i = 0; i < 1000; i++) {
WeakHashMap<byte[][], byte[][]> d = new WeakHashMap<byte[][], byte[][]>();
d.put(new byte[1000][1000], new byte[1000][1000]);
maps.add(d);
System.gc();
System.err.println(i);
}

会造成OOM异常。

注意一下,他在for循环里每次都 new 一个新的WeakHashMap,并且key和value都是大对象,之后,他在 for循环的最后增加了一句访问 WeakHashMap的size(),使得不会造成OOM。

首先上面的代码并不是没有执行GC,而是仅对 WeakHashMap中的key中的byte数组进行了回收,而value依然保持。我们可以先做个试验,把上面的value用小对象代替

for (int i = 0; i < 10000; i++) {
WeakHashMap<byte[][], Object> d = new WeakHashMap<byte[][], Object>();
d.put(new byte[1000][1000], new Object());
maps.add(d);
System.gc();
System.err.println(i);
}

上面的代码,即使执行10000次也没有问题,证明key中的byte数组确实被回收了。

那为何key中的referent的数据被GC,却没有触发WeakHashMap去做清除整个key的操作呢?

因为他for循环中每次都new一个新的WeakHashMap,在put操作后,虽然GC将WeakReference的key中的byte数组回收了,并将事件通知到了ReferenceQueue,但后续却没有相应的动作去触发 WeakHashMap 去处理 ReferenceQueue,所以 WeakReference 包装的key依然存在在WeakHashMap中,其对应的value也当然存在。

而在for循环的尾巴增加了一句 d.size()方法,却可以了,是因为

size()里面触发了expungeStaleEntries 操作,它将
ReferenceQueue中的 WeakReference对象从map中删除了,对应着value也一并删除了,使得value也被GC回收了。

话说ReferenceQueue的更多相关文章

  1. Java Reference & ReferenceQueue一览

    Overview The java.lang.ref package provides more flexible types of references than are otherwise ava ...

  2. java强引用、软引用、弱引用、虚引用

    前言概述 在JDK1.2以前的版本中,当一个对象不被任何变量引用,那么程序就无法再使用这个对象.这就像在日常生活中,从商店购买了某样物品后,如果有用,就一直保留它,否则就把它扔到垃圾箱,由清洁工人收走 ...

  3. 话说C++中的左值、纯右值、将亡值

    写在前面 C++中有“左值”.“右值”的概念,C++11以后,又有了“左值”.“纯右值”.“将亡值”的概念.关于这些概念,许多资料上都有介绍,本文在拾人牙慧的基础上又加入了一些自己的一些理解,同时提出 ...

  4. ReferenceQueue<T>随笔

    参考资料: ReferenceQueue食用手册 java引用食用手册 ReferenceQueue源代码里面很好的展示了java队列的实现思路, 以及多线程观察者的实现思路 多线程观察者实现思路: ...

  5. Java魔法堂:四种引用类型、ReferenceQueue和WeakHashMap

    一.前言 JDK1.2以前只提供一种引用类型——强引用 Object obj = new Object(); .而JDK1.2后我们多另外的三个选择分别是软引用 java.lang.ref.SoftR ...

  6. ReferenceQueue的使用

    转:http://www.iflym.com/index.php/java-programe/201407140001.html 1 何为ReferenceQueue 在java的引用体系中,存在着强 ...

  7. 强、软、弱、虚引用,ReferenceQueue,WeakHashMap

    强引用(Reference):所谓强引用就是普通引用.普通引用引用的对象,即使内存不足时,一般情况下也不会被回收. 软引用(weakReference):如果对象被且仅被软引用所引用时,内存不足时,会 ...

  8. 话说C语言const用法

    const在C语言中算是一个比较新的描述符,我们称之为常量修饰符,意即其所修饰 的对象为常量(immutable). 我们来分情况看语法上它该如何被使用. 1.函数体内修饰局部变量. 例: void ...

  9. 团体程序设计天梯赛-练习集L1-021. 重要的话说三遍

    L1-021. 重要的话说三遍 时间限制 400 ms 内存限制 65536 kB 代码长度限制 8000 B 判题程序 Standard 作者 陈越 这道超级简单的题目没有任何输入. 你只需要把这句 ...

  10. Java对象的强、软、弱和虚引用原理+结合ReferenceQueue对象构造Java对象的高速缓存器

    //转 http://blog.csdn.net/lyfi01/article/details/6415726 1.Java对象的强.软.弱和虚引用 在JDK 1.2以前的版本中,若一个对象不被任何变 ...

随机推荐

  1. EasyDarwin,EasyNVR, EasyNVS, EasyDSS...这些都是啥

    EasyDarwin 是开源的基础版本, 其他都是衍生产品(http://www.easydarwin.org/) EasyNVR, 就是视频监控录像机,可以查看实时监控和保存监控视频, 如下图, ( ...

  2. Argo CD初体验

    什么是 Argo CD? Argo CD 是一个声明式的 GitOps 持续交付工具,用于 Kubernetes 集群.它通过持续监控 Git 仓库中的 Kubernetes 资源配置文件,将这些配置 ...

  3. qumu虚拟机启动后无法远程连接

    通过 virsh 在启动 qemu 虚拟机,可以通过 VNC 访问虚拟机,但无法通过设置的 SSH 的外部映射端口登录.首先在宿主机上查看虚拟机(csv\tpm\name)的网络配置,可以看到端口映射 ...

  4. ASP.NET Core – 操作 Uri 和 Query

    前言 以前就有写过了 Asp.net core 学习笔记 (操作 URL 和 Query), 但很乱, 这篇作为整理. Uri 介绍 结构: [Scheme]://[Host]:[Port][/Pat ...

  5. js 翻译 c# 注意事项

    1. split('') 在 c# 是不可以 .Split("") 的 要写 ToCharArray() 更新: 2021-09-25, split('') 是不好的写法, es6 ...

  6. 月薪20k以上的软件测试工程师的必备知识点?全部拿走吧!

    我们都知道作为一个软件测试工程师,入门相对比较简单,但是要达到技术精通,甚至薪资能达到20k以上的话,那绝对需要对测试开发有一个系统的了解,以及对这些系统的知识能够熟练掌握. 今天的话是我从阿里以为做 ...

  7. Windows NoiLinux

    在 Windows 下使用 NoiLinux ubuntu-noi-v2.0.iso 下载 ubuntu-noi-v2.0.iso 打开 VMWare,创建新的虚拟机 -> 自定义(高级)-&g ...

  8. 智慧矿山IT智能运维自动化解决方案

    矿山企业是国民经济中的重要组成部分,其资源开发和产业链条中涉及的环节与过程非常繁琐和复杂.随着我国矿山企业规模逐年扩大,为了提高其生产效率和降低其生产成本,信息化.数字化建设成为当下矿山企业发展的重要 ...

  9. Thinkphp原生验证码的使用

    Thinkphp原生验证码的使用 一. 获取验证码 public function verifyCode(){ $captcha = new \think\captcha\Captcha(); $ca ...

  10. 前端工程化解决方案webpack使用小结

    前端工程化解决方案webpack,模块化.组件化.规范化.自动化,使得前端开发更加高效. 功能:代码压缩混淆.处理浏览器端js的兼容性.以模块化的方式处理项目中的资源 webpack插件:clean- ...