设计WeakReference的那段日子

当你遇到要开发一个缓存,并且是短期内就过期的那种缓存的需求?你会怎么实现呢?

Mark Reinhold看着1.1版的Java代码沉思着,最近社区传来1.1版本的一些问题,尼玛生活不容易啊。当初为了让开发者更轻松的开发代码,我们设计了垃圾回收,让开发不用管这些事情。现在可倒好,方便倒是方便了,不够灵活的问题又来了。这真是人类的终极难题啊。又要便宜又要好货!!!!!

开发者们有这样的需求,说他们要开发一个缓存组件。希望在map中的数据定期的被回收,而不至于造成内存泄露。

这在1.1中并没有这样的能力。如果要实现这样的功能。只能在java code层面来处理。寄希望于垃圾回收器是无法实现的。

他一边抱怨,一边看着墙上“为人民服务”,心想还是要时刻为开发者们服务,于是他开始琢磨,可不可以提供一种更加灵活的回收策略。

现在的做法是当一个对象不被任何变量引用时,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及状态,程序才能使用它。这就像在日常生活中,从商店购买了某样物品后,如果有用,就一直保留它,否则就把它扔到垃圾箱,由清洁工人收走。

这时候如果你再想把垃圾箱中的东西捡回来,那是没门了。

但有时候情况并不这么简单,你可能会遇到类似鸡肋一样的物品,食之无味,弃之可惜。这种物品现在已经无用了,保留它会占空间,但是立刻扔掉它也不划算,因 为也许将来还会派用场。对于这样的可有可无的物品,一种折衷的处理办法是:如果家里空间足够,就先把它保留在家里,如果家里空间不够,即使把家里所有的垃 圾清除,还是无法容纳那些必不可少的生活用品,那么再扔掉这些可有可无的物品。

Mark想到了这些生活场面,一下子豁然开朗。他想着,那就把对象的引用分成几个级别。

现在这种引用是一种,就叫强引用。然后再来个软引用、弱引用、虚引用。

这四种引用级别由高到低。这样回收器到时候根据四种情况,做出对应的回收策略。 这样总比只有强引用要好。我看看社区还能说什么!

mark果断的写下了下列代码:

public abstract class Reference<T> {

对就叫引用吧。先搞个引用类。

我们把真正的对象传入进来吧。

private T referent;  

这个字段专门由垃圾回收器来处理。垃圾回收器根据引用类型决定是否回收该对象。比如weakreference,也就是弱引用,那么每次垃圾回收时,就会把该对象设置为null。

众多的弱引用类型应该找个地方存储起来。那么怎么存储呢?

这里我们选择了一个队列来存储。自己定义了一个reference queue,具体会在以后的文章中说明。

ReferenceQueue<? super T> queue;

那么垃圾回收器是怎么把引用们加入到队列中的呢?这里我们给回收器一个字段,回收器把要加入队列的字段设置在这个字段上:

private static Reference pending = null;

回收器把要入队的字段赋值给pending后,那么入队的事情由谁来做呢?

现在是不是要设计一个线程handler呢?然后我们启动这个线程,无限循环来入队?

那就搞个handler吧。

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;
                    r.next = r;
                } 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);
        }
    }
}

引用处理器负责把引用们加入队列。

那么 怎么启动这个线程呢?总不能让开发者自己启动吧。还是自己启动吧。

static {
    ThreadGroup tg = Thread.currentThread().getThreadGroup();
    for (ThreadGroup tgn = tg;
         tgn != null;
         tg = tgn, tgn = tg.getParent());
    Thread 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();
}

既然把一个对象包装成了引用类型,比如弱引用,那应该要给提供获取对象的方法:

public T get() {
    return this.referent;
}

上面这个get方法,当引用对象被程序或者gc清掉的话,那么将会返回null。

再提供一个清除方法吧。这个方法是留给子类以及其它程序们使用的,gc清除引用是直接清除,根本不需要调用这个方法。

public void clear() {
    this.referent = null;
}

再提供一些队列的操作的吧。同样也是提供给程序使用的,而不是gc。

public boolean isEnqueued() {

    synchronized (this) {
        return (this.queue != ReferenceQueue.NULL) && (this.next != null);
    }
}

public boolean enqueue() {
    return this.queue.enqueue(this);
}

第一个isEnqueued方法负责返回引用是否加入了队列。

enqueue方法负责入队。这个方法是提供给java code来使用。gc入队不需要使用这个方法。

哦,对了,构造函数得加上:

/* -- Constructors -- */

Reference(T referent) {
    this(referent, null);
}

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

我们提供了两个构造函数。一个只需要传入对象就可以了。另一个还要传入一个队列。

传入了自定义队列。就得java code中手动做一些gc操作来帮助gc去完成清理工作。

reference类写完了。现在我们开始写一个weakreference类吧。

public class WeakReference<T> extends Reference<T> {

    public WeakReference(T referent) {
        super(referent);
    }

    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }

}

没有做什么特殊操作。就是简单的继承了下。

好,现在上一个例子吧:

Object obj = new Object();
ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();
WeakReference<Object> weakRef = new WeakReference<Object>(obj, refQueue);
System.out.println(weakRef.get());
System.out.println(refQueue.poll());
obj = null;
System.gc();
System.out.println(weakRef.get());
System.out.println(refQueue.poll());

结果输出:

java.lang.Object@30c028cc null null null

如果你不传入queue的话,那么就会使用默认的Null这个queue。

从上面的这个例子中你也许发现了,当我们对对象设置为null 以后,然后告诉gc让其去回收。然后,我们发现,get已经为null了,这时候gc发现obj的可达性已经改变,于是就把obj的weak引用实例赋值给pending,referencehandler 发现了pending不为null,就把该引用加入队列。之后gc会清理队列中的弱引用。至此,完成了引用从使用到清理的全过程。

这就是引用的一个基本流程。

现在你也许还并不知道说WeakReference的使用场景。其实有很多地方都适合食用reference。比如mark们就通过WeakReference为你封装了一款map,名字叫做:WeakHashMap.

当你遇到要开发一个缓存,并且是短期内就过期的那种缓存的需求时,你就可以使用WeakHashMap来实现,你可以看看该类的源码,其实和我们常用的hashmap是差不多的,内部结构基本一样。唯一不一样的地方就是用来hold住key和value的那个内部类Entry是一个弱应用,继承了WeakReference。

如果在没有WeakReference的时候,如果你使用传统的hashmap来实现缓存的话,由于都是强引用,所以一直不会回收,最后就可能会导致内存溢出。

除了WeakReference外,还有:

软引用(SoftReference) 软引用在JVM报告内存不足的时候才会被GC回收,否则不会回收,正是由于这种特性软引用在caching和pooling中用处广泛。 虚引用(PhantomReference)     当GC一但发现了虚引用对象,将会将PhantomReference对象插入ReferenceQueue队列,而此时PhantomReference所指向的对象并没有被GC回收,而是要等到ReferenceQueue被你真正的处理后才会被回收。 Phantom Reference(幽灵引用) 与 WeakReference 和 SoftReference 有很大的不同, 因为它的 get() 方法永远返回 null, 这也正是它名字的由来 PhantomReference 唯一的用处就是跟踪 referent 何时被 enqueue 到 ReferenceQueue 中. PhantomReference 有两个好处: 其一, 它可以让我们准确地知道对象何时被从内存中删除, 这个特性可以被用于一些特殊的需求中(例如 Distributed GC, XWork 和 google-guice 中也使用 PhantomReference 做了一些清理性工作). 其二, 它可以避免 finalization 带来的一些根本性问题, 上文提到 PhantomReference 的唯一作用就是跟踪 referent 何时被 enqueue 到 ReferenceQueue 中, 但是 WeakReference 也有对应的功能, 两者的区别到底在哪呢 ? 这就要说到 Object 的 finalize 方法, 此方法将在 gc 执行前被调用, 如果某个对象重载了 finalize 方法并故意在方法内创建本身的强引用, 这将导致这一轮的 GC 无法回收这个对象并有可能引起任意次 GC, 最后的结果就是明明 JVM 内有很多 Garbage 却 OutOfMemory, 使用 PhantomReference 就可以避免这个问题, 因为 PhantomReference 是在 finalize 方法执行后回收的,也就意味着此时已经不可能拿到原来的引用, 也就不会出现上述问题, 当然这是一个很极端的例子, 一般不会出现.

总之,我们通过Reference和ReferenceQueue与GC进行互动,从而实现了各种引用。除了上面的介绍那些引用外,你也可以扩展出自己的符合你需求的引用类型来。本文我们主要介绍的是reference,在之后的推送中,我们会专门介绍 reference queue。

原文发布于微信公众号 - ImportSource(importsource)

原文发表时间:2016-12-27

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏北京马哥教育

两句话轻松掌握 python 最难知识点——元类

千万不要被所谓“元类是99%的python程序员不会用到的特性”这类的说辞吓住。因为每个中国人,都是天生的元类使用者 学懂元类,你只需要知道两句话: 道生一,...

4179
来自专栏Java3y

广州三本找Java实习经历

在学习编程时,跟我类似的人应该会有一个疑问:究竟学到什么程度才能找到一份实习/工作呢?

4080
来自专栏PPV课数据科学社区

【完整案例21张PPT】大数据监测P2P跑了风险 附PDF下载

★每日一题(答案次日公布) 昨日Q12答案:B Q12: 假设12个销售价格记录组已经排序如下:5, 10, 11, 13, 15,35, 50, 55,...

2795
来自专栏流媒体

C语言内存模型

1973
来自专栏数据之美

Java 多线程之 Runnable VS Thread 及其资源共享问题

对于 Java 多线程编程中的 implements Runnable 与 extends Thread,部分同学可能会比较疑惑,它们之间究竟有啥区别和联系呢?...

2376
来自专栏IT派

两句话轻松掌握 Python 最难知识点

千万不要被所谓"元类是99%的python程序员不会用到的特性"这类的说辞吓住。因为每个中国人,都是天生的元类使用者

1392
来自专栏chafezhou

Python 工匠:使用数字与字符串的技巧

数字是几乎所有编程语言里最基本的数据类型,它是我们通过代码连接现实世界的基础。在 Python 里有三种数值类型:整型(int)、浮点型(float)和复数(c...

911
来自专栏耕耘实录

漫谈正则表达式

版权声明:本文为耕耘实录原创文章,各大自媒体平台同步更新。欢迎转载,转载请注明出处,谢谢

1164
来自专栏Java爬坑系列

【Java入门提高篇】Day2 接口

  上一篇讲完了抽象类,这一篇主要讲解比抽象类更加抽象的内容——接口。   什么是接口呢?先来看一个现实中的栗子,我们常用的插座,一般分为两孔和三孔,所以基本上...

1938
来自专栏xingoo, 一个梦想做发明家的程序员

日志分析系统——Hangout源码学习

这两天看了下hangout的代码,虽然没有运行体验过,但是也算是学习了一点皮毛。 架构浅谈 Hangout可以说是java版的Logstash,我是没有测...

2898

扫码关注云+社区

领取腾讯云代金券