Java 中的四种(强、软、弱和虚)引用

一、强引用(StrongReference)

强引用很常见,其写法如下:

StringBuffer buffer = new StringBuffer();

上面创建了一个 StringBuffer 对象,并将这个对象的(强)引用存到变量 buffer 中。强引用最重要的就是它能够让引用变得强(Strong),这就决定了它和垃圾回收器的交互。具体来说,如果一个对象通过一串强引用链接可到达(Strongly reachable),它是不会被回收的。如果不想让正在使用的对象被回收,这正是所需要的。

但是强引用如此之强,在一个程序里,将一个类设置成不可被扩展是有点不太常见的,当然这个完全可以通过类标记成 final 实现。或者也可以更加复杂一些,就是通过内部包含了未知数量具体实现的工厂方法返回一个接口(Interface)。举个例子,想要使用一个叫做 Zoo 的类,但是这个类不能被继承,所以无法增加新的功能。

但是想追踪 Zoo 对象的额外信息,该怎么办? 假设需要记录每个对象的序列号,但是由于 Zoo 类并不包含这个属性,而且也不能扩展导致也不能增加这个属性。其实一点问题也没有,HashMap 完全可以解决上述的问题。

serialNumberMap.put(zoo, zooSerialNumber);

表面看上去没问题,但是 zoo 对象的强引用很有可能会引发问题。可以确信当一个 zoo 序列号不需要时,应该将这个条目从 map 中移除。如果没有移除的话,可能会导致内存泄露,亦或者手动移除时删除了正在使用的 zoo,会导致有效数据的丢失。其实这些问题很类似,这就是没有垃圾回收机制的语言管理内存时常遇到的问题。

另一个强引用可能带来的问题就是缓存,尤其是像图片这样的大文件的缓存。假设有一个程序需要处理用户提供的图片,通常的做法就是做图片数据缓存,因为从磁盘加载图片代价很大,并且同时也想避免在内存中同时存在两份一样的图片数据。

缓存被设计的目的就是避免去再次加载哪些不需要的文件。你会很快发现在缓存中会一直包含一个到已经指向内存中图片数据的引用。使用强引用会强制图片数据留在内存,这就需要决定什么时候图片数据不需要并且手动从缓存中移除,进而可以让垃圾回收器回收。因此再一次被强制做垃圾回收器该做的工作,并且人为决定是该清理到哪一个对象。

二、软引用(SoftReference)

软引用基本上和弱引用差不多,只是相比弱引用,它阻止垃圾回收器回收其指向的对象的能力强一些。如果一个对象是弱引用可到达,那么这个对象会被垃圾回收器接下来的回收周期销毁。但是如果是软引用可到达,那么这个对象会停留在内存更时间上长一些。当内存不足时垃圾回收器才会回收这些软引用可到达的对象。

由于软引用可到达的对象比弱引用可达到的对象滞留内存时间会长一些,可以利用这个特性来做缓存。这样的话,就可以节省很多事情,垃圾回收器会关心当前哪种可到达类型以及内存的消耗程度来进行处理。

三、弱引用(WeakReference)

Java 中的弱引用具体指的是java.lang.ref.WeakReference类,以下是官方文档对它做的说明:
弱引用对象的存在不会阻止它所指向的对象被垃圾回收器回收。弱引用最常见的用途是实现规范映射(canonicalizing mappings,比如哈希表)。
假设垃圾回收器在某个时间点决定一个对象是弱可达的(weakly reachable)(也就是说当前指向它的全都是弱引用),这时垃圾回收器会清除所有指向该对象的弱引用,然后把这个弱可达对象标记为可终结(finalizable)的,这样它随后就会被回收。

弱引用简单来说就是将对象留在内存的能力不是那么强的引用。使用 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 对象存在于内存中(比如已经卖出了这件产品),假设指向它的引用为 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)

四种引用类均继承自 Reference,其中有个构造函数:

Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
} 

该构造方法中提供了一个 ReferenceQueue 类型的参数,通过提供该参数,便把创建的引用对象注册到了一个引用队列上,如此当它被垃圾回收器清除时,就会把它送入这个引用队列中,便可以对这些被清除的引用对象进行统一管理。

六、总结

  1. 强引用(StrongReference):通常通过 new 来创建一个新对象时返回的引用就是一个强引用,若一个对象通过一系列强引用可到达,它就是强可达的(strongly reachable),那么它就不被回收。

  2. 软引用(SoftReference):软引用可达的对象在内存不充足时才会被回收,因此软引用要比弱引用“强”一些。

  3. 弱引用(WeakReference):弱引用简单来说就是将对象留在内存的能力不是那么强的引用。使用 WeakReference,垃圾回收器决定引用的对象何时回收并且将对象从内存移除。若一个对象是弱引用可达,无论当前内存是否充足它都会被回收。

  4. 虚引用(PhantomReference):虚引用是 Java 中最弱的引用,那么它弱到什么程度呢?通过虚引用无法获取到被引用的对象,虚引用存在的唯一作用就是当它指向的对象被回收后,虚引用本身会被加入到引用队列中,用作记录它指向的对象已被回收。

你可能感兴趣的:(JVM,java)