JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference),虚引用(Phantom Reference)。这四种引用强度依次减弱。目前我们所熟知的普遍意义上的“引用”一般指的是是强引用。
最普遍的引用类型,日常开发过程中通过通过‘=’直接赋值所产生的引用就是强引用,类似Object obj = new Object();
。一个对象如果有一个有效的强引用(注意是有效的强引用,什么是有效引用可见【JAVA核心知识】2: JVM的垃圾回收与回收算法3.1:确定垃圾部分)那GC一定不会回收它。也就是说在JVM内存不足时,JVM宁愿抛出OOM(Out Of Memory Error),使程序终止,也不会回收一个拥有强引用的对象来解决内存不足的问题。
对象失去强引用的方式有两种:一种是对象的引用超出其作用域,在栈中的引用被覆盖或释放,二是显式的主动将对象的引用赋值为null。当对象的有效强引用都失去后,下一次JVM进行GC时就会对其进行销毁,实现内存回收。
软引用通过SoftReference类实现。对于只有软引用的对象,当堆内存足够时它不会被回收,当堆内存不足时就会被回收。
创建一个软引用:
String str = new String("SoftReference"); // 强引用
SoftReference<String> sr = new SoftReference<String>(str); // 软引用
str = null; // 消除强引用,只留下软引用
软引用可以用来做缓存,堆内存足够时数据可以安然的存在堆内存中,堆内存不足时又会被GC掉避免造成OOM。
在上面的代码中,可以看到定义String对象使用的是new String而不是直接写String str = "SoftReference";
这样做的原因通过下面的弱引用(Weak Reference)部分结合实际的GC前后的状态来解释。
另外SoftReference可以通过SoftReference(T referent, ReferenceQueue super T> q)构造方法使用ReferenceQueue来跟踪对象的回收状态,这个部分则放在虚引用(Phantom Reference)部分来解释。因为虚引用(Phantom Reference)的主要作用就是通过ReferenceQueue跟踪对象的回收状态。
弱引用通过WeakReference类来实现,它比软引用的强度更低,对于只有弱引用的对象来说,一旦进行GC,无论堆内存空间是否足够,它都会被回收掉。创建弱引用:
public class WeakReferenceDemo {
public static void main(String[] args) {
String strCon = "WeakReference";
WeakReference<String> strConWr = new WeakReference<String>(strCon); // 弱引用
strCon = null;
String strEnt = new String("WeakReference"); // 强引用
WeakReference<String> strEntWr = new WeakReference<String>(strEnt); // 弱引用
strEnt = null; // 消除强引用,只留下弱引用
System.out.println("strConWr GC前:" + strConWr.get()); // WeakReference
System.out.println("strEntWr GC前:" + strEntWr.get()); // WeakReference
System.gc(); // 通知JVM需要进行一次GC
try {
Thread.sleep(666); // GC并不是通知了JVM就会马上执行,这里等待JVM执行GC
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("strConWr GC后:" + strConWr.get()); // 这里依然返回WeakReference
System.out.println("strEntWr GC后:" + strEntWr.get()); // 这里返回null
}
}
执行结果:
strConWr GC前:WeakReference
strEntWr GC前:WeakReference
strConWr GC后:WeakReference
strEntWr GC后:null
可以看到,通过new String(“WeakReference”)创建的strEntWr在GC之后被回收掉返回了null,但是通过="WeakReference"直接赋值的strConWr在GC之后依然返回了WeakReference.这说明它引用的那一块内存并没有回收掉。原因在于直接赋值的strConWr所引用的字符串在字符串常量池,而通过new创建对象的strEntWr引用的字符串对象则在堆内存中。而GC回收的是堆内存空间,虽然字符串常量池在JDK1.7就已经移入到堆中,但是普通堆对象与常量池中的常量在GC条件与机制方面并不相同。所以就导致这里strEntWr被回收,而strConWr依然存在的情况。
* 关于JVM内存区域,可见【JAVA核心知识】1: JVM内存区域
弱引用一般与强引用联合使用,用做释放那些不便直接操作的对象。ThreadLocal所关联对象的释放就用到了弱引用。
例如创建一个ThreadLocal:
Object obj = new Object();
ThreadLocal<Object> tlo = new ThreadLocal<Object>();
tlo.set(obj);
ThreadLocal是被放在一个线程Thread所持有的ThreadLocalMap中并且是作为Key的(ThreadLocalMap内部维护了一个继承了WeakReference的ThreadLocalMap.Entry数组,利用开地址法确定下标,Entry弱引用ThreadLocal,并设置一个value强引用obj)。此时ThreadLocal就会同时存在两个引用:一个是tlo的强引用,一个是Thread->threadLocals(ThreadLocalMap)->entry->key(ThreadLocal)的弱引用。当tlo使用完之后,将tlo置为null或tlo超出其作用域从而消除强引用,ThreadLocal就只剩下一个弱引用,他会在下次GC时被回收。
如果entry->key也是强引用会发生什么情况呢?这种场景下ThreadLocal要满足回收条件,就需要entry断开对ThreadLocal的强引用。但是threadLocals被定义为default访问级别,而entry是ThreadLocalMap的内部类。也就说无法操作将entry的key的设置为null直接断开对ThreadLocal的强引用,或者接操控threadLocals去remove掉entry,使entry失去强引用间接断开对ThreadLocal的强引用。除此之外还可以通过销毁线程Thread,使得这个引用链变为无效链。但是如果Thread是线程池线程呢?线程池核心线程是不会被销毁的,自然不能限制线程池线程不能使用ThreadLocal,所以销毁线程的方法也变的不可行了。但是如果entry->key的强引用不消除又会使得ThreadLocal无法被回收,造成内存泄漏,这肯定是不允许的。而弱引用却能很好的解决这个问题,即满足了使用时的获取,又满足了无用时的回收。
WeakReference同样可以通过WeakReference(T referent, ReferenceQueue super T> q)使用ReferenceQueue来跟踪对象的回收状态,使用示例放在在虚引用部分。
JAVA类库提供了WeakHashMap类帮助开发者更方便的使用弱引用,它对key的引用就是弱引用。
虚引用也被称为幻影引用或者幽灵引用。它的强度比WeakReference还要低。一个对象是否有虚引用对其生存与回收没有任何影响,通过虚引用无法获取到对象实例。虚引用的唯一作用就是跟踪对象的回收状态。
虚引用使用于跟踪对象回收状态,或者在对象完全回收之后执行一些特地操作的场景。
通过虚引用创建对象回收通知:
public class PhantomReferenceDemo {
public static class MyPhantomReference<T> extends PhantomReference<T> {
String desc;
public MyPhantomReference(String desc, T referent, ReferenceQueue<T> q) {
super(referent, q);
this.desc = desc;
}
}
public static void main(String[] args) {
String str = new String("PhantomReference");
ReferenceQueue<String> rq = new ReferenceQueue<String>();
MyPhantomReference<String> pr = new MyPhantomReference<String>("虚引用测试对象", str, rq);
System.out.println(pr.get()); // null;无法通过虚引用获得对象实例
str = null; // 消除强引用
System.gc(); // 通知JVM需要进行一次GC
try {
Thread.sleep(666); // GC并不是通知了JVM就会马上执行,这里等待JVM执行GC
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(pr.get()); // null
MyPhantomReference<String> gcpr = (MyPhantomReference<String>) rq.poll();
if (gcpr != null) {
System.out.println(gcpr.get()); // null
System.out.println(gcpr.desc + "被回收了!"); //输出“虚引用测试对象被回收了!”
}
}
}
执行结果:
null
null
null
虚引用测试对象被回收了!
从ReferenceQueue中poll出的Reference无法获取其所指向的对象,在虚引用跟踪回收状态的演示中,可以看到从ReferenceQueue中poll出的gcpr.get()返回的是null。这不仅仅是因为虚引用对象无法获取到对象实例,而是因为在GC之后,所引用的对象实例已经被回收,所以从ReferenceQueue中poll的Reference实例的引用自然就是null了。这里在弱引用(WeakReference)演示中strEntWr.get()为null也可以证明了这一点,实际验证亦是如此。(GC后从ReferenceQueue中poll出的就是strEntWr。试想一下,如果GC之后依然可以通过Reference获得对象,那怎么能算GC呢?毕竟内存还占用着)
在java.lang.ref包下,除了SoftReference,WeakReference,PhantomReference,ReferenceQueue类外,还可以看到FinalReference类,以及继承了FinalReference的Finalizer。
它们的访问级别都是default,说明它们是JVM内存处理用的类。实际上它的作用依然与GC息息相关。在【JAVA核心知识】2: JVM的垃圾回收与回收算法的确定垃圾部分的可达性分析法中可以看到这样的描述:
第一次标记如果无有效引用的对象需要执行finalize,那么对象就会被放入一个名为F-Queue队列中。等待稍后的第二次标记。第二次标记:JVM会建立一个低优先级的线程finalizer线程来触发F-Queue队列中对象的finalize方法。
标记可回收的对象放到队列中,然后完成特定的动作(触发finalize方法),是不是很熟悉?Finalizer正是JVM GC的可达性分析法用来完成二次标记过程的(利用引用的机制)。
参考资料:
Java中的强引用,软引用,弱引用,虚引用有什么用?的回答
面试官:谈谈强引用、软引用、弱引用、幻象引用?
Java中的四种(强、软、弱和虚)引用
FinalReference详解