我们知道Java中的引用类型有四种:强引用(strong reference)、软引用(soft reference)、弱引用(weak reference)以及虚引用(phantom reference)。这四种引用类型使得Java中的对象有五种可达(Reachability)状态:强可达(strongly reachable)、软可达(softly reachable)、弱可达(weak reachable)、虚可达(phantom reachable)以及不可达(unreachable)。对象的可达性将会影响到该对象是否会被垃圾回收器回收,以及何时被回收。
这些可达性到底代表什么意思?它和四种引用类型有什么关系?
GC Roots
想要了解对象的可达性,首先要知道一个概念:GC Roots。(主要参考了《深入理解Java虚拟机》一书的3.2.2-3.2.4章节)
JVM在GC时要进行可达性分析,这个可达性就是相对于GC Roots的。GC Roots是JVM选定的一些对象,将这些对象作为起始点,搜索这些对象引用到的对象,接着再搜索新搜索到的对象所引用的对象...直到搜索不到任何新的对象。从GC Roots到某个可达对象的路径称为该对象的引用链(Reference Chain),对象的引用链并不唯一,一是因为一个对象可能会被多个GC Roots搜索到,二是同一个GC Root可能有不同的搜索路径访问到该对象。所有搜索到的对象,对于GC Roots来说都是可达的(可能是强可达,也可能是弱可达等等),所有未搜索到的但仍存活的对象都是不可达的,不可达的对象都是可回收的。但是并不是所有可达的对象就一定不会被回收,所有不可达的对象也不是一定会被回收(因为存在不可达对象重新变成可达对象的可能)。
Java中,可以作为GC Roots的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的变量。
- 方法区中常量引用的对象。
- 本地方法栈中引用的native对象。
可达性
前面说的以GC Roots为起始点做的可达性分析只将对象的可达性分成两种:可达和不可达。对于可达对象还可以细分为四种可达状态,虽然都是可达对象,但是JVM对不同可达状态下的对象是否进行回收以及何时进行回收是要区别对待的。
下面根据可达状态从强到弱依次介绍它们:
- 强可达对象:该对象存在这么一条引用链,这条引用链中的所有引用类型都是强引用,那么这个对象就是强可达的。什么样的引用类型是强引用类型?Java中普通的对象引用类型就是强引用类型。软引用、弱引用、虚引用类型需要通过java.lang.ref包下的SoftReference、WeakReference、PhantomReference类显示构造。所以一个对象的一条引用链中如果没有使用任何Reference对象(强引用没有对应的引用类型类),那它就是强可达的。GC时,强可达的对象是一定不会被回收的。
- 软可达对象:对象不是强可达的,但是存在这么一条引用链,这个引用链中除了强引用类型外,只有软引用类型,不存在弱引用或虚引用类型。GC时一般不会回收软可达对象,只有在JVM将要发生OOM之前才会对软可达对象进行回收。
- 弱可达对象:对象既不是强可达的也不是软可达的,但是对象的引用链中存在弱引用类型,当然也可以存在强引用和软引用类型,但不存在虚引用类型。当对象处于弱可达状态时,它会被标记为可回收,并在当前这一次GC时对它进行回收。
- 虚可达对象:对象既不是强可达的,也不是软可达的或弱可达的,对象的每一条引用链中都至少存在一个虚引用类型。当对象处于虚可达状态时,说明它已经被回收了。
引用类型
我们已经知道除了强引用类型外,其它的引用类型都需要通过指定的java.lang.ref.Reference的子类来构造:
public class ReferenceType {
private static class Foo {
}
public static void main(String[] args) {
// 强引用类型
Foo foo = new Foo();
// 软引用类型
SoftReference fooSoftReference = new SoftReference<>(new Foo());
SoftReference fooSoftReference1 = new SoftReference<>(new Foo(), new ReferenceQueue<>());
// 弱引用类型
WeakReference fooWeakReference = new WeakReference<>(new Foo());
WeakReference fooWeakReference1 = new WeakReference<>(new Foo(), new ReferenceQueue<>());
// 虚引用类型
PhantomReference fooPhantomReference = new PhantomReference<>(new Foo(), new ReferenceQueue<>());
}
}
那除了我们一直使用的强引用类型之外,其它三种引用类型在什么时候才能使用到呢?
对于软引用类型,SoftReference类的Javadoc有这样一句:
Soft references are most often used to implement memory-sensitive caches.
就是说软引用经常被用来实现内存敏感型的缓存。其实软引用就是用来引用那些有用但不是必需的对象,如果堆内存够用就保留着,如果不够用就清理掉。但是注意不要过度使用软引用,因为我们不知道这些对象何时会被回收,只能保证的是在发生OOM之前一定会回收这些对象(如果之前没被回收的话),而且不同的回收器针对软可达对象也会有不同的回收策略,如什么情况下才认为堆内存不够用了?堆内存不够用的话是先对堆进行扩容,还是先回收软可达对象?我在几个Android手机上进行测试发现,多数手机的虚拟机(delvik/art)会优先回收软可达对象,如果空闲内存仍然不够再进行堆的扩容。如果把那些你觉得可能会用到的,而实际上很少会用到的对象使用软引用类型来引用的话,那势必会造成堆的空闲内存减少,那么就可能会引起程序GC次数的增加或引起不必要的堆的扩容。
我们使用弱引用类型引用某个对象大多数情况下是为了不影响这个对象的回收。如何实现不影响回收的呢?前面我们说过当对象不是强可达也不是软可达时,如果引用链中存在弱引用,这个对象就是弱可达的,而弱可达的对象会被标记为可回收,并将在本次GC时对它进行回收。所以当我们使用了弱引用类型的话,对象就会可能从强可达状态或者软可达状态变成弱可达状态(因为引用链中存在弱引用类型),而弱可达状态的对象会在本次GC时被回收(有例外,后面会提到),所以基本上不会影响该对象的回收。同时我们又可以通过WeakReference.get()来获取被应用的对象,如果获取到的对象非空,说明对象还没到弱可达状态(仍是强可达或软可达状态),如果获取到的为空,说明该对象已经变成了弱可达状态,它很快就会被回收或已经被回收,所以我们不能再使用它了。
但是这里有一个可能会让人理解错误的地方,弱可达状态的对象是不是一定会在本次GC时被回收,或者说WeakReference.get() == null
时就一定表明它引用的对象要被回收了吗?
虽然大部分情况下的确是这样的,但并不是绝对的。当对象被标记为可回收后,还需要判断是否需要执行该对象的finalize()方法,如果需要则会把该对象放到一个队列中等待执行它的finalize()方法;而如果在执行finalize()方法时重新让外部的某个对象(强)引用到自身,那么这个对象就会从弱可达变成强可达,这样下次GC时它就不会被回收了(这就是我前面说的存在不可达对象重新变成可达对象的可能)。当然这种情况是很少见的,因为需要执行finalize()方法的条件是类重写了finalize()方法,并且该对象的finialize()方法还没有被执行过。但是最终得到的答案是弱可达状态的对象不一定会在下一次GC时被回收。
public class WeakReferenceTest {
private static Foo sFoo;
private static final class Foo {
@Override
protected void finalize() throws Throwable {
super.finalize();
// 在finalize()中把自身赋值到一个外部变量中
sFoo = this;
}
}
public static void main(String[] args) {
WeakReference weakReference = new WeakReference<>(new Foo());
assert weakReference.get() != null;
new Thread(() -> {
// 手动触发GC和finalization
Runtime.getRuntime().gc();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new AssertionError();
}
System.runFinalization();
Foo foo = weakReference.get();
if (foo == null) {
System.out.println("foo is gone");
} else {
System.out.println("foo is still here");
}
if (sFoo == null) {
System.out.println("sFoo is gone");
} else {
System.out.println("sFoo is still here");
}
}).start();
}
}
/**
* 输出:
* foo is gone
* sFoo is still here
*/
虚引用其实挺难让人理解的,因为它看似没有什么用。PhantomReference.get()总是返回null,那我要这样一个引用类型干什么?其实从上面创建几种引用类型实例的代码就可以看出来:PhantomReference不像SoftReference和WeakReference,它只有一个接受被引用对象和引用队列对象的构造函数,而SoftReference和WeakReference都可以不传入引用队列对象,PhantomReference.get()不是它的重点,重点是它所关联的那个引用队列。这个引用队列的作用是当该引用队列关联的虚引用对象所引用的对象变成虚可达状态时,这个虚引用对象本身(PhantomReference对象本身)会被(立即或在随后的某一时间)放入到这个引用队列中,这样我们就可以通过在引用队列中能否找到某个虚引用对象来判断它所引用的对象是否已经处于虚可达状态。
判断对象是否已经处于虚可达状态又有什么用呢?看一下PhantomReference类的Javadoc:
Phantom reference objects, which are enqueued after the collector determines that their referents may otherwise be reclaimed.
也就是说当这个虚引用对象被放入到引用队列中时,它所引用的对象已经被回收了。是真正的被回收了,不像弱引用那样还可以通过finalize()方法来逃脱回收。所以如果我们想判断某个对象是不是真正的被回收了,最准确的方法是使用PhantomReference,通过它关联的引用队列中是否存在该虚引用对象来判断。