聊聊Java中引用类型

Java中引用类型,可能有一定开发经验人,才会了解到这一步,本文将从基础开始,随后结合jdk及框架中对引用使用、OOM分析 深入探讨Java中引用类型。

介绍

背过面试题的都知道,Java中有四种引用类型:

  • 强引用:就算oom,也不回收
  • 软引用SoftReference:要oom时候,才回收
  • 弱引用WeakReference:gc就回收
  • 虚引用``:每次get方法,都是null,用途是作为gc时候的通知。

Reference 继承结构

几种引用类型,都是继承 Reference
聊聊Java中引用类型
其中,软引用构造时,无法传递ReferenceQueue队列:

    public SoftReference(T referent) {
        super(referent);
        this.timestamp = clock;
    }

弱引用和虚引用,在构造时,需要传递 ReferenceQueue 供监听回收通知:

弱引用:

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

对于弱引用,get方法会直接获取reference值并返回。

虚引用:

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

虚引用的get方法,则永远返回null,即永远不可达

    public T get() {
        return null;
    }

Reference

看看里面变量:

public abstract class Reference<T> {
    private T referent;         /* Treated specially by GC */   // 实际的引用
    volatile ReferenceQueue<? super T> queue;  // 缓存queue对象
    volatile Reference next;   // 下一个节点
    transient private Reference<T> discovered;  /* used by VM */
    static private class Lock { }
    private static Lock lock = new Lock();
    private static Reference<Object> pending = null;
}

next :这个字段在不同情况下,指向对象不同,按照java doc描述如下:

/* When active: NULL
* pending: this
* Enqueued: next reference in queue (or this if last)
* Inactive: this
*/

discovered: 由vm维护,在active和pending状态时,维护不同队列,分以下几种情况:

/* When active: next element in a discovered reference list maintained by GC (or this if last)
* pending: next element in the pending list (or null if last)
* otherwise: NULL
*/

pending:pending作为静态变量,应用唯一,可以理解为头结点,由vm将reference放入pending队列中,由discovered连接,而最终由Reference-handler 线程将对应待丢弃对象放入对应的ReferenceQueue当中。

Java中Reference引用整体扭转流程如下图所示:
聊聊Java中引用类型

  1. 从Active到Pending状态,是有vm经过可达性分析之后,丢到Pending队列中,使用discover连接。
  2. 从Pending到Enqueued 状态,则是由 ReferenceHandler 线程进行调度,ReferenceHandler 具有最高优先级。
  3. 当对象从queue中删除时,变为Inactive状态,才会真正被垃圾回收掉,如果是使用 SoftReference ,当可达性分析不存在时,则直接进入Inactive状态。

ReferenceQueue

在Java引用中 ,ReferenceQueue 角色其实是维护者一个head 头结点的角色,里面的enqueue方法是采用头插法构造队列的。

ReferenceHandler

看看从Pending到Enqueued 状态,java程序是如何处理的:
在Reference静态代码块中,生成并启动ReferenceHandler:

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

循环尝试处理:

        public void run() {
            while (true) {
                tryHandlePending(true);
            }
        }

tryHandlePending方法:

    static boolean tryHandlePending(boolean waitForNotify) {
        Reference<Object> r;
        Cleaner c;
        try {
            synchronized (lock) {
                if (pending != null) {
                    r = pending;
                    // 判断该reference是否为Cleaner
                    c = r instanceof Cleaner ? (Cleaner) r : null;
                   	// pending指向r的下一个待pending的节点
                    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) {
            // 让出cpu,相信gc会成功挺过这次oom?
            Thread.yield();
            // retry
            return true;
        } catch (InterruptedException x) {
            // retry
            return true;
        }

        // 优先clean
        if (c != null) {
            c.clean();
            return true;
        }
		// 把r放入到r对应的ReferenceQueue中
        ReferenceQueue<? super Object> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
    }

Cleaner

Cleaner 实际上是一个虚引用PhantomReference,Cleaner可以理解为JDK的亲儿子,ReferenceHandler 中对Cleaner的操作明显有点护犊子,在JDK代码中,主要是nio直接内存的使用有维护:
聊聊Java中引用类型
Cleaner有以下特性:

  1. 自己维护这一个双向链表,有一个静态变量first指代头节点,next和prev分别指向前驱和后继节点。
  2. 同样是采用头插法进行维护
  3. 构造方法属性为private,且接受一个Runnable类型变量,在最终执行clean方法时,直接执行run方法进行clean。这样虽然可以由开发者自定义实现清除函数,但是也带来了不确定性:当一个clean线程执行时间过长,会导致整个ReferenceHandler 后续等待节点饥饿。

所以大多数情况下,程序员可以自行 通过 继承 PhantomReference 实现这样的功能。

FinalReference

Reference 还有一个子类 FinalReference,由于该引用是包可见性,所以 Java中对他的使用,也仅仅局限于 Finalizer 类,这个类和面试中一直在背过的finalize 就有关了。
背过的面试题:

  1. finalize 方法,功能类似于c++中析构函数,但又不是析构函数。
  2. finalize 执行线程优先级低,可能被执行,也可能不被执行,并且一个对象只会有一次执行的机会,即复活一次?

大多数特性都可以从 Finalizer 中代码体现出来。

如果某一个类有重写finalize方法时,当jvm认为该对象没有没有其他引用时,就会调用finalize方法,可以给该类再次赋予引用,避免被gc,但是一个对象只有一次执行finalize机会。

Finalizer

final class Finalizer extends FinalReference<Object> {
}

包可见,且为final 类型,无法被子类重写。
Finalizer 构造方法为私有,且只能有vm调用:

    /* Invoked by VM */
    static void register(Object finalizee) {
        new Finalizer(finalizee);
    }

当jvm初始化实例时,发现类重写了finaize方法,则会调用register,将该对象通过尾插法插入到Finalizer队列中:

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

Finalizer有以下字段属性:

    private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
    private static Finalizer unfinalized = null;    // 指向最后一个节点,
    private static final Object lock = new Object();   // 自定义锁
    private Finalizer
        next = null,
        prev = null;

Finalizer内部维护和一条双向链表,且unfinalized指向最后一个节点,节点之间通过next和prev连接。

FinalizerThread

类似于引用将pending状态节点入队,Finalizer也有一个线程 FinalizerThread ,他负责将所有没有引用的对象出队并执行finalize 方法,但是该线程优先级要比ReferenceHandler 低。

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

FinalizerThreadrun 方法(截取核心):

            for (;;) {
                try {
                    Finalizer f = (Finalizer)queue.remove();   // 从引用队列中获取对象
                    f.runFinalizer(jla);
                } catch (InterruptedException x) {   // 忽略interrupt异常
                    // ignore and continue
                }
            }

runFinalizer方法:

    private void runFinalizer(JavaLangAccess jla) {
        synchronized (this) {  
            if (hasBeenFinalized()) return;
            remove();
        }
        try {
            Object finalizee = this.get();
            if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
                jla.invokeFinalize(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方法有如下特性:

  1. 锁住当前Reference对象,如果next=null,即证明已经不在finalizer队列中,则不在执行,因为入队入口是唯一的,所以当一个对象执行过finalize方法后,就出队了,所以当又要被gc时,不会再执行finalize方法了。
  2. 获取Reference对象,执行finalize方法。
  3. catch 住 Throwable 异常,即忽略所有异常。最后调用父类clear方法,将reference置为null。
  4. 优先级低,可能不会被执行

总结

  1. Java虽然有四种引用类型,但是使用不当将会引发内存泄漏,因为到ReferenceQueue时,会临时变为强引用类型。
  2. 对于这些引用,都要经过至少2次GC,才能被回收。

觉得对你有帮助?不如关注博主公众号: 六点A君
聊聊Java中引用类型

上一篇:mysql 的 3306、33060 端口区别


下一篇:IfcPile