引用*
写在最前,本篇文章大部分来源于b站尚硅谷JVM全套教程的提炼,并附带自己的理解。主要是为了帮助自己理解,和用于复习。如果同时还能对其他人有所裨益,那就更好不过了。如果有谬误的地方,还请不吝指出。
介绍
JDK1.2后,java对引用的概念进行了扩充,分为强引用、软引用、弱引用、虚引用。
强引用(StrongReference)
最传统的“引用”,指在程序代码中普遍存在的引用赋值,无论任何情况下,只要强引用存在,垃圾回收器永远不会回收掉被引用的对象。
默认情况的引用类型都是强引用。
强引用的对象都是可触及的(详见finalize机制),垃圾回收永远不会回收。
当且仅当一个对象没有其它引用关系,且当前引用被赋值为null(或过了作用域)就可以被垃圾回收了。
所以,强引用是造成java内存泄漏的主要原因之一。
特点:
- 强引用可以直接访问目标对象
- 所指向对象在任何时候都不会被系统回收,而宁愿抛出OOM异常
- 强引用可能导致内存泄漏
软引用(SoftReference)- 内存不足即回收
在发生内存溢出之前,会把对象列入回收范围内进行第二次回收。如果回收后还没有足够内存,才会抛出内存溢出异常。
即先回收不可触及的对象,如果还没有足够内存,则回收软引用关联对象。
通常用来实现内存敏感的缓存。
垃圾回收期在某个时刻决定回收可达对象的时候,会清理掉软引用,并可选地把引用存放在一个引用队列。
内存够:不会回收
代码示例
设置VM参数-Xms10m -Xmx10m -XX:+PrintGCDetails
public static void main(String[] args) {
byte[] b = new byte[1024*1024*2];
SoftReference<byte[]> s = new SoftReference<>(new byte[1024*1024*3]);
System.out.println(s.get());
try{
b=new byte[1024*1024*4];
}finally {
System.out.println(s.hashCode());
}
System.out.println(s.get());
}
我们发现,在内存不够的情况下,第一个打印语句不为null,第二个打印语句为null,没有抛出OOM异常,说明已经被回收。
怎么证明当内存足够时,垃圾回收器不会回收软引用呢?
public static void main(String[] args) {
byte[] b = new byte[1024*1024*2];
SoftReference<byte[]> s = new SoftReference<>(new byte[1024*1024*3]);
System.gc();
System.out.println(s.get());
}
当我们强制使用System.gc()时,通过打印语句可以看出JVM进行了垃圾回收,然而并没有回收软引用指向的对象。
弱引用(WeakReference)- 发现即回收
只能生存到下一次垃圾收集之前,当垃圾收集器工作时,无论空间是否足够,都会回收掉只被弱引用关联的对象。
但是,由于垃圾回收器的线程优先级很低,并不一定能很快发现持有弱引用的对象,这种情况下,弱引用对象可存在较长时间。
弱引用和软引用一样,在构造弱引用时,也可以制定一个引用队列,当弱引用对象被回收时,就会加入指定队列,通过队列可以跟踪对象回收情况。
虚引用(PhantomReference)
是否有虚引用,完全不会对对象的生存时间构成影响,也无法通过虚引用获取一个对象的实例,唯一作用是在被回收时,收到一个系统通知。
当试图通过虚引用的get()方法得到对象时,只会得当null
注意,必须要提供引用队列作为参数,当垃圾回收器准备回收对象时,如果还有虚引用,就会在回收对象后,将虚引用加入引用队列,以通知回收情况。
由于虚引用可以跟踪对象的回收时间,可以将资源释放操作放置在虚引用执行和记录
终结器引用(FinalReference)
这一块宋老师讲的比较少
FinalReference类仅对同一个包的类可见,意味着我们不能创建其实例。
Finalizer类继承了这个FR类
我们可以通过在类中实现finalize()方法来达到使用此引用的目的。
只要实现了finalize()方法,且方法体不为空,则就会被标记为一个finalizer(在类被加载过程中就已经被标记了)
之前讲的finalize的机制还要更复杂一些
简单来说就是:当一个finalier仅在Finalizer类的对象中被注册(即仅被这个对象引用),说明可以执行finalize()方法了(即可以被回收),所以就会把Finalizer对象放在Finalizer类的ReferenceQueue里,gc完成之前,jvm会唤醒RQ(如果RQ不为空,且注意这个唤醒并非一定能使得此线程马上获得锁),将会从队列中移出Finalizer对象,然后触发runFinalizer()方法(即得到其注册的finalizer,并会在本地方法中调用finalize()方法)。
在Finalizer类中创建了一个Finalizer线程,在run()方法里面会执行这个Finalizer类对象的runFinalizer()方法,由于优先级不是特别高,所以会有之前所说的finalize()的执行时间不确定的情况。
finalize()方法为什么只会执行一次?
当执行完第一次以后,finalizer已经和Finalizer对象脱离关系了,下次GC时就会发现没有引用再指向该finalizer,所以就不会调用其finalize()方法了。
以上都为对JVM源码分析之FinalReference完全解读的解读,若想更深入理解,可以移步这里。
小结:
一个finalizer至少得经历两次GC才能被成功清除。
- 第一次GC时,发现有Finalize对象里的变量强引用指向这个finalizer,故只能将其将入RQ,而不回收它。注意,GC时要获取一致性快照,即不能发生对象引用关系的变化,所以第一次GC不可能回收掉finalizer。
- 如果Finalizer线程抢到了运行时间,且RQ不为空,那么它可能幸运地将RQ中的Finalizer对象弹出,并调度finalizer的finalize()方法。
- 如果此方法没有使得这个对象复活,那么下一次GC时,对象就会被回收。所以最少是两次GC。
如果Finalizer线程还没有执行,或者没有执行到finalize()方法,那么可能会等待多个GC才能被回收。那么对象可能就会进入老年代,而老年代需要major GC或者full GC来进行回收了,这样代价就会很大了。
注意,finalize()方法被调用且对象没被复活,并不代表对象就被回收了,而是在不久的将来被回收(下一次GC)。