垃圾回收 —— 引用的分类

知识框架:


  1. 强引用:永远不会被回收
  2. 软引用:在内存不足时才被回收
  3. 弱引用:下一次回收时就会被回收
  4. 虚引用:无法根据虚引用来访问对象,其作用只是在回收前能收到一个通知 ~ 引用队列
  5. 注意:如果直接使用字符串字面量,是不受这些引用影响的,因为放在字符串常量池了

传统对引用的定义只有:被引用和未被引用,但这就无法描述一些 ”食之无味,弃之可惜“ 的对象

如果我们希望能描述一类对象:当内存空间还足够时,能保留在内存之中,如果内存空间在进行垃圾收集后仍然非常紧张,那就可以抛弃这些对象,那就需要更细致的引用分类了

在 JDK 1.2 版之后,Java 对引用的概念进行了扩充,将引用分为以下四种,由强到弱依次为:

1. 强引用(Strong Reference)

一定不会被回收,是最传统的 “引用” 的定义,指在程序代码之中普遍存在的引用赋值

无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象

2. 软引用(Soft Reference)

内存不足时才被回收,用来描述一些还有用,但非必须的对象,在 内存要溢出时 才回收

在系统 将要 发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。通常用来作为缓存,在内存空间足够时就保留对象,不足时进行清理,如:

1
2
3
4
SoftReference<StringBuilder> soft = new SoftReference<>(new StringBuilder("AAA"));
System.gc();
System.runFinalization();
System.out.println(soft.get());

建立软引用之后,可以通过 get 来获得对象或者重新建立强引用
即使强制进行了垃圾回收(gc() + runFinalization()),但在内存足够时也不会清除软引用对象

3. 弱引用(Weak Reference)

下一次垃圾回收时就会被回收,用来描述那些非必须对象,但是它的强度比软引用更弱一些

被弱引用关联的对象只能生存到 下一次垃圾收集 发生为止,当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象,如下:

**▲ 注意:**在实践中发现,如果弱引用的对象是 字符串,那么下一次垃圾回收并不会被回收掉

1
2
3
4
WeakReference<String> weak = new WeakReference<>("aaa");
System.out.println(weak.get());
System.gc();
System.out.println(weak.get());

上边这样写的话,gc 之后依旧能够拿到字符串对象,原因是直接使用字符串字面量,是将引用指向了字符串常量池中的字符串,如果使用 new String(“”) 显示创建的话,就会在堆中有 String 对象,从而被垃圾回收器回收

1
2
3
4
WeakReference<StringBuilder> weak = new WeakReference<>(new StringBuilder("bbb"));
System.out.println(weak.get());
System.gc();
System.out.println(weak.get());

这样 gc 后取到的对象就为 null 了

4. 虚引用(Phantom Reference)

仅仅用来跟踪对象的回收,也称为 “幽灵引用” 或者 “幻影引用”,它是最弱的一种引用关系

一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。设置虚引用的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知,如下:

▲ 注意:还是和弱引用一样,如果直接使用字符串字面量的话,对象并不会被回收!这在 GC 后调用 remove 方法依旧阻塞可以看出来

1
2
3
4
5
6
7
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<StringBuilder> phantom = new PhantomReference<>(new StringBuilder("ccc"), queue);
System.out.println(phantom.get());
// System.out.println(queue.remove());
System.gc();
System.out.println(phantom.get());
System.out.println(queue.remove());

在这个例子中,注释掉的那一句会阻塞,因为队列中没东西
当进行了依次 gc 后,再 remove 后会发现多了一个对象,这个就是虚引用对象的地址

4.1. 引用队列(Reference Queue)

上面的代码中出现了一个引用队列,当 GC 准备回收一个对象时,如果发现它还仅有软引用(或弱引用,或虚引用)指向它,就会在回收该对象之前,把这个软引用(或弱引用,或虚引用)加入到与之关联的引用队列(ReferenceQueue)中。

这样只需要遍历这个队列,就能够知道哪些对象被回收了,而不需要遍历各个对象执行 get() 去判断对象是否已经被回收,能很大程度提高效率

当软引用(或弱引用,或虚引用)对象所指向的对象被回收了,那么这个引用对象本身就没有价值了,如果程序中存在大量的这类对象(注意,我们创建的软引用、弱引用、虚引用的 对象本身是个强引用,不会自动被 GC 回收)就会浪费内存。因此我们这就可以手动回收位于引用队列中的引用对象本身

即:在引用队列中出现了某个对象,那就可以手动将某个对象的引用对象回收了,节省内存