import java.util.ArrayList;
import java.util.List;
import java.util.WeakHashMap; public class TestWeakHashMap {
public static void main(String[] args) throws Exception {
// createDoNothing();
createSomething();
} /**
* 由于Java内存是64M~256M不等,所以再不改变内存参数的情况下,该测试跑一段后就内存溢出了。果不其然,WeakHashMap这个时候并没有自动帮我们释放不用的内存。
*/
private static void createDoNothing() {
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);
}
} /**
* 总结来说:WeakHashMap并不是你啥也干他就能自动释放内部不用的对象的,而是在你访问它的内容的时候释放内部不用的对象。这两句话看似区别不大,但是有时候一个小小的区别就会要了命的。
*/
private static void createSomething() {
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); for (int j = 0; j < i; j++) {
System.err.println(j+ " size" + maps.get(j).size());
}
}
}
}

http://hongjiang.info/java-referencequeue/

看到了这篇帖子: 《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回收了。

WeakHashMap回收时机的更多相关文章

  1. WeakHashMap回收时机结合JVM 虚拟机GC的一些理解

    一直很想知道WeakHashMap的使用场景,想来想去只能用在高速缓存中,而且缓存的数据还不是特别重要,因为key(key不存在被引用的时候)随时会被回收 所以研究了一下WeakHashMap的回收时 ...

  2. java-初识引用分类及Map实现类WeakHashMap

    1.同样的,话不多讲直接上代码 (1)认识了解下引用分类及其作用 package com.otherMapProduce; import java.lang.ref.WeakReference; /* ...

  3. JVM 专题十九:垃圾回收(三)垃圾回收相关概念

    1. System.gc()的理解 在默认情況下,通过System.gc()或者Runtime. getRuntime().gc()的调用,会显式触发Full GC,同时对老年代和新生代进行回收,尝试 ...

  4. JVM强引用、软引用、弱引用、虚引用、终结器引用垃圾回收行为总结

    JVM引用 我们希望能描述这样一类对象: 当内存空间还足够时,则能保留在内存中:如果内存空间在进行垃圾收集后还是很紧张,则可以抛弃这些对象. -[既偏门又非常高频的面试题]强引用.软引用.弱引用.虚引 ...

  5. 第 16 章 【硬核!】 垃圾回收相关 GC细讲

    第 16 章 垃圾回收相关概念 1.System.gc() 的理解 1.1.System.gc() 方法 System.gc() 方法 在默认情况下,通过System.gc()者Runtime.get ...

  6. 定时Job在IIS中潜在危险-IIS 定期回收

    引言 有时我们会在IIS中启用一些定时服务,但是你必须清楚IIS会定期回收Asp.net的应用程序的.首先来看IIS啥时候回收APPDomain.   APPDomain 回收时机 There are ...

  7. 菜鸟之旅——.NET垃圾回收机制

    .NET的垃圾回收机制是一个非常强大的功能,尽管我们很少主动使用,但它一直在默默的在后台运行,我们仍需要意识到它的存在,了解它,做出更高效的.NET应用程序:下面我分享一下我对于垃圾回收机制(GC)的 ...

  8. 关于JVM的垃圾回收(GC) 这可能是你想了解的

    目录 1 JVM中Java对象的分类 2 JVM的GC类型及触发条件 2.1 Young GC 2.2 Full GC 3 Java对象生成时的内存申请过程 3 Oracle JDK中的垃圾收集器 3 ...

  9. 白话说java gc垃圾回收

    gc是java区别于其他好几门语言(c/c++)的一个代表功能(当然也有很多可以自动管理内存的语言,如所有的脚本语言,你根本不知道内存管理这回事)! 当然,之所以要把c/c++和java相比,是因为j ...

随机推荐

  1. 【转】Oracle AWR 配置查看

    源地址:http://blog.sina.com.cn/s/blog_439628be0101d7l3.html

  2. SOCKET 服务器停止服务时清理连接操作

    刚想到一个情况,正好是工作中遇到的. 当服务器停止时,正好有连接的请求正在被处理,而此时服务端需要等待该请求被正常的处理完,然后给客户端发送响应报文. 正常情况下,服务器停止的时候,是希望发送了响应报 ...

  3. ASCIITable: 演示 Arduino 串口输出的进阶功能

    原文地址 - https://www.arduino.cc/en/Tutorial/ASCIITable ASCII字符表 本例展示了高级的串口打印功能,通过本功能可以在Arduino软件(IDE)的 ...

  4. 项目中用到RouteTable,发布到IIS7中无法访问

    项目中用到RouteTable,发布到IIS7中,访问之后没有任何反应,google半天终于找到了解决方法,就是要把iis的“HTTP重定向”功能打开

  5. nginx日志分割脚本

    [root@localhost nginx]# cat logs/nginx.pid 5118[root@localhost nginx]# kill -QUIT 5118-QUIT : 关闭进程-H ...

  6. CRLF和LF

    协作项目,开发环境不同(mac,window)构建过程中,命令行报错(expecting LF but only find CRLF) 打开git bash,输入 $ git config --glo ...

  7. openlayers

    很久没有写东西了,最近突然想看看地图,就翻看了下,用了2-3周时间看看网页,学习做了下:先看做的效果:

  8. 十、DG

    1.DG是dependency graph,我认为是依赖图,和DAG(有向无环图)的区别:DAG用来做场景管理,每个node代表的不是tranform就是shape,有父子关系. DG是代表了每个实体 ...

  9. jquery ajax success 函数 异步调用方法中不能给全局变量赋值的原因及解决办法

    jquery ajax success 函数 异步调用方法中不能给全局变量赋值的原因及解决办法   在调用一个jquery的ajax方法时我们有时会需要该方法返回一个值或者给某个全局变量赋值,可是我们 ...

  10. mvc4 分离Controller 出现 未找到路径“/”的控制器或该控制器未实现 IController

    一般MVC项目都会把Controller 分离出来独立类库,以前用mvc3一直这样做,测试发布都能够正常运行,这次用了mvc4,发现会报错:HTTP 404.您正在查找的资源(或者它的一个依赖项)可能 ...