当你遇到要开发一个缓存,并且是短期内就过期的那种缓存的需求?你会怎么实现呢?
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 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!