0%

JDK Reference Object

对 Java 的五种引用(强引用、软引用、弱引用、虚引用、终引用)进行了概念上的介绍,着重介绍了 Reference 配合 ReferenceQueue 的状态流转、ReferenceQueue 的原理介绍和 FinalReference 与 JVM 的调用关系和原理。

Reference Object

Java 引用有五种类型:强(strong)引用、软(soft)引用、弱(weak)引用、虚(phantom)引用、终(final)引用。除了强引用,其他几种引用均继承java.lang.ref.Reference

Strong Reference

最常用的引用类型,在执行下面的语句时,变量 o 即为一个强引用。

强引用(Strong Reference)是我们平时使用最多的一种对象引用,当一个对象被关键字 new 实例化出来的时候,JVM 会在堆(heap)内存中开辟一片内存区域,用于存放与该实例对应的数据结构。JVM 垃圾回收器线程会在达到 GC 条件的时候尝试回收(Full GC,Young GC)堆栈内存中的数据,强引用的特点是只要引用到 ROOT 根的路径可达,无论怎样的 GC 都不会将其释放,而是宁可出现 JVM 内存溢出。

Object o = new Object();

Reference基本用法

//T 被引用的对象
Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

如果我们在创建一个引用对象时,指定了ReferenceQueue,那么当引用对象指向的对象达到合适的状态(根据引用类型不同而不同)时,GC 会把引用对象本身添加到这个队列中,方便我们处理它,因为引用对象指向的对象 GC 会自动清理,但是引用对象本身也是对象(是对象就占用一定资源),所以需要我们自己清理。

例如:

Object o = new Object();
SoftReference<Object> sr = new SoftReference<>(o , queue);

sr为软引用,指向o这个对象,o会在一定时机被 GC 自动清理,但是sr对象本身的清理工作依赖于queue,当sr出现在queue中时,说明其指向的对象已经无效,可以放心清理了。

Reference的四种状态

每一时刻,Reference对象都处于下面四种状态中。这四种状态用Reference的成员变量queuenext(类似于单链表中的next)来标示。

ReferenceQueue<? super T> queue; 
Reference next;

Active:新创建的引用对象都是这个状态,在 GC 检测到引用对象已经到达合适的 reachability 时,GC 会根据引用对象是否在创建时制定ReferenceQueue参数进行状态转移,如果指定了,那么转移到Pending,如果没指定,转移到Inactive。在这个状态中

//如果构造参数中没指定queue,那么queue为ReferenceQueue.NULL,否则为构造参数中传递过来的queue
queue = ReferenceQueue || ReferenceQueue.NULL
next = null

Pending:pending-Reference 列表中的引用都是这个状态,它们等着被内部线程ReferenceHandler处理(会调用ReferenceQueue.enqueue方法)。没有注册的实例不会进入这个状态。在这个状态中

//构造参数参数中传递过来的queue
queue = ReferenceQueue
next = 该queue中的下一个引用,如果是该队列中的最后一个,那么为this

Enqueued:调用ReferenceQueue.enqueued方法后的引用处于这个状态中。没有注册的实例不会进入这个状态。在这个状态中

queue = ReferenceQueue.ENQUEUED;
next = 该queue中的下一个引用,如果是该队列中的最后一个,那么为this;

Inactive:最终状态,处于这个状态的引用对象,状态不会在改变。在这个状态中

queue = ReferenceQueue.NULL;
next = this;

有了这些约束,GC 只需要检测next字段就可以知道是否需要对该引用对象采取特殊处理

  • 如果nextnull,那么说明该引用为Active状态
  • 如果next不为null,那么 GC 应该按其正常逻辑处理该引用。

注意

  1. 如果构造函数中指定了ReferenceQueue,那么事后程序员可以通过该队列清理引用
  2. 如果构造函数中没有指定了ReferenceQueue,那么 GC 会自动清理引用

Reference Subclass

软引用(soft reference)

软引用指向的对象会在程序即将触发OOM时被 GC 清理掉,之后,引用对象会被放到ReferenceQueue中。软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性。

弱引用(weak reference)

弱引用关联的对象可以被 young GC 和 full GC 回收并且 GC 时无论内存是否充足都会回收。被弱引用关联的对象,引用对象会被放到ReferenceQueue中。一般用来实现 canonicalizing mapping,也就是本文要讲的WeakHashMap。使用 Phantom Reference 进行清理动作要比 Object 的 finalize方法更加灵活。

虚引用(phantom reference)

虚引用并不影响对象的生命周期,在任何时候都可能被垃圾回收器回收。调用虚引用的get方法,总会返回null。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

应用场景举例:socket.close() 方法并不能百分之百的保证一定能够成功关闭 socket 资源,我们可以借助 PhantomReference 的特性,在垃圾回收器对 socket 对象进行回收的时候再次尝试一次清理,虽然也不能百分之百地保证资源能够彻底释放,但是这样做能够提高资源释放的概率。

终引用(final reference)

FinalReference 在 JDK 的实现

//类访问权限:package 不能直接对其进行扩展
class FinalReference<T> extends Reference<T> {

    public FinalReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

JDK 对 FinalReference 的扩展

final class Finalizer extends FinalReference { /* Package-private; must be in
                                                  same package as the Reference
                                                  class */

    /* A native method that invokes an arbitrary object's finalize method is
       required since the finalize method is protected
     */
    static native void invokeFinalizeMethod(Object o) throws Throwable;

    private static ReferenceQueue queue = new ReferenceQueue();
    private static Finalizer unfinalized = null;
    private static final Object lock = new Object();

    private Finalizer
        next = null,
        prev = null;

     /* Invoked by VM */
    static void register(Object finalizee) {
        //调用Finalizer以及上层FinalReference构造
        new Finalizer(finalizee);
    }  
    
    private Finalizer(Object finalizee) {
        super(finalizee, queue);
        //调用add方法:将当前对象插入到Finalizer对象链里,链里的对象和Finalizer类静态关联。
        //言外之意是在这个链里的对象都无法被GC掉,除非将这种引用关系剥离
        //(因为`Finalizer`类无法被unload)
        add();
    }

    private void add() {
        synchronized (lock) {
            if (unfinalized != null) {
                this.next = unfinalized;
                unfinalized.prev = this;
            }
            unfinalized = this;
        }
    }
    ...
}    

什么是Finalizer类

判断当前类是否为Finalizer类的标准是覆盖finalize方法,且该方法体必须非空。

//Object
protected void finalize() throws Throwable { }

需要注意的是,JVM 在类加载的时候会遍历当前类的所有方法,包括父类的方法,只要重写非空finalize方法就认为这个类是 Finalizer 类。

如果不是 Finalizer 类的话,该类在被 GC 回收时其实并不会调用它的finalize方法。

Finalizer类何时传到Finalizer.register方法

对象的创建其实是被拆分成多个步骤的,比如A a=new A(2)这样一条语句对应的字节码如下:

0: new           #1                  // class A
3: dup
4: iconst_2
5: invokespecial #11                 // Method "<init>":(I)V

先执行 new 分配好对象空间,然后再执行 invokespecial 调用构造函数,JVM 里其实可以让用户在这两个时机中选择一个,将当前对象传递给Finalizer.register方法来注册到Finalizer对象链里,这个选择取决于是否设置了RegisterFinalizersAtInit这个 vm 参数,默认值为 true,也就是在构造函数返回之前调用Finalizer.register方法,如果通过-XX:-RegisterFinalizersAtInit关闭了该参数,那将在对象空间分配好之后将这个对象注册进去。

另外需要提醒的是,当我们通过 clone 的方式复制一个对象时,如果当前类是一个 Finalizer 类,那么在 clone 完成时将调用Finalizer.register方法进行注册。

hotspot如何实现Finalizer类对象在构造函数执行完毕后调用Finalizer.register

执行一个构造函数时,会去调用父类的构造函数,主要是为了初始化继承自父类的属性,那么任何一个对象的初始化最终都会调用到Object的空构造函数里(任何空的构造函数其实并不空,会含有三条字节码指令,如下代码所示),为了不对所有类的构造函数都埋点调用Finalizer.register方法,hotspot 的实现是,在初始化Object类时将构造函数里的return指令替换为_return_register_finalizer指令,该指令并不是标准的字节码指令,是 hotspot 扩展的指令,这样在处理该指令时调用Finalizer.register方法,以很小的侵入性代价完美地解决了这个问题。

0: aload_0
1: invokespecial #21                 // Method java/lang/Object."<init>":()V
4: return

Finalizer类对象的GC回收

Finalizer类的clinit方法(静态块)里,我们看到它会创建一个FinalizerThread守护线程,这个线程的优先级并不是最高的,意味着在CPU很紧张的情况下其被调度的优先级可能会受到影响

private static class FinalizerThread extends Thread {
    private volatile boolean running;
    FinalizerThread(ThreadGroup g) {
        super(g, "Finalizer");
    }
    public void run() {
        if (running)
            return;
        running = true;
        for (;;) {
            try {
                Finalizer f = (Finalizer)queue.remove();
                f.runFinalizer();
            } catch (InterruptedException x) {
                continue;
            }
        }
    }
}

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();
}

这个线程用来从queue里获取Finalizer对象,然后执行该对象的runFinalizer方法,该方法会将Finalizer对象从Finalizer对象链里剥离出来,这样意味着下次GC发生时就可以将其关联的Finalizer对象回收了,最后将这个Finalizer对象关联的Finalizer对象传给一个 native 方法invokeFinalizeMethod

如果这个对象被判定为有必要执行 finalized 方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在 finalized 方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。

finalized 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize() 中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。

private void runFinalizer() {
    synchronized (this) {
        if (hasBeenFinalized()) return;
        remove();
    }
    try {
        Object finalizee = this.get();
        if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
            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();
}

 static native void invokeFinalizeMethod(Object o) throws Throwable;

其实invokeFinalizeMethod方法就是调了这个f对象的finalize方法.

JNIEXPORT void JNICALL
Java_java_lang_ref_Finalizer_invokeFinalizeMethod(JNIEnv *env, jclass clazz,
                                                  jobject ob)
{
    jclass cls;
    jmethodID mid;

    cls = (*env)->GetObjectClass(env, ob);
    if (cls == NULL) return;
    mid = (*env)->GetMethodID(env, cls, "finalize", "()V");
    if (mid == NULL) return;
    (*env)->CallVoidMethod(env, ob, mid);
}

Finalizer对象的finalize方法会执行多次吗

如果我们在f对象的finalize方法里重新将当前对象赋值,变成可达对象,当这个f对象再次变成不可达时还会执行finalize方法吗?答案是否定的,因为在执行完第一次finalize方法后,这个Finalizer对象已经和之前的Finalizer对象剥离了,也就是下次GC的时候不会再发现Finalizer对象指向该Finalizer对象了,自然也就不会调用这个Finalizer对象的finalize方法了。

Finalizer对象何时被放到ReferenceQueue里

当 GC 发生时,GC 算法会判断Finalizer类对象是不是只被Finalizer类引用(Finalizer类对象被Finalizer对象引用,然后放到Finalizer对象链里),如果这个类仅仅被Finalizer对象引用,说明这个对象在不久的将来会被回收,现在可以执行它的finalize方法了,于是会将这个Finalizer对象放到Finalizer类的ReferenceQueue里,但是这个Finalizer类对象其实并没有被回收,因为Finalizer这个类还对它们保持引用,在GC完成之前,JVM会调用ReferenceQueue中 lock 对象的 notify 方法(当ReferenceQueue为空时,FinalizerThread线程会调用ReferenceQueue的 lock 对象的 wait 方法直到被 JVM 唤醒),此时就会执行上面 FinalizeThread 线程里看到的其他逻辑了。

Finalizer导致的内存泄露

这里举一个简单的例子,我们使用挺广的Socket通信,SocksSocketImpl的父类其实就实现了finalize方法:

/**
 * Cleans up if the user forgets to close it.
 */
protected void finalize() throws IOException {
    close();
}

其实这么做的主要目的是万一用户忘记关闭Socket,那么在这个对象被回收时能主动关闭Socket来释放一些系统资源,但是如果用户真的忘记关闭,那这些socket对象可能因为FinalizeThread迟迟没有执行这些socket对象的finalize方法,而导致内存泄露。

Finalizer的客观评价

Java 里我们看到有构造函数,但是并没有看到析构函数一说,Finalizer其实是实现了析构函数的概念,我们在对象被回收前可以执行一些“收拾性”的逻辑,应该说是一个特殊场景的补充,但是这种概念的实现给Finalizer对象生命周期以及 GC 等带来了一些影响:

  • Finalizer对象因为Finalizer的引用而变成了一个临时的强引用,即使没有其他的强引用,还是无法立即被回收
  • Finalizer对象至少经历两次 GC 才能被回收,因为只有在FinalizerThread执行完了Finalizer对象的finalize方法的情况下才有可能被下次GC回收,而有可能期间已经经历过多次GC了,但是一直还没执行Finalizer对象的finalize方法
  • CPU资源比较稀缺的情况下FinalizerThread线程有可能因为优先级比较低而延迟执行Finalizer对象的finalize方法
  • 因为Finalizer对象的finalize方法迟迟没有执行,有可能会导致大部分Finalizer对象进入到old分代,此时容易引发old分代的GC,甚至Full GC,GC暂停时间明显变长
  • Finalizer对象的finalize方法被调用后,这个对象其实还并没有被回收,虽然可能在不久的将来会被回收

对象可及性的判断

在很多时候,一个对象并不是从根集直接引用的,而是一个对象被其他对象引用,甚至同时被几个对象所引用,从而构成一个以根集为顶的树形结构。下面是可及性判断的规则:

  1. 单条引用路径可及性判断:在这条路径中,最弱的一个引用决定对象的可及性。
  2. 多条引用路径可及性判断:几条路径中,最强的一条的引用决定对象的可及性。

在这个树形的引用链中,箭头的方向代表了引用的方向,所指向的对象是被引用对象。

假设在树形引用链中,「路径1」和 「路径3」为强引用,「路径2」为软引用,「路径4」为弱引用。

对于「对象5」按照这两个判断原则,「路径1-2」取最弱的引用(也就是「路径2」的软引用),因此该路径对「对象5」的引用为软引用。同样,「路径3-4」为弱引用。在这两条路径之间取最强的引用,于是「对象5」是一个软可及对象。

ReferenceQueue

引用队列,在检测到适当的可到达性更改后,垃圾回收器将已注册的引用对象添加到该队列中。

ReferenceQueue 名义上是一个队列,但实际内部并非有实际的存储结构,它的存储是依赖于内部节点之间的关系来表达。可以理解为 queue 是一个类似于链表的结构,这里的节点其实就是 reference 本身。

pending和discovered

//要处理的对象链表,通过discovered排队
transient private Reference<T> discovered;  /* used by VM */
//JVM 在 GC 时通过discovered不断地拿到下一个对象赋值给pending(因有两个线程访问,需加锁)
private static Reference<Object> pending = null;
if (pending != null) {
     r = pending;
    // 'instanceof' might throw OutOfMemoryError sometimes
    // so do this before un-linking 'r' from the 'pending' chain...
    c = r instanceof Cleaner ? (Cleaner) r : null;
    // unlink 'r' from 'pending' chain
    pending = r.discovered;
    r.discovered = null;

重要方法

Clear方法

/**
 * Clears this reference object.  Invoking this method will not cause this
 * object to be enqueued.
 *
 * <p> This method is invoked only by Java code; when the garbage collector
 * clears references it does so directly, without invoking this method.
 */
public void clear() {
    this.referent = null;
}

ReferenceHandler线程

这个线程在 Reference 类的 static 构造块中启动,并且被设置为高优先级和 daemon 状态。此线程要做的事情,是不断的检查 pending 是否为 null,如果 pending不为 null,则将 pending 进行 enqueue,否则线程进入 wait 状态。

由此可见,pending 是由 JVM 来赋值的,当 Reference 内部的 referent 对象的可达状态改变时,JVM 会将 Reference 对象放入 pending 链表。并且这里 enqueue 的队列是我们在初始化(构造函数)Reference 对象时传进来的 queue,如果传入了 null(实际使用的是ReferenceQueue.NULL),则 ReferenceHandler 则不进行 enqueue 操作,所以只有非 RefernceQueue.NULL 的 queue 才会将 Reference 进行 enqueue。

ReferenceQueue 是作为 JVM GC 与上层 Reference 对象管理之间的一个消息传递方式,它使得我们可以对所监听的对象引用可达发生变化时做一些处理

static {
    ThreadGroup tg = Thread.currentThread().getThreadGroup();
    for (ThreadGroup tgn = tg;
         tgn != null;
         tg = tgn, tgn = tg.getParent());
    Thread handler = new ReferenceHandler(tg, "Reference Handler");
    /* If there were a special system-only priority greater than
         * MAX_PRIORITY, it would be used here
         */
    handler.setPriority(Thread.MAX_PRIORITY);
    handler.setDaemon(true);
    handler.start();

    // provide access in SharedSecrets
    SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
        @Override
        public boolean tryHandlePendingReference() {
            return tryHandlePending(false);
        }
    });
}

其优先级最高,可以理解为需要不断地处理引用对象。

private static class ReferenceHandler extends Thread {

    private static void ensureClassInitialized(Class<?> clazz) {
        try {
            Class.forName(clazz.getName(), true, clazz.getClassLoader());
        } catch (ClassNotFoundException e) {
            throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
        }
    }

    static {
        // pre-load and initialize InterruptedException and Cleaner classes
        // so that we don't get into trouble later in the run loop if there's
        // memory shortage while loading/initializing them lazily.
        ensureClassInitialized(InterruptedException.class);
        ensureClassInitialized(Cleaner.class);
    }

    ReferenceHandler(ThreadGroup g, String name) {
        super(g, name);
    }

    public void run() {
        while (true) {
            tryHandlePending(true);
        }
    }
}
/**
     * Try handle pending {@link Reference} if there is one.<p>
     * Return {@code true} as a hint that there might be another
     * {@link Reference} pending or {@code false} when there are no more pending
     * {@link Reference}s at the moment and the program can do some other
     * useful work instead of looping.
     *
     * @param waitForNotify if {@code true} and there was no pending
     *                      {@link Reference}, wait until notified from VM
     *                      or interrupted; if {@code false}, return immediately
     *                      when there is no pending {@link Reference}.
     * @return {@code true} if there was a {@link Reference} pending and it
     *         was processed, or we waited for notification and either got it
     *         or thread was interrupted before being notified;
     *         {@code false} otherwise.
     */
static boolean tryHandlePending(boolean waitForNotify) {
    Reference<Object> r;
    Cleaner c;
    try {
        synchronized (lock) {
            if (pending != null) {
                r = pending;
                // 'instanceof' might throw OutOfMemoryError sometimes
                // so do this before un-linking 'r' from the 'pending' chain...
                c = r instanceof Cleaner ? (Cleaner) r : null;
                // unlink 'r' from 'pending' chain
                pending = r.discovered;
                r.discovered = null;
            } else {
                // The waiting on the lock may cause an OutOfMemoryError
                // because it may try to allocate exception objects.
                if (waitForNotify) {
                    lock.wait();
                }
                // retry if waited
                return waitForNotify;
            }
        }
    } catch (OutOfMemoryError x) {
        // Give other threads CPU time so they hopefully drop some live references
        // and GC reclaims some space.
        // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
        // persistently throws OOME for some time...
        Thread.yield();
        // retry
        return true;
    } catch (InterruptedException x) {
        // retry
        return true;
    }

    // Fast path for cleaners
    if (c != null) {
        c.clean();
        return true;
    }

    ReferenceQueue<? super Object> q = r.queue;
    if (q != ReferenceQueue.NULL) q.enqueue(r);
    return true;
}