一、概述

  为了更好的理解WeakHashMap的原理,我们有必要先来了解一下WeakReference的作用及实现原理。Java中有一个专门的包java.lang.ref,里面定义了我们通常所说的几种引用,具体来说如下:

Reference: 基础的引用类,是一个抽象类,定义了引用的一些基本方法

SoftReference: 软引用,软引用对象在应用出现OOM之前会被回收。

WeakReference: 弱引用,如果一个对象只被弱引用关联,则垃圾回收器会回收它。

PhantomReference:幻引用,这类用得比较少,个人也不太理解,应该是属于引用级别最弱的引用。

  ReferenceQueue: 引用队列,垃圾回收器会将已回收的队象放到这个队列里。

除了上述的三类引用之外,还有一类引用叫做强引用,强引用就是我们通常所说的引用,所以这里java并没有单独定义一个引用类来表示。

本文的重点是介绍WeakReference的使用,及其实现原理。

二、WeakReference的使用示例

  在介绍其实现原理前,我们先来看一下它的使用效果,根据WeakReference的定义, 如果一个对象只被弱引用所引用,那么这个对象就是弱可达的,弱可达对象会在系统进行垃圾回收时被回收。

  可能这样说还是不好理解,所以我们以一个示例来说明其使用的效果,代码如下:

public class TestWeakReference {
public static void main(String[] args) throws Exception{ UserInfo userInfo = new UserInfo("Jim Green");
UserInfo anotherUser = userInfo;
WeakReference<UserInfo> weakUser = new WeakReference<>(userInfo); System.out.println("\nBefore userInfo is null");
System.out.println("strong ref:" + anotherUser);
System.out.println("weak ref:" + weakUser.get()); userInfo = null;
System.gc(); System.out.println("\nAfter userInfo is null");
System.out.println("strong ref:" + anotherUser);
System.out.println("weak ref:" + weakUser.get()); anotherUser = new UserInfo("Jim White");
System.gc();
System.out.println("\nAfter anotherUser is changed");
System.out.println("strong ref:" + anotherUser);
System.out.println("weak ref:" + weakUser.get()); }
} class UserInfo {
private String name; public UserInfo(String name){
this.name = name;
} @Override
public String toString() {
return "Name is " + name;
}
}

上述代码的运行结果如下:

从上面的结果可以看到,刚开始,这三个引用都指向同一个用户对象Jim Green, 然后我们将原引用置为null,进行垃圾回收,但由于还有一个强引用指向这个Jim Green, 所以这个用户对象不会被回收,最后,当没有强引用再指向它了,再做垃圾回收,则Jim Green被回收了。

这个结果也印证了WeakReference的作用,如果其指向的对象没有被任何强引用指向,则该对象是可以回收的。

下面我们再以图例的形式来演示这个过程。

三、WeakReference 的实现原理

  上面我们详细演示了使用的方式,接下来我们再说一下其主要实现。

1. UML图

WeakReference , Reference和ReferenceQueue三者之间的关系如下:

  上图中列出了这三个类的一些主要的属性和方法。其中WeakReference是抽象类,但是提供了一个引用所需要的基本功能,而WeakReference则只是简单继承,并没有实现任何扩展的功能。

  Reference有两个构造的方法,分别是带队列和不带队列的,如下:

    Reference(T referent) {
this(referent, null);
} Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

  可见,referent属性存储了其所引用的对象,而queue这个字段是可选的,前面说到,queue的作用保存对象将被回收的引用,由垃圾回收器负责往里面添加,但如果不提供,则没有这一过程。

  2. 状态说明:

  根据类的说明,引用有四种状态:

  Active: 这是创建该引用后的初始状态,如果该引用对象所引用的对象的可达性发生了变化,则引用本身的状态将变为Pending或Inactive,取决于这个引用在创建时是否使用了队列。如果使用了队列,则状态将会转化为Pending,否则直接变为Inactive。

  Pending: 由Active转化而来,处于该状态的引用在一个公共的pending队列里,等待被添加到queue里去。对于pending队列的处理由一个优先级比较高的守护线程实时监控处理。如果该引用在初始化时没有使用queue,则不会被加到pending队列里,当然也不可能添加到queue里。

  Enqueued: 由Pending转化而来,如果pending列表中的引用被正确添加到了queue里,则引用的状态为enqueued,如果该引用被从queue队列里移除了,则其将变为InActive状态。同样,这个前提就是构造时需要指定queue.

  InActive: 最后的状态,处于这个状态的引用对象实际上已经没什么作用了,状态也不会发生任何改变。

  这四种状态只是一种说明,实际上Reference对象并没有任何的status字段,不过作为队列中的节点,它有一个next字段,当状态为Active时,其next为null,而当其为其它状态时,next一定不为null,而是指向队列中的下一个引用,如果其本身就是队列中的最后一个元素,则next指向其自身。

  3. 原理说明:

  在介绍了结构和状态说明后,我们再来对其实现进行分析,这个需要分两种情况:

  1)构造时没有使用queue:

  这种情况比较简单,状态转换只有Active和InActive,不涉及到队列的操作,当引用所指向的对象没有任何其它的强引用时,垃圾回收器将会回收该对象,而其状态也应该会变为InActive.值得注意的时,这样get()就会直接返回null了。

  2)构造函数使用了queue:

  这种情况就复杂了,当引用指向的对象没有其它的强引用时,垃圾回收器会先将其添加到pending队列里,而Reference会通过一个公共的守护线程来处理pending队列里的引用对象,将其添加到queue队列中去。

  这个过程的处理逻辑如下:

 private static class ReferenceHandler extends Thread {

         ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
} public void run() {
for (;;) { Reference r;
synchronized (lock) {
if (pending != null) {
r = pending; //取一个元素
Reference rn = r.next;//找队列中下一个引用对象
pending = (rn == r) ? null : rn; //如果是最后一个元素,则pending队列置空
r.next = r; //改变其next
} else {
try {
lock.wait(); //等待
} catch (InterruptedException x) { }
continue;
}
} // Fast path for cleaners
if (r instanceof Cleaner) {
((Cleaner)r).clean();
continue;
} ReferenceQueue q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);//调用队列中的入队方法
}
}
}

  上面的方法会一直处理pending队列直到为null,之后将处于wait状态,那么问题来了,当pending队列再次不为空时,这个线程需要被唤醒。往pending队列里加引用对象,并执行唤醒操作的工作是谁来完成的呢?答案是由垃圾回收器在回收引用指向的对象时来调用的

  所以说,是垃圾回收器完成了引用对象从Active到Pending的转换,而引用对象的线程完成了引用对象由Pending到Enqueued的转换。

  接下来我们再了解下ReferenceQueue中入队的处理过程:

 boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
synchronized (r) {
if (r.queue == ENQUEUED) return false;
synchronized (lock) {
r.queue = ENQUEUED;
r.next = (head == null) ? r : head;
head = r;
queueLength++;
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(1);
}
lock.notifyAll();
return true;
}
}
}

 从其实现逻辑可以看出,每次入队是从头入队,入队后,更新其queue属性,这样可以防止多次入队。

对应还有出队的功能,这个就不再分析了。

  queue的入队我们介绍完了,但是垃圾回收器不会对这个队列做出队操作,那么这个队列有什么用呢?JDK中有一段对其的描述如下:

  “在创建引用对象时,通过向 引用队列 注册 一个适当的引用对象,程序可以请求在对象可到达性更改时获得通知。在垃圾回收器确定引用的可到达性已经更改为对应于引用类型的值之后的某一时间,它会将引用添加到相关的队列中。此时,该引用被认为是 已加入队列的。通过轮询或阻塞,直到获得了引用,程序才可以从队列中移除引用。引用队列是通过 ReferenceQueue 类实现的。”

  所以,这个queue是提供给应用程序通知用的,也就是说,程序可以通过监听这个队列,来获悉哪些弱引用所指向的对象已经被回收了,进而程序可以做相应的处理。至于如何监控,可以参考Reference类中对于pending队列的处理方式。

四、总结

  至此,我们对Reference及ReferenceQueue的实现方式做了一个完整的介绍,下面再总结一下:

  1. WeakReference在创建时需要关联到另一个对象,如果该对象没有别的普通(强)引用,则该对象将会被垃圾回收器回收。

  2. Reference在定义时可以指定一个类型为ReferenceQueue的队列,该队列的作用则是存储那些关联对象已经被回收了的Reference对象,以供应用程序监听,但如何处理由应用程序自己决定。

  3. WeakReference继承于Reference,但自身并未提供扩展的功能。

基础-WeakReference的更多相关文章

  1. java基础回顾(六)——WeakReference、SoftReference

    在Java里, 当一个对象o被创建时, 它被放在Heap里. 当GC运行的时候, 如果发现没有任何引用指向o, o就会被回收以腾出内存空间. 或者换句话说, 一个对象被回收, 必须满足两个条件: 1) ...

  2. C#基础知识回顾:1.由WeakReference想到对象的创建与销毁

    .Net Framework中,把资源分为托管资源和非托管资源两大类, 托管资源指可以通过.Net Frame垃圾回收器进行回收的资源,主要是指分配在托管堆上你的内存资源,这类资源的回收是不需要人工干 ...

  3. Java基础加强之多线程篇(线程创建与终止、互斥、通信、本地变量)

    线程创建与终止 线程创建 Thread类与Runnable接口的关系 public interface Runnable { public abstract void run(); } public ...

  4. JAVA基础整理-集合篇(一)

    集合作为JAVA的基础知识,本来感觉自己理解的很清楚了,但是在最近的一次面试中还是答得不尽如人意!再次做一下整理,以便加深理解以及随时查阅. 首先,java.util包中三个重要的接口及特点:List ...

  5. java基础语法要点<一>(基于1.8)

    http://yishouce.com/java/run http://www.shucunwang.com/RunCode/java/ 数据类型 8种基本数据类型及对应的 类型封装器 byte, s ...

  6. 基础4 Android基础

    基础4 Android基础 1. Activity与Fragment的生命周期. Activity生命周期 打开应用 onCreate()->onStart()->onResume 按BA ...

  7. 基础1 JavaSe基础

    JavaSe基础 1. 九种基本数据类型的大小,以及他们的封装类 boolean 无明确指定 Boolean char 16bits Character byte 8bits Byte short 1 ...

  8. JVM基础(5)-垃圾回收机制

    一.对象引用的类型 Java 中的垃圾回收一般是在 Java 堆中进行,因为堆中几乎存放了 Java 中所有的对象实例.谈到 Java 堆中的垃圾回收,自然要谈到引用.在 JDK1.2 之前,Java ...

  9. java基础解析系列(七)---ThreadLocal原理分析

    java基础解析系列(七)---ThreadLocal原理分析 目录 java基础解析系列(一)---String.StringBuffer.StringBuilder java基础解析系列(二)-- ...

随机推荐

  1. Android开发学习之路-下拉刷新以及GridView的使用

    GridView是类似于ListView的控件,只是GridView可以使用多个列来呈现内容,而ListView是以行为单位,所以用法上是差不多的. 主布局文件,因为要做下拉刷新,所以加了一个Prog ...

  2. Atitit 知识图谱的数据来源

    Atitit 知识图谱的数据来源   2. 知识图谱的数据来源1 a) 百科类数据2 b) 结构化数据3 c) 半结构化数据挖掘AVP (垂直站点爬虫)3 d) 通过搜索日志(query record ...

  3. 每天一个linux命令(41):ps命令

    Linux中的ps命令是Process Status的缩写.ps命令用来列出系统中当前运行的那些进程.ps命令列出的是当前那些进程的快照,就是执行ps命令的那个时刻的那些进程,如果想要动态的显示进程信 ...

  4. python数据操作方法封装

    工作中经常会用到数据的插叙.单条数据插入和批量数据插入,以下是本人封装的一个类,推荐给各位: #!/usr/bin/env python # -*- coding:utf-8 -*- # Author ...

  5. Redis基础介绍及安装示例

    1.基本概念 Redis是由Salvatore Sanfilippo(意大利)开发的一个开源的高性能键值存储数据库,于2009年发布第一个版本并与同一年开源,官方站点:http://www.redis ...

  6. WCF传输1-你是否使用过压缩或Json序列化?

    1.当遇到需要传输大量数据时,怎么样传输数据? 2.压缩数据有哪几种常见的方式? 问题1解答:通过压缩来传输数据 问题2解答: (1)WCF自带的压缩方式 (2)自定义WCF binding进行压缩 ...

  7. Linux 启动过程分析

    本文仅简单介绍Linux的启动过程,在此基础上做简要的分析.对于Linux启动过程中内部详细的函数调用不做介绍,只是希望本文能给新手起到一个抛砖引玉的作用,以便深入研究Linux的启动过程.下图基本展 ...

  8. 170多个Ionic Framework学习资源(转载)

    在Ionic官网找到的学习资源:http://blog.ionic.io/learning-ionic-in-your-living-room/ 网上的文章比较多,但是很多时候我们很难找到自己需要的. ...

  9. Android学习笔记之短信验证码的获取和读取

    PS:最近很多事情都拖拖拉拉的..都什么办事效率啊!!! 还得吐槽一下移动运营商,验证码超过五次的时候,直接把我的手机号封闭.真是受够了. 学习笔记: 1.Android之如何获取短信验证码. 2.如 ...

  10. Android数据库表的创建和数据升级操作

    之前的文章有提到,可以在xml文件中配置数据库信息:http://www.cnblogs.com/wenjiang/p/4492303.html,现在就讲如何利用这些信息类构建数据库. xml文件大概 ...