Java中的四种引用:强引用、软引用、弱引用和虚引用
Java有四种强度不同的引用。从强到弱它们分别是:强引用、软引用、弱引用和虚引用。
强引用(StrongReference)
强引用就是我们经常使用的引用,其写法如下:
StringBuffer buffer = new StringBuffer();
上面创建了一个StringBuffer对象,并将这个对象的(强)引用存到变量buffer中。强引用最重要的就是它能够让引用变得强(Strong),这就决定了它和垃圾回收器的交互。具体来说,如果一个对象通过一串强引用链接可到达(Strongly reachable),它是不会被回收的。如果你不想让你正在使用的对象被回收,这就正是你所需要的。
但是强引用如此之强,在一个程序里,将一个类设置成不可被扩展是有点不太常见的,当然这个完全可以通过类标记成final实现。或者也可以更加复杂一些,就是通过内部包含了未知数量具体实现的工厂方法返回一个接口(Interface)。举个例子,我们想要使用一个叫做Zoo的类,但是这个类不能被继承,所以无法增加新的功能。
但是我们如果想追踪Zoo对象的额外信息,我们该怎么办? 假设我们需要记录每个对象的序列号,但是由于Zoo类并不包含这个属性,而且也不能扩展导致我们也不能增加这个属性。其实一点问题也没有,HashMap完全可以解决上述的问题。
serialNumberMap.put(zoo, zooSerialNumber);
这表面看上去没有问题,但是zoo对象的强引用很有可能会引发问题。我们可以确信当一个zoo序列号不需要时,我们应该将这个条目从map中移除。如果我们没有移除的话,可能会导致内存泄露,亦或者我们手动移除时删除了我们正在使用的zoos,会导致有效数据的丢失。其实这些问题很类似,这就是没有垃圾回收机制的语言管理内存时常遇到的问题。但是我们不用去担心这个问题,因为我们使用的时具有垃圾回收机制的Java语言。
另一个强引用可能带来的问题就是缓存,尤其是像图片这样的大文件的缓存。假设你有一个程序需要处理用户提供的图片,通常的做法就是做图片数据缓存,因为从磁盘加载图片代价很大,并且同时我们也想避免在内存中同时存在两份一样的图片数据。
缓存被设计的目的就是避免我们去再次加载哪些不需要的文件。你会很快发现在缓存中会一直包含一个到已经指向内存中图片数据的引用。使用强引用会强制图片数据留在内存,这就需要你来决定什么时候图片数据不需要并且手动从缓存中移除,进而可以让垃圾回收器回收。因此你再一次被强制做垃圾回收器该做的工作,并且人为决定是该清理到哪一个对象。
软引用(SoftReference)
软引用基本上和弱引用差不多,只是相比弱引用,它阻止垃圾回收期回收其指向的对象的能力强一些。如果一个对象是弱引用可到达,那么这个对象会被垃圾回收器接下来的回收周期销毁。但是如果是软引用可以到达,那么这个对象会停留在内存更时间上长一些。当内存不足时垃圾回收器才会回收这些软引用可到达的对象。
由于软引用可到达的对象比弱引用可达到的对象滞留内存时间会长一些,我们可以利用这个特性来做缓存。这样的话,你就可以节省了很多事情,垃圾回收器会关心当前哪种可到达类型以及内存的消耗程度来进行处理。
弱引用(WeakReference)
Java中的弱引用具体指的是java.lang.ref.WeakReference<T>类,以下是官方文档对它做的说明:
弱引用对象的存在不会阻止它所指向的对象被垃圾回收器回收。弱引用最常见的用途是实现规范映射(canonicalizing mappings,比如哈希表)。
假设垃圾收集器在某个时间点决定一个对象是弱可达的(weakly reachable)(也就是说当前指向它的全都是弱引用),这时垃圾收集器会清除所有指向该对象的弱引用,然后把这个弱可达对象标记为可终结(finalizable)的,这样它随后就会被回收。与此同时或稍后,垃圾收集器会把那些刚清除的弱引用放入创建弱引用对象时所指定的引用队列(Reference Queue)中。
弱引用简单来说就是将对象留在内存的能力不是那么强的引用。使用WeakReference,垃圾回收器会帮你来决定引用的对象何时回收并且将对象从内存移除。创建弱引用写法如下:
WeakReference<Zoo> weakZoo = new WeakReference<Zoo>(zoo);
使用weakZoo.get()就可以得到真实的Zoo对象,因为弱引用不能阻挡垃圾回收器对其回收,你会发现(当没有任何强引用到zoo对象时)使用get时突然返回null。
解决上述的zoo序列数记录的问题,最简单的办法就是使用Java内置的WeakHashMap类。WeakHashMap和HashMap几乎一样,唯一的区别就是它的键(不是值!!!)使用WeakReference引用。当WeakHashMap的键标记为垃圾的时候,这个键对应的条目就会自动被移除。这就避免了上面不需要的Zoo对象手动删除的问题。使用WeakHashMap可以很便捷地转为HashMap或者Map。
一旦弱引用对象开始返回null,该弱引用指向的对象就被标记成了垃圾。而这个弱引用对象(非其指向的对象)就没有什么用了。通常这时候需要进行一些清理工作。比如WeakHashMap会在这时候移除没用的条目来避免保存无限制增长的没有意义的弱引用。
引用队列可以很容易地实现跟踪不需要的引用。当你在构造WeakReference时传入一个ReferenceQueue对象,当该引用指向的对象被标记为垃圾的时候,这个引用对象会自动地加入到引用队列里面。接下来,你就可以在固定的周期,处理传入的引用队列,比如做一些清理工作来处理这些没有用的引用对象。
理解:
为什么使用弱引用?
现在有一个Pandas类代表熊猫,这个类被设计为不可扩展的,而此时我们想要为每只熊猫增加一个编号。一种解决方案是使用HashMap<Pandas, Integer>。于是问题来了,如果我们已经不再需要一个Pandas对象存在于内存中(比如已经卖出了这件产品),假设指向它的引用为pandaA,我们这时会给pandaA赋值为null,然而这时pandaA过去指向的Pandas对象并不会被回收,因为它显然还被HashMap引用着。所以这种情况下,我们想要真正的回收一个Pandas对象,仅仅把它的强引用赋值为null是不够的,还要把相应的条目从HashMap中移除。显然“从HashMap中移除不再需要的条目”这个工作我们不想自己完成,我们希望告诉垃圾收集器:在只有HashMap中的key在引用着Pandas对象的情况下,就可以回收相应Pandas对象了。显然,根据前面弱引用的定义,使用弱引用能帮助我们达成这个目的。我们只需要用一个指向Pandas对象的弱引用对象来作为HashMap中的key就可以了。
如何使用弱引用?
拿上面介绍的场景举例,我们使用一个指向Pandas对象的弱引用对象来作为HashMap的key,只需这样定义这个弱引用对象:
Pandas pandaA = new Pandas(...);
WeakReference<Pandas> weakPandaA = new WeakReference<>(pandaA);
现在,若引用对象weakPandaA就指向了Pandas对象pandaA。那么我们怎么通过weakPandaA获取它所指向的Pandas对象pandaA呢?很简单,只需要下面这句代码:
Pandas pandas= weakPandaA.get();
实际上,对于这种情况,Java类库为我们提供了WeakHashMap类,使用和这个类,它的键自然就是弱引用对象,无需我们再手动包装原始对象。这样一来,当pandaA变为null时(表明它所引用的Pandas已经无需存在于内存中),这时指向这个Pandas对象的就是由弱引用对象weakPandaA了,那么显然这时候相应的Pandas对象时弱可达的,所以指向它的弱引用会被清除,这个Pandas对象随即会被回收,指向它的弱引用对象会进入引用队列中。
虚引用 (PhantomReference)
与软引用,弱引用不同,虚引用指向的对象十分脆弱,我们不可以通过get方法来得到其指向的对象。它的唯一作用就是当其指向的对象被回收之后,自己被加入到引用队列,用作记录该引用指向的对象已被销毁。
当弱引用的指向对象变得弱引用可到达,该弱引用就会加入到引用队列。这一操作发生在对象析构或者垃圾回收真正发生之前。理论上,这个即将被回收的对象是可以在一个不符合规范的析构方法里面重新复活。但是这个弱引用会销毁。虚引用只有在其指向的对象从内存中移除掉之后才会加入到引用队列中。其get方法一直返回null就是为了阻止其指向的几乎被销毁的对象重新复活。
虚引用使用场景主要由两个。它允许你知道具体何时其引用的对象从内存中移除。而实际上这是Java中唯一的方式。这一点尤其表现在处理类似图片的大文件的情况。当你确定一个图片数据对象应该被回收,你可以利用虚引用来判断这个对象回收之后再继续加载下一张图片。这样可以尽可能地避免可怕的内存溢出错误。
第二点,虚引用可以避免很多析构时的问题。finalize方法可以通过创建强引用指向快被销毁的对象来让这些对象重新复活。然而,一个重写了finalize方法的对象如果想要被回收掉,需要经历两个单独的垃圾收集周期。在第一个周期中,某个对象被标记为可回收,进而才能进行析构。但是因为在析构过程中仍有微弱的可能这个对象会重新复活。这种情况下,在这个对象真实销毁之前,垃圾回收器需要再次运行。因为析构可能并不是很及时,所以在调用对象的析构之前,需要经历数量不确定的垃圾收集周期。这就意味着在真正清理掉这个对象的时候可能发生很大的延迟。这就是为什么当大部分堆被标记成垃圾时还是会出现烦人的内存溢出错误。
使用虚引用,上述情况将引刃而解,当一个虚引用加入到引用队列时,你绝对没有办法得到一个销毁了的对象。因为这时候,对象已经从内存中销毁了。因为虚引用不能被用作让其指向的对象重生,所以其对象会在垃圾回收的第一个周期就将被清理掉。
显而易见,finalize方法不建议被重写。因为虚引用明显地安全高效,去掉finalize方法可以虚拟机变得明显简单。当然你也可以去重写这个方法来实现更多。这完全看个人选择。
引用队列(Reference Queue)
实际上,WeakReference类有两个构造函数:
//创建一个指向给定对象的弱引用
WeakReference(T referent)
//创建一个指向给定对象并且登记到给定引用队列的弱引用
WeakReference(T referent, ReferenceQueue<? super T> q)
我们可以看到第二个构造方法中提供了一个ReferenceQueue类型的参数,通过提供这个参数,我们便把创建的弱引用对象注册到了一个引用队列上,这样当它被垃圾回收器清除时,就会把它送入这个引用队列中,我们便可以对这些被清除的弱引用对象进行统一管理。
总结
- 强引用(Strong Reference):通常我们通过new来创建一个新对象时返回的引用就是一个强引用,若一个对象通过一系列强引用可到达,它就是强可达的(strongly reachable),那么它就不被回收;
- 软引用(Soft Reference):软引用和弱引用的区别在于,若一个对象是弱引用可达,无论当前内存是否充足它都会被回收,而软引用可达的对象在内存不充足时才会被回收,因此软引用要比弱引用“强”一些;
- 弱引用(Weak Reference):弱引用简单来说就是将对象留在内存的能力不是那么强的引用。使用Weak Reference,垃圾回收器会帮你来决定引用的对象何时回收并且将对象从内存移除。
- 虚引用(Phantom Reference):虚引用是Java中最弱的引用,那么它弱到什么程度呢?它是如此脆弱以至于我们通过虚引用甚至无法获取到被引用的对象,虚引用存在的唯一作用就是当它指向的对象被回收后,虚引用本身会被加入到引用队列中,用作记录它指向的对象已被回收。