我之前面试过一些高级Java软件工程师,其中有一个面试题目就是“你对弱引用(weak reference)了解多少?”。我提这个问题的目的并不是想要面试者对这个话题做一个深入技术性的阐述,如果他们回答说“呃,这个貌似与垃圾回收(garbage collection)有点关系”,我就想当满意了。但出乎我的意料的是,20多个有着至少5年经验的资深软件工程师中,只有两个知道有弱引用这么回事,其中还只有一个真正了解弱引用。我试图给他们一点提示,期待他们能够恍然大悟——“哦,原来弱引用是这么回事儿”,可惜没有一个人。我不明白为什么这个知识点这么鲜为人知,要知道,弱引用是一个非常有用的特性,它在七年前(译者注:相对于原文的写作时间2006年)Java 1.2版本发布的时候就提供了。
现在,我并不是希望你成为弱引用方面的专家来证明自己是一个优秀的Java软件工程师,但依我拙见,你至少应该知道弱引用是怎么回事,否则你怎么知道什么时候该使用弱引用?既然弱引用是一个鲜为人知的特性,下面我就简要的介绍下什么是弱引用,怎么使用它以及何时使用它。
强引用
我先从强引用(Strong References)的回顾开始吧。强引用就是一个普通的Java引用,正如大家每天使用的一样,例如,下面的代码:
StringBuffer buffer = new StringBuffer();
这段代码创建了一个StringBuffer对象并把它的强引用保存到buffer变量中。是,是,这个有点太小儿科了,但请大家保持耐心。强引用的一个重点,也就是什么使得他们强,在于它们与垃圾回收器(
garbage collector)交互的方式。确切的说,如果一个对象通过一个强引用链强可达,那它就不会被垃圾回收器回收。正如你总不想垃圾回收器把你正在用的东西销毁,强引用在大多数情况下正是你想要的。
当引用太强的时候
我们有时候会碰到一些不能被合理继承的类,像被final修饰的类,甚至一些更复杂的场景,例如一个工厂方法返回的接口可能有多个实现,还有可能是哪些实现都不知道。假设你要用一个Widget类,并且由于某种原因,通过继承Widget类来添加新的功能是不大可能或不切实际的。
如果你需要记录对象的一些额外信息该怎么办?在这个例子中,我们需要记录每个Widget对象的序列号,但Widget类并没有序列号这个属性,并且由于Widget是无法继承的,我们也不能添加一个这样的属性。一点问题都没有,用HashMap就可以了:
serialNumberMap.put(widget, widgetSerialNumber);
这种做法表面上看起来没什么问题,但Widget对象的强引用很可能会引发一些问题。要知道,当一个Widget对象的序列号不再需要的时候,我们必须把它相关的键值对从map中移除,否则可能会造成内存泄露(如果我们在必须移除它的时候没有移除)或者意外地发现丢失了一些序列号(如果我们移除了仍在使用的Widget对象)。如果这些现象听起来很耳熟,那应该是一些没有垃圾回收功能的编程语言的用户在试图管理内存的时候经常碰到的问题,但在像Java这样更高级的编程语言中,我们似乎并不需要担心这个问题。
另一个与强引用相关的常见问题就是缓存,尤其是占用内存比较多的那种,例如图片缓存。假设你有一个要使用用户提供的图片的应用程序,像我正在使用的网站设计工具。自然你想把这些图片缓存起来,由于每次都从磁盘中读取实在太耗费时间了。另外,你也不想这些图片在内存中缓存两份。由于图片缓存是用于防止图片从磁盘中做不必要的加载,你理所当然地认为缓存一直要保存内存中已经存在的图片。由于缓存使用的普通强引用强制图片驻留在内存中,这使得你必须采取某种方法来决定图片什么时候不再需要了并把它从缓存中移除以便于垃圾回收器回收它,否则就会碰到上文中提出的内存泄露问题。你又一次需要重复垃圾回收器做的事情并且手工判断一个对象是否还应该驻留在内存中。
弱引用
简单来说,弱引用(Weak References)就是一种没有强到迫使对象必须驻留在内存的引用。弱引用允许你使用垃圾回收器来帮你决定对象是否可达,因此不用自己判断了。你可以采用如下方式创建一个弱引用:
WeakReference<Widget> weakWidget = new WeakReference<Widget>(widget);
在代码中,你可以使用weakWidget.get()来获取对应的Widget对象。由于弱引用不至于强到阻止垃圾回收,你会发现当没有widget的强引用时,weakWidget.get()
突然开始返回null。
解决上面提出的“Widget序列号”问题的最简单方法就是使用JDK提供的WeakHashMap类。除了key(注意,不是value)是使用弱引用外,WeakHashMap的用法和HashMap没多大不同。当一个WeakHashMap的key变成垃圾的时候,相应的键值对就会被自动移除。这种方式避免了我描述的缺陷(使用HashMap需要自己判断什么时候移除键值对),并且,把HashMap换成WeakHashMap也不需要做额外的改动。如果使用Map接口来引用创建的map,其他的代码甚至感觉不到什么变化。
引用队列
一旦WeakReference对象的get()方法用始返回null,它所指向的对象就变成了垃圾并且WeakReference对象也没什么用了。这通常意味需要进行清理,例如,WeakHashMap此时就会移除已经被回收的key以避免保存持续增长的弱引用类型的key。
ReferenceQueue类可以跟踪已经死亡的引用(dead reference)
。如果你把ReferenceQueue作为参数传给WeakReference的构造器,那么当WeakReference对象所指向的对象变成垃圾的时候,这个WeakReference对象会被自动加入到引用队列中,然后你就可以通过引用队列来对已经死亡的引用做一些清理操作。
不同强弱的引用
软引用
虚引用
- 它是你知道对象何时从内存中移除的唯一方法。这通常不会那么有用,但在一些特殊的场景非常有用,例如维护大尺寸图片时,你可以在图片真正被回收之后再去载入下一幅图片 ,这可以减少致命的的OutOfMemoryError出现的几率。
- 虚引用可以避免对象终结的主要问题,即可以通过在finalize()方法中创建指向对象的强引用来使对象复活,你可能会说,那又怎样?覆盖了finalize()方法的对象的问题是必须经过两次单独的垃圾回收周期才能决定对象是否要回收。由于对象在终结之前有极小的可能性被复活,因此在对象能从内存中移除之前,垃圾回收器还需要运行一次。另外,由于对象终结可能不会被及时执行,那么在对象等待被终结的过程中,还会有其他的垃圾回收周期,这意味着清理垃圾对象可能会有严重的延迟,这也是当堆中大多数对象都是可被回收的垃圾时仍会出现OutOfMemoryError的原因。使用虚引用就避免了这个问题,因为一旦虚引用进入引用队列,你就拿不到虚引用指向的对象了(它指向的对象已经不在内存中了),那么在第一次垃圾回收时只要发现对象是虚可达的,对象就可以被立刻清理掉,然后你就可以在你方便的时候处理需要处理的任何资源。