Java核心技术-高级特性(2)- SoftReference, WeakReference and PhantomReference

时间:2022-03-22 19:18:14

Java.lang.ref 是 Java 类库中比较特殊的一个包,它提供了与 Java 垃圾回收器密切相关的引用类。这些引用类对象可以指向其它对象,但它们不同于一般的引用,因为它们的存在并不防碍 Java 垃圾回收器对它们所指向的对象进行回收。其好处就在于使者可以保持对使用对象的引用,同时 JVM 依然可以在内存不够用的时候对使用对象进行回收。因此这个包在用来实现与缓存相关的应用时特别有用。同时该包也提供了在对象的“可达”性发生改变时,进行 提醒的机制。本文通过对该包进行由浅入深的介绍与分析,使读者可以加深对该包的理解,从而更好地利用该包进行开发。
java.lang.ref 包的介绍

我们可以先来看一下 java.lang.ref 这个包的结构,如图 1 所示

Java核心技术-高级特性(2)- SoftReference, WeakReference and PhantomReference

图 2. java.lang.ref 包中类的继承关系 :

Java核心技术-高级特性(2)- SoftReference, WeakReference and PhantomReference

Reference 是一个抽象类,而 SoftReference,WeakReference,PhantomReference 以及 FinalReference 都是继承它的具体类。

接下来我们来分别介绍和分析强引用以及 java.lang.ref 包下各种虚引用的特性及用法。

StrongReference, SoftReference, WeakReference 以及 PhantomReference 的特性及用法

StrongReference:

我们都知道 JVM
中对象是被分配在堆(heap)上的,当程序行动中不再有引用指向这个对象时,这个对象就可以被垃圾回收器所回收。这里所说的引用也就是我们一般意义上申
明的对象类型的变量(如 String, Object, ArrayList 等),区别于原始数据类型的变量(如 int, short, long
等)也称为强引用。

在了解虚引用之前,我们一般都是使用强引用来对对象进行引用。如:

  1. String tag = new String("T");

此处的 tag 引用就称之为强引用。而强引用有以下特征:

1.强引用可以直接访问目标对象。

2.强引用所指向的对象在任何时候都不会被系统回收。

3.强引用可能导致内存泄漏。

我们要讨论的这三种 Reference 较之于强引用而言都属于“弱引用”,也就是他们所引用的对象只要没有强引用,就会根据条件被 JVM 的垃圾回收器所回收,它们被回收的时机以及用法各不相同。下面分别来进行讨论。

SoftReference:

SoftReference 在“弱引用”中属于最强的引用。SoftReference
所指向的对象,当没有强引用指向它时,会在内存中停留一段的时间,垃圾回收器会根据 JVM 内存的使用情况(内存的紧缺程度)以及
SoftReference 的 get() 方法的调用情况来决定是否对其进行回收。(后面章节会用几个实验进行阐述)

具体使用一般是通过 SoftReference 的构造方法,将需要用弱引用来指向的对象包装起来。当需要使用的时候,调用
SoftReference 的 get() 方法来获取。当对象未被回收时 SoftReference 的 get()
方法会返回该对象的强引用。如下:

  1. SoftReference<Bean> bean = new SoftReference<Bean>(new Bean("name", 10));
  2. System.out.println(bean.get());// “name:10”

软引用有以下特征:

1.软引用使用 get() 方法取得对象的强引用从而访问目标对象。

2.软引用所指向的对象按照 JVM 的使用情况(Heap 内存是否临近阈值)来决定是否回收。

3.软引用可以避免 Heap 内存不足所导致的异常。

当垃圾回收器决定对其回收时,会先清空它的 SoftReference,也就是说 SoftReference 的 get() 方法将会返回 null,然后再调用对象的 finalize() 方法,并在下一轮 GC 中对其真正进行回收。

WeakReference:

WeakReference 是弱于 SoftReference 的引用类型。弱引用的特性和基本与软引用相似,区别就在于弱引用所指向的对象只要进行系统垃圾回收,不管内存使用情况如何,永远对其进行回收(get() 方法返回 null)。

完全可以通过和 SoftReference 一样的方式来操作 WeakReference,这里就不再复述。

弱引用有以下特征:

1.弱引用使用 get() 方法取得对象的强引用从而访问目标对象。

2.一旦系统内存回收,无论内存是否紧张,弱引用指向的对象都会被回收。

3.弱引用也可以避免 Heap 内存不足所导致的异常。

PhantomReference:

PhantomReference 是所有“弱引用”中最弱的引用类型。不同于软引用和弱引用,虚引用无法通过 get() 方法来取得目标对象的强引用从而使用目标对象,观察源码可以发现 get() 被重写为永远返回 null。

那虚引用到底有什么作用?其实虚引用主要被用来 跟踪对象被垃圾回收的状态,通过查看引用队列中是否包含对象所对应的虚引用来判断它是否
即将被垃圾回收,从而采取行动。它并不被期待用来取得目标对象的引用,而目标对象被回收前,它的引用会被放入一个 ReferenceQueue
对象中,从而达到跟踪对象垃圾回收的作用。

所以具体用法和之前两个有所不同,它必须传入一个 ReferenceQueue 对象。当虚引用所引用对象被垃圾回收后,虚引用会被添加到这个队列中。如:

  1. public static void main(String[] args) {
  2. ReferenceQueue<String> refQueue = new ReferenceQueue<String>();
  3. PhantomReference<String> referent = new PhantomReference<String>(
  4. new String("T"), refQueue);
  5. System.out.println(referent.get());// null
  6. System.gc();
  7. System.runFinalization();
  8. System.out.println(refQueue.poll() == referent); //true
  9. }

值得注意的是,对于引用回收方面,虚引用类似强引用不会自动根据内存情况自动对目标对象回收,Client 需要自己对其进行处理以防 Heap 内存不足异常。

虚引用有以下特征:

虚引用永远无法使用 get() 方法取得对象的强引用从而访问目标对象。

虚引用所指向的对象在被系统内存回收前,虚引用自身会被放入 ReferenceQueue 对象中从而跟踪对象垃圾回收。

虚引用不会根据内存情况自动回收目标对象。

另外值得注意的是,其实 SoftReference, WeakReference 以及 PhantomReference
的构造函数都可以接收一个 ReferenceQueue 对象。当 SoftReference 以及 WeakReference
被清空的同时,也就是 Java 垃圾回收器准备对它们所指向的对象进行回收时,调用对象的 finalize() 方法之前,它们自身会被加入到这个
ReferenceQueue 对象中,此时可以通过 ReferenceQueue 的 poll() 方法取到它们。而
PhantomReference 只有当 Java 垃圾回收器对其所指向的对象真正进行回收时,会将其加入到这个 ReferenceQueue
对象中,这样就可以追综对象的销毁情况。

以下示例是think in java中的示例:

  1. package yudong.containers;
  2. //: containers/References.java
  3. // Demonstrates Reference objects
  4. import java.lang.ref.*;
  5. import java.util.*;
  6. class VeryBig {
  7. private static final int SIZE = 10000;
  8. private long[] la = new long[SIZE];
  9. private String ident;
  10. public VeryBig(String id) {
  11. ident = id;
  12. }
  13. public String toString() {
  14. return ident;
  15. }
  16. protected void finalize() {
  17. System.out.println("Finalizing " + ident);
  18. }
  19. }
  20. public class References {
  21. private static ReferenceQueue<VeryBig> rq = new ReferenceQueue<VeryBig>();
  22. public static Reference<? extends VeryBig> checkQueue() {
  23. Reference<? extends VeryBig> inq = rq.poll();
  24. System.out.println("inq : =" + inq);
  25. if (inq != null)
  26. System.out.println("In queue: " + inq.get());
  27. return inq;
  28. }
  29. public static void main(String[] args) throws InterruptedException {
  30. int size = 10;
  31. // Or, choose size via the command line:
  32. if (args.length > 0)
  33. size = new Integer(args[0]);
  34. /* 下面这个循环中的对象被包装成SoftReference,而且程序中没有这些对象的强引用
  35. * 那么在JVM还没有out of memory的时候就不会回收,这些VeryBig对象,也就不会执行finalize()方法。
  36. */
  37. LinkedList<SoftReference<VeryBig>> sa = new LinkedList<SoftReference<VeryBig>>();
  38. for (int i = 0; i < size; i++) {
  39. sa.add(new SoftReference<VeryBig>(new VeryBig("Soft " + i), rq));
  40. System.out.println("Just created: " + sa.getLast());
  41. checkQueue();
  42. }
  43. /*下面这些对象被包装成WeakReference,在没有强引用的时候,gc会将它们都进行标记
  44. * 表示可以被回收,在被回收之前,对象会执行finalize()方法。
  45. */
  46. LinkedList<WeakReference<VeryBig>> wa = new LinkedList<WeakReference<VeryBig>>();
  47. for (int i = 0; i < size; i++) {
  48. wa.add(new WeakReference<VeryBig>(new VeryBig("Weak " + i), rq));
  49. System.out.println("Just created: " + wa.getLast());
  50. checkQueue();
  51. }
  52. SoftReference<VeryBig> s = new SoftReference<VeryBig>(new VeryBig("Soft"));
  53. WeakReference<VeryBig> w = new WeakReference<VeryBig>(new VeryBig("Weak"));
  54. PhantomReference<VeryBig> z = new PhantomReference<VeryBig>(new VeryBig("Weak"), rq);
  55. LinkedList<PhantomReference<VeryBig>> pa = new LinkedList<PhantomReference<VeryBig>>();
  56. for (int i = 0; i < size; i++) {
  57. pa.add(new PhantomReference<VeryBig>(new VeryBig("Phantom " + i),rq));
  58. System.out.println("Just created: " + pa.getLast());
  59. checkQueue();
  60. }
  61. }
  62. } /* (Execute to see output) */// :~

以下这个示例充分说明了使用PhantomReference的情况,有人发现一旦实现了finalize后,并不会把
PhantomReference加入referenceQueue,运行以下代码发现,其实第一次GC并未真正进行回收,在第二次运行gc后,通过反射
机制PhantomReference的referent,我们拿到了真正的原始对象:

  1. package yudong.containers;
  2. import java.lang.ref.PhantomReference;
  3. import java.lang.ref.Reference;
  4. import java.lang.ref.ReferenceQueue;
  5. import java.lang.reflect.Field;
  6. public class Test2 {
  7. public static boolean isRun = true;
  8. public static void main(String[] args) throws Exception {
  9. VeryBig abc = new VeryBig("abc");
  10. System.out.println(abc.getClass() + "@" + abc.hashCode());
  11. final ReferenceQueue<VeryBig> referenceQueue = new ReferenceQueue<VeryBig>();
  12. new Thread() {
  13. public void run() {
  14. while (isRun) {
  15. Object o = referenceQueue.poll();
  16. if (o != null) {
  17. try {
  18. Field rereferent = Reference.class.getDeclaredField("referent");
  19. rereferent.setAccessible(true);
  20. Object result = rereferent.get(o);
  21. System.out.println("gc will collect:"+ result.getClass() + "@"+ result.hashCode());
  22. } catch (Exception e) {
  23. e.printStackTrace();
  24. }
  25. }
  26. }
  27. }
  28. }.start();
  29. PhantomReference<VeryBig> abcWeakRef = new PhantomReference<VeryBig>(abc,referenceQueue);
  30. abc = null;
  31. System.gc();
  32. System.out.println("first gc");
  33. Thread.currentThread().sleep(3000);
  34. System.out.println(referenceQueue.poll());
  35. System.gc();
  36. System.out.println("second gc");
  37. Thread.currentThread().sleep(3000);
  38. isRun = false;
  39. }
  40. }

不同 Java 虚拟机上的表现与分析

让我们来回顾一下四种引用类型的表现以及在垃圾回收器回收清理内存时的表现 .

软引用 (SoftReference), 引用类型表现为当内存接近满负荷 , 或对象由 SoftReference.get()
方法的调用没有发生一段时间后 , 垃圾回收器将会清理该对象 . 在运行对象的 finalize 方法前 , 会将软引用对象加入
ReferenceQueue 中去 .

弱引用 (WeakReference), 引用类型表现为当系统垃圾回收器开始回收时 , 则立即会回收该对象的引用 . 与软引用一样 , 弱引用也会在运行对象的 finalize 方法之前将弱引用对象加入 ReferenceQueue.

强引用 (FinalReference), 这是最常用的引用类型 . JVM 系统采用 Finalizer 来管理每个强引用对象 , 并将其被标记要清理时加入 ReferenceQueue, 并逐一调用该对象的 finalize() 方法 .

虚引用 (PhantomReference), 这是一个最虚幻的引用类型 . 无论是从哪里都无法再次返回被虚引用所引用的对象 .
虚引用在系统垃圾回收器开始回收对象时 , 将直接调用 finalize() 方法 , 但不会立即将其加入回收队列 . 只有在真正对象被 GC
清除时 , 才会将其加入 Reference 队列中去 .