SoftReference, WeakReference, PhantomReference的区别
2018-08-07 16:11 码年 阅读(1439) 评论(1) 编辑 收藏 举报一段时间以前,我面试了一些Java高级工程师职位的候选人。在我问的众多问题中的一个是“谈谈你对Java弱引用的理解”。我并没有期待一个类似于技术论文一样的答案。候选人如果回答说“嗯…,这不是和垃圾回收有关系吗?”,我可能就已经很满意了。然而出乎我意料的是我问的二十几个候选人差不多都有五年以上的Java经验,但是只有两个人知道弱引用的存在,只有一个人具有实际的使用经验。我甚至尝试解释一下,以得到“啊哦,是这样啊”的回应,但是并没有。我不确定为什么很多人对弱引用不够了解,因为弱引用是从Java 1.2就引入并被广泛使用的功能。
现在,我不会建议你为了做一个不错的Java开发人员而成为一个弱引用的专家。但是我个人认为你最起码应该了解他们是什么。否则你怎么知道什么时候应该使用它们?这篇文章会对以下方面做一个扼要的说明:什么是弱引用,怎么使用它们,什么时候使用它们。
强引用
首先让我们回忆一下强引用。强引用是一个普通的引用,你每天都会使用。例如:
StringBuffer buffer = new StringBuffer();
创建一个新的StringBuffer()并且保存一个对它的强引用到buffer变量。是的,是的,这是小儿科,但是忍受我一下先。强引用重要的地方(使得它们成为“强引用”的地方)就是它们是怎样和垃圾回收器交互的。确切的说,如果一个对象可以通过一个强引用链条被访问到,它是不能被垃圾回收的。因为你不会想让垃圾回收器销毁你正在工作着的对象。这正是你想要的。
当强引用太强了
我们经常会遇到想扩展一些类的功能但是这些类又不太容易被扩展的情形。有时是类被标记为了final,有时是类太复杂了,比如说一个工厂方法返回一个interface类型的对象,但是对象的具体实现是不知道的(甚至都不可能知道)。假设你必须要使用Widget类,但是由于某种原因你不能够继承Widget去扩展更多的功能。
如果我们需要跟踪保存一些Widget额外的信息的话,应该怎么办?假设我们需要跟踪保存每一个Widget的序列号(Serial Number),但是Widget并没有序列号属性。由于Widget不能被继承,我们也不能添加序列号属性。没关系,我们可以使用HashMap:
serialNumberMap.put(widget, widgetSerialNumber);
乍一看,好像没什么问题。但是对widget的强引用肯定会造成问题。我们不得不清楚的知道什么时候某个Widget的序列号将不再被需要,我们可以把它从HashMap中移除。否则将会产生内存泄漏(如果我们应该从HashMap移除掉widget的时候却没有移除)或者令人费解的发现我们丢掉了一些序列号(如果我们移除掉了还在使用的widget)。如果这些问题很眼熟,它们其实是非垃圾回收编程语言使用者在管理内存时面临的问题。对于更加文明的编程语言比如Java,我们是不应该关心这个问题的。
强引用另外一个常见的问题是关于缓存的,特别是涉及到大的结构,比如图片。假设你的应用程序需要处理用户上传的图片,就像我做过的网站设计工具一样。很自然地,你想要缓存这些图片,因为从磁盘加载它们有非常大的开销,并且你也想避免对一个可能很大的图片同时加载两份到内存。
因为一个图片的缓存可以使我们避免从磁盘重新加载到内存(当我们并不是绝对的需要这样),你可能很快会意识到缓存应当对内存中存在的每一个图片都有一个引用。然而使用常规的强引用,将会使得图片一直保存在内存中。这将会使你(和上面一样)不得不以某种方式决定图片在什么时候不再需要了,进而可以从缓存中移除,之后GC可以回收掉它。再一次的,你不得不模仿GC的行为并且以人工的方式决定一个对象是否应该留在内存中。
弱引用(WeakReference)
弱引用简单的说是一个不足以使得一个对象保留在内存中的引用。弱引用允许你利用GC来判定对象是否可达,而不必自己来判定。创建弱引用的方式如下:
WeakReference weakWidget = new WeakReference(widget);
然后你在其他地方可以使用weakWidget.get() 来获取实际的widget对象。当然弱引用不足以保证对象不被GC回收,所以你可能会忽然发现(如果没有强引用指向widget) weakWidget.get() 突然开始返回null。
解决上面的“widget序列号”问题,最简单的方式是使用内置的WeakHashMap类。除了键(注意:不是值!)使用WeakReference指向以外,WeakHashMap和HashMap工作方式完全一样。如果WeakHashMap的一个键变成了垃圾,对应的键值对将会自动从中移除。这避免了前面提到的问题,代码也只是从使用HashMap简单的改变为使用WeakHashMap。如果你遵循惯用的用Map接口指向你的map类的风格,其他的代码甚至都不需要知道这种改变。
ReferenceQueue
当一个WeakReference开始返回null时,它所指向的对象变成了垃圾,这个WeakReference对象本身几乎也没什么用了。这意味着有一些清除工作需要做。例如,WeakHashMap不得不移除掉这样的死掉的键值对以避免包含越来越多死掉的WeakReference。
ReferenceQueue使得跟踪死掉的引用变得简单。如果你给WeakReference的构造方法传入一个ReferenceQueue,那么当这个WeakReference的对象所指向的对象成为垃圾的时候,这个WeakReference对象将会被插入这个ReferenceQueue。接下来,你可以隔一段时间就处理一下ReferenceQueue,做一些对死掉的WeakReference任何你想做的清理工作。
不同程度的弱引用
到现在我只是提到了弱引用,但是实际上有四种从强到弱不同强度的引用:强引用,软引用(SoftReference),弱引用(WeakReference)和虚幻引用(PhantomReference)。我们已经讨论了强引用和弱引用,接下来讨论其他两种。
软引用(SoftReference)
SoftReference除了比WeakReference不急于扔掉所指向的对象以外它们是一样的。一个对象如果最强的引用只是WeakReference,将会在下次GC发生的时候被遗弃掉。但是一个对象如果只被软引用指向,简单的讲它将会再坚持存在一段时间。
SoftReference并没有被要求和WeakReference有什么不同的行为,但是被SoftReference引用的对象简单的讲只要内存足够将会一直被保留。这是它们适合作为缓存的根本所在,就像上面描述的图像缓存一样,因为你可以交给GC去关心对象以哪种方式可达(强引用对象将永远不会从内存移除),GC有多么的需要去回收缓存所占用的内存。
虚幻引用(PhantomReference)
PhantomReference与SoftReference和WeakReference都不一样。它对于对象的引用非常的虚幻,以致于你甚至没有办法获取被引用的对象—它的get()方法总是返回null。它唯一的用处就是跟踪被引用的对象何时进入到ReferenceQueue,从而知道被引用对象何时死掉了。然而这和WeakReference有什么不同呢?
确切的说,不同的地方在于何时进入到ReferenceQueue。WeakReference在它所指的对象被GC认定为只是弱引用可达的时候就会被放到ReferenceQueue中。这个发生在finalize()调用之前或者垃圾回收之前;理论上说所指向的对象甚至可以通过finalize()方法“复活”,但是WeakReference将会保持死亡状态。PhantomReference只有当对象从物理内存中移除的时候才会进入到ReferenceQueue,并且由于它的get () 方法总是返回null,这保证了你不能够“复活”一个濒死的对象。
PhantomReference有什么好处呢?我只知道两个正式使用它的情形:首先它允许你准确的判断一个对象什么时候从物理内存中移除。实际上这是唯一的判断方式。大致上说这不是非常的有用,但是在某些特定的场合可能会很方便:如果你确切的知道一个图片应当会被回收掉,你可以等到它确实已经从内存中移除了然后再加载下一个图片,由此你可以降低吓人的OutOfMemoryError发生的概率。
其次,PhantomReference避免了对象终结(finalization)固有的问题:finalize()方法可以通过创建强引用来“复活”对象。这又能怎么样?问题是一个复写了finalize()方法的对象,现在必须需要至少经过两轮的GC过程,才能被回收掉。第一轮判定一个对象为垃圾,从而可以去调用它的finalize()方法。由于在finalize()方法中“复活”的可能性(很小但确实存在),GC不得不在真正回收对象之前再跑一轮。并且因为finalize()方法的调用并不是即时性的,当对象在等待它的finalize()方法被调用的时候,GC可能已经跑过好多轮了。这意味着从判定对象为垃圾到真正回收内存之间存在着严重的延时,这也是你为什么会在大部分对象为垃圾的情况下还会遇到OutOfMemoryError的原因。
使用PhantomReference,这种情况是不可能的—当一个PhantomReference入队列的时候,你绝对没有办法获取已经死掉的对象(这很好,因为它已经不在内存中了)。因为PhantomReference不能被用来复活一个对象, GC如果发现对象只是被虚幻引用,在第一轮的时候就可以立即回收这个对象。而你可以在任何方便的时候清理资源。
或许finalize()方法本来从最初就不应该被提供出来。PhantomReference使用起来绝对是更安全和更高效的,并且移除掉finalize()方法,会使得JVM的某些部分变得相当的简单。但是PhantomReference的使用会包含比较多的工作,所以我也承认我大部分时间也还是在使用finalize()。好消息是至少你有一个选择。
总结
我确定你们中的某些人在喃喃自语,因为我在讨论一个存在了好多年的功能,而并不是在讨论一个从未被讨论过的话题。以我的经验,很多Java开发者确实对弱引用不是太了解(如果知道一点的话)。我感觉需要写篇文章刷新一下记忆,希望你能从这篇回顾中有一点儿收获。
作者公众号(码年)扫码关注:
英文原文: