最近在看一些数据结构的源码的时候发现了Reference这个类,突然就想起来关于Java引用的知识,并不了解里面真实的知识,今天就来深入源码来好好学习一番。所有的源码都在java.lang.ref包下面。注意体会里面的设计模式,多想想总有收获。
基础
Java引用体系中我们最熟悉的就是强引用类型,如 A a= new A();这是我们经常说的强引用StrongReference,jvm gc时会检测对象是否存在强引用,如果存在由根对象对其有传递的强引用,则不会对其进行回收,即使内存不足抛出OutOfMemoryError。
除了强引用外,Java还引入了SoftReference,WeakReference,PhantomReference,FinalReference.Java额外引入这个四种类型引用主要目的是在jvm 在gc时,按照引用类型的不同,在回收时采用不同的逻辑。可以把这些引用看作是对对象的一层包裹,jvm根据外层不同的包裹,对其包裹的对象采用不同的回收策略,或特殊逻辑处理。 这几种类型的引用主要在jvm内存缓存、资源释放、对象可达性事件处理等场景会用到。
对象可达性判断
垃圾回收时会依据两个原则来判断对象的可达性:
- 单一路径中,以最弱的引用为准
- 多路径中,以最强的引用为准
ReferenceQueue & Reference
Reference作为SoftReference,WeakReference,PhantomReference,FinalReference这几个引用类型的父类。主要有两个字段referent、queue,一个是指所引用的对象,一个是与之对应的ReferenceQueue。Reference类有个构造函数 Reference(T referent, ReferenceQueue<? super T> queue)
,可以通过该构造函数传入与Reference相伴的ReferenceQueue。
ReferenceQueue本身提供队列的功能,有入队(enqueue)和出队(poll,remove,其中remove阻塞等待提取队列元素)。ReferenceQueue对象本身保存了一个Reference类型的head节点,Reference封装了next字段,这样就是可以组成一个单向链表。这种元素包含Queue的方式,确实同时ReferenceQueue提供了两个静态字段NULL,ENQUEUED. Null是内部定义的一个空类。
static ReferenceQueue<Object> NULL = new Null<>();
static ReferenceQueue<Object> ENQUEUED = new Null<>();
这两个字段的主要功能:NULL是当我们构造Reference实例时queue传入null时,会默认使用NULL,这样在enqueue时判断queue是否为NULL,如果为NULL直接返回,入队失败。ENQUEUED的作用是防止重复入队,reference后会把其queue字段赋值为ENQUEUED,当再次入队时会直接返回失败。
boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
synchronized (r) {
// 如果已经enqueue了,就不需要再做
if (r.queue == ENQUEUED) return false;
synchronized (lock) {
// 设置ENQUEUE
r.queue = ENQUEUED;
// 没有设置head的话,r开始为链表的头元素,否则就是头结点的插入
r.next = (head == null) ? r : head;
head = r;
// queue长度+1;
queueLength++;
if (r instanceof FinalReference) {
// 如果是FinalReference的话,VM里面的计数+1
sun.misc.VM.addFinalRefCount(1);
}
// 通知所有的lock.wait,这个是Reference里面的线程ReferenceHandler调用
lock.notifyAll();
return true;
}
}
}
我们来看看Reference的线程组,wait和notify调用的实例:
private static class ReferenceHandler extends Thread {
// 加入某个线程组里面去,取个名字
ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}
public void run() {
for (;;) {
Reference r;
synchronized (lock) {
if (pending != null) {
r = pending;
Reference rn = r.next;
pending = (rn == r) ? null : rn;
r.next = r;
} else {
try {
// 否则就进行阻塞等待,是在ReferenceQueue中调用的notify方法
lock.wait();
} catch (InterruptedException x) { }
continue;
}
}
// Fast path for cleaners
// 如果Reference实现了Cleaner接口,那么就调用clean方法来获取我们想要的值
// 做一些清理操作。这个类在dt.jar中实现的。
if (r instanceof Cleaner) {
((Cleaner)r).clean();
continue;
}
ReferenceQueue q = r.queue;
// 将ReferenceQueue加入到Queue中
if (q != ReferenceQueue.NULL) q.enqueue(r);
}
}
}
Reference与ReferenceQueue之间是如何工作的呢?Reference里有个静态字段pending,同时还通过静态代码块启动了Reference-handler thread。当一个Reference的referent被回收时,垃圾回收器会把reference添加到pending这个链表里,然后Reference-handler thread不断的读取pending中的reference,把它加入到对应的ReferenceQueue中。我们可以通过下面代码块来进行把SoftReference,WeakReference,PhantomReference与ReferenceQueue联合使用来验证这个机制。为了确保SoftReference在每次gc后,其引用的referent都被回收,我们需要加入-XX:SoftRefLRUPolicyMSPerMB=0参数,这个原理下文中会在讲。
/**
* 为了确保System.gc()后,SoftReference引用的referent被回收需要加入下面的参数
* -XX:SoftRefLRUPolicyMSPerMB=0
*/
public class ReferenceTest {
private static List<Reference> roots = new ArrayList<>();
public static void main(String[] args) throws Exception {
ReferenceQueue rq = new ReferenceQueue();
new Thread(new Runnable() {
@Override
public void run() {
int i=0;
while (true) {
try {
// 这个是只读取pending里面的值
Reference r = rq.remove();
System.out.println(“reference:”+r);
//为null说明referent被回收
System.out.println( “get:”+r.get());
i++;
System.out.println( “queue remove num:”+i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
for(int i=0;i<100000;i++) {
byte[] a = new byte[1024*1024];
// 分别验证SoftReference,WeakReference,PhantomReference
Reference r = new SoftReference(a, rq);
//Reference r = new WeakReference(a, rq);
//Reference r = new PhantomReference(a, rq);
roots.add(r);
System.gc();
System.out.println(“produce”+i);
TimeUnit.MILLISECONDS.sleep(100);
}
}
}
通过jstack命令可以看到对应的Reference Handler thread
“Reference Handler” #2 daemon prio=10 os_prio=31 tid=0x00007f8fb2836800 nid=0x2e03 in Object.wait() [0x000070000082b000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
– waiting on <0x0000000740008878> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
– locked <0x0000000740008878> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
当reference与referenQueue联合使用的主要作用就是当reference指向的referent回收时(或者要被回收 如下文要讲的Finalizer),提供一种通知机制,通过queue取到这些reference,来做额外的处理工作。当然,如果我们不需要这种通知机制,我们就不用传入额外的queue,默认使用NULL queue就会入队失败。
SoftReference
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 JVM不仅仅只会考虑当前内存情况,还会考虑软引用所指向的referent最近使用情况和创建时间来综合决定是否回收该referent。这部分需要考虑Hotspot的源码
String str=new String("abc"); // 强引用
SoftReference<String> softRef=new SoftReference<String>(str); // 软引用
SoftReference的源代码
public class SoftReference<T> extends Reference<T> {
/**
* 时间戳时钟,垃圾回收机制更新
*/
static private long clock;
/**
* 使用getter的时候会进行更新。当soft引用被声明时,VM使用这个字段,但是这个不是要求
*/
private long timestamp;
public SoftReference(T referent) {
super(referent);
this.timestamp = clock;
}
public SoftReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
this.timestamp = clock;
}
/**
* 使用的时候计数
*/
public T get() {
T o = super.get();
if (o != null && this.timestamp != clock)
this.timestamp = clock;
return o;
}
}
这里需要特别注意的是:如果错误地使用了软引用,可能会引起频繁的gc,这个是大家都不想见到的,所以使用的时候还是需要注意一下。
弱引用(WeakReference)
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。相比SoftReference来说,WeakReference对JVM GC几乎是没有影响的。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
其实任何一个引用都可以与ReferenceQueue关联。
虚引用(PhantomReference)
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
强引用(StrongReference)
就是我们平时的new,强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。
FinalReference
FinalReference 引用类型主要是为虚拟机提供的,提供 对象被gc前需要执行finalize方法的对象 的机制。
FinalReference 很简单就是extend Reference类,没有做其他逻辑,只是把访问权限改为package,因此我们是无法直接使用的。Finalizer类是我们要讲的重点,它继承了FinalReference,并且是final 类型的。Finalize实现很简单,也是利用上面我们讲的ReferenceQueue VS Reference机制。
FinalizerThread
Finalizer静态代码块里启动了一个deamon线程,我们通过jstack命令查看线程时,总会看到一个Finalizer线程,就是这个原因:
/**
* 启动一个FinalizerThread线程
*/
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread finalizer = new FinalizerThread(tg);
// 执行权限稍微低一些
finalizer.setPriority(Thread.MAX_PRIORITY - 2);
finalizer.setDaemon(true);
finalizer.start();
}
FinalizerThread run方法是不断的从queue中去取Finalizer类型的reference,然后执行runFinalizer释放方。我们来看一下这个方法:
private void runFinalizer() {
synchronized (this) {
// 如果已经执行过Finalized方法,返回
if (hasBeenFinalized()) return;
remove();
}
try {
Object finalizee = this.get();
if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
// 调用finalizee方法
invokeFinalizeMethod(finalizee);
/* Clear stack slot containing this variable, to decrease
the chances of false retention with a conservative GC */
finalizee = null;
}
} catch (Throwable x) { }
super.clear();
}
可以看出如果finalize方法中抛出异常会被直接吃掉
如何使用Finalizer
只要类覆写了Object 上的finalize方法,方法体非空。那么这个类的实例都会被Finalizer引用类型引用的。下文中我们简称Finalizer 型的referent为finalizee。
Finalizer的构造函数是private的,也就是不能通过new 来生成一个Fianlizer reference。只能通过静态的register方法来生成。同时Finalizer有个静态字段unfinalized,维护了一个未执行finalize方法的reference列表,在构造函数中通过add()方法把Finalizer引用本身加入到unfinalized列表中,同时关联finalizee和queue,实现通知机制。维护静态字段unfinalized的目的是为了一直保持对未未执行finalize方法的reference的强引用,防止被gc回收掉。
那么register是被VM何时调用的呢?JVM通过VM参数 RegisterFinalizersAtInit 的值来确定何时调用register,RegisterFinalizersAtInit默认为true,则会在构造函数返回之前调用。
何时入queue
当一个finalizee 只剩Finalizer引用,没有其他引用时,需要被回收了,GC就会把该finalizee对应的reference放到Finalizer的refereneQueue中,等待FinalizerThread来执行finalizee的finalize方法,然后finalizee对象才能被GC回收。