原文
Posted by enicholas on May 4, 2006 at 5:06 PM PDT
译文
我面试的这几个人怎么这么渣啊,连弱引用概念都没有。不行,我要写一篇吐槽一下。
相信我,弱引用很重要。
强引用(Strong references)
首先先回顾一下强引用(strong reference)。强引用是常规的Java引用,你每天都会使用。例如:
StringBuffer buffer = new StringBuffer();
创建了一个StringBuffer
,并在变量缓冲区中存储了其强引用。是的,就是这个玩意儿,但请耐心些。关于强引用,重要的是,亦即使其成为强引用的部分是,它们如何与垃圾收集器(garbage collector,GC)相互作用的。具体而言,如果一个对象可以通过强引用链可达的话(强可达, strong reachable),那它不会被垃圾收集。正如你不想垃圾收集器销毁你正在使用的对象一样,这正是你期望的。
当强引用太强时
一般而言,应用通常会使用它可以合理扩展的类。这些类可以被标记为final
,或者可以是更复杂的,例如由工厂方法返回的接口,隐藏了一些未知或者根本不可知数量的具体实现。假设你想使用Widget
类,扩展它是不可能或者说是不切实际的。
当需要跟踪对象的额外信息时该怎么办呢?这里,假设我们需要跟踪每个Widget
的序列号属性,因为Widget
是不可扩展的,不能添加这个属性。根本没问题,可以用HashMap
:
serialNumberMap.put(widget, widgetSerialNumber);
从表面来看,这是可行的,但对widget
对象的强引用很可能会有问题。我们必须知道什么时候一个特定的Widget
对象的序列号不再需要了,这样才能够从map中移除其对应的项。否则,我们想享受内存泄漏的热情服务(如果没在应该移除时移除Widget
对象),或者莫名的丢失了一些序列号(如果在仍需使用时移除了Widget
对象)。如果这些问题听起来很熟悉,他们应该只是那些尝试自己做内存管理、而非使用垃圾收集的语言面临的问题,而幸福的Java程序员们根本不需要担心这些问题。
强引用的另一个常见问题是缓存(caching),特别是使用了像图片这种叫较大的结构时。考虑一个应用需要处理用户提供的图片,就像我在写的Web站点设计工具。自然的想缓存这些图片,因为从磁盘加载它们太耗时了;同时要避免内存中同时出现大图片的两个副本。
图片缓存的目的是避免不必要的图片重新加载,这意味这缓存必须持有已在内存中的任何一个图片的引用。用常规的强引用,这些引用强制图片保留在内存中,这需要程序员确定何时图片不再需要了,可以从缓存中删除,从而可以被垃圾收集。再次的,程序员*重复垃圾收集器的行为,在代码中显式确定对象应不应该在内存中。
弱引用(Weak references)
简单而言,弱引用(weak reference)是一类引用,它们还没有足够强到可以强制对象保留在内存中。弱引用可以帮助充分利用垃圾收集器的对象可达性确定功能,而不需要做额外的努力。像下面这样创建一个弱引用:
WeakReference<Widget> weakWidget = new WeakReference<Widget>(widget);
在其他代码中可以使用weakWidget.get()
获得实际的Widget
对象。当然,弱引用还没有强到可以阻止垃圾收集,可能会发现在没有其他widget
强引用时,weakWidget.get()
会返回null
。
要解决上面的Widget
的序列号问题,最简单的方法是使用WeakHashMap
类。WeakHashMap
在HashMap
的基础上弱引用了键值。如果WeakHashMap
的一个键变为垃圾了,则其对应的项从映射中自动删除。这避免了我上一节所述的问题。如果你是面向接口编程的实践者,将HashMap
改为WeakHashMap
后,其他代码甚至对这一变化毫无感知。
引用队列
一旦一个WeakReference
开始返回null
,其指向的对象变为了垃圾,这个WeakReference
对象就没什么用了。这通常意味着需要做一些清理工作;例如WeakHashMap
必须删除这写已失效的项,以避免持有数量不断增长的WeakReference
。
ReferenceQueue
类跟踪已失效的引用。如果将一个ReferenceQueue
实例作为弱引用的构造器参数传入,引用的对象会在失效时自动入队。可以随后以固定的周期处理ReferenceQueue
,执行一些清理工作。
弱的不同程度
到这里我一直使用弱引用这个词,但实际上有四种不同引用强度:强(strong), 软(soft),弱(weak)和幻象(phantom),强度从高到低。前面已经讨论了强引用和弱引用,下面说说另外两个。
软引用
软引用就像弱引用,除了它在抛弃引用的对象时没弱引用那么意志坚定。一个对象弱可达时(即对其最强的引用是WeakReference
),将在下一次垃圾收集时会回收;但一个软可达对象会纠缠一段时间。
SoftReference
不需要有WeakReference
之外的其他功能,实际上在内存足够用时,软可达对象通常存活很久。这使得它们成为缓存实现的基础,例如上面讨论的图片缓存;可以很大度的将判断对象的可达性和所需内存留给垃圾收集器去担心了。
幻象引用
幻象引用(PhantomReference
)与SoftReference
或者WeakReference
不同,它们对对象的引用如此脆弱,你甚至无法通过它们获得实际对象,它们的get()
方法总是返回null
。幻象引用的唯一用途是跟踪入ReferenceQueue
的引用,那时可以明确的知道所引用的对象已经失效了。那跟WeakReference
有什么区别呢?
幻象引用与弱引用的区别体现在入队时。弱引用对象在其引用的对象变为弱可达时立即入队,这在
对象终止(finalization)或者垃圾收集实际发生之前,理论上一些finalize()
方法实现中甚至可以复活对象,但是弱引用本身已经失效。PhantomReference
仅在其引用的对象在内存中物理删除后入队,其get()
总是返回null
,明确的阻止近乎死亡对象的复活。
PhantomReference
有什么用呢?我只考虑两个情景:首先,它可以明确的确定何时对象被从内存中删除,这也是唯一的方法。这不是那么通用,但在特定情形下特别有用,例如处理大图片时:如果你明确知道一个图片应该被垃圾收集,可以在尝试加载下一张图片前等待其实际发生,从而降低OutOfMemoryError
发生的可能性。
第二,PhantomReference
避免了对象终止问题:finalize()
方法可以通过新创建强引用复活对象。那又怎么样了?问题变成了覆盖finalize()
方法的对象至少需要在两个垃圾收集周期中判断是否需要回收。第一个周期判定对象成为了垃圾,准备对象终止。因存在对象可以在终止过程中被复活的可能性,在对象被实际删除前必须再次执行垃圾收集。因为对象终止不是实时的,在对象等待终止过程中可能已经执行了好几个垃圾回收周期了。这会造成实际清理垃圾对象时的延迟,就是堆中大部分已经是垃圾时产生OutOfMemoryError
的原因。
PhantomReference
在手,这个问题不再有:PhantomReference
入队时,已经没有办法获得已死对象的指针了,它已经不再内存中了。因为PhantomReference
不能用于复活其引用的对象,这个对象可以在第一次被作为幻象可达发现的垃圾收集周期中直接清理。你可以销毁其他需要手动销毁的对象。
DO NOT EVER USE
finalize()
.
结论
相信我,没错的。
吐槽
这个问题4月份(大概)在京东某部笔试中遇到,当时不以为然,现在也不以为然,这篇译文纯属个人的无聊之行为。
原文旧是旧了点,看JDK 1.7中Reference
类还是since 1.2就知道其还是可以一看的。
上线后有几个参与开发的5-年经验的Javaer盯着过JVM GC日志?
实在要做也可以,找些闲的蛋疼的5+年经验的吧。