前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >设计WeakReference的那段日子

设计WeakReference的那段日子

作者头像
ImportSource
发布2018-04-03 15:27:03
8600
发布2018-04-03 15:27:03
举报
文章被收录于专栏:ImportSourceImportSource

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

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

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

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

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

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

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

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

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

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

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

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

代码语言:javascript
复制
public abstract class Reference<T> {
代码语言:javascript
复制

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

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

代码语言:javascript
复制
private T referent;  

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

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

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

代码语言:javascript
复制
ReferenceQueue<? super T> queue;

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

代码语言:javascript
复制
private static Reference pending = null;

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

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

那就搞个handler吧。

代码语言:javascript
复制
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);
        }
    }
}

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

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

代码语言:javascript
复制
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();
}

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

代码语言:javascript
复制
public T get() {
    return this.referent;
}

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

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

代码语言:javascript
复制
public void clear() {
    this.referent = null;
}

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

代码语言:javascript
复制
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入队不需要使用这个方法。

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

代码语言:javascript
复制
/* -- 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类吧。

代码语言:javascript
复制
public class WeakReference<T> extends Reference<T> {

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

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

}

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

好,现在上一个例子吧:

代码语言:javascript
复制
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。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2016-12-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 ImportSource 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档