软引用和弱引用的实现

2016年01月27日

  --- layout: post title: Java 引用类型 category: 技术 tags: 源码 keywords: description: ---

引用类型一共分为四种,分别对应四种不同的垃圾回收策略:

  • 幽灵引用/虚引用, 和没有引用对象几乎一样, 即通过幽灵引用是无法访问对象的.垃圾回收不考虑幽灵引用, 如果一个对象只被幽灵引用指向,则在垃圾回收时,会直接回收.(提供了一种比finalization机制更加灵活的资源清理机制,文章后面会提到finalizetion机制存在的问题)
  • 弱引用, 如果一个对象只被弱引用关联时, 则它也只能够存活到下一次垃圾回收, 在发生垃圾回收之前,是可通过弱引用访问对象的,发生垃圾回收的时候,会将它回收掉.(在文档中提到这中引用可用来实现 canonicalizing mappings, 关于这个映射,请看这个文章:传送门, ThradLocalMap的key即使用的这种引用类型 )
  • 软引用, 如果一个对象只被软引用关联, 则只有在所有不可达对象和虚引用指向对象,以及弱引用指向对象都被回收, 内存仍然不足的情况才会被回收掉(常用做内存敏感的缓存).
  • 强引用, 任何情况下,只要有强引用存在,就不会回收指向的对象, 如果内存不足,则发生OOM.

Java 对不同引用类型的支持

下面是java.lang.ref包下,引用支持的相关类

SoftReference, WeakReference, PhanfomReference分别对应上面提到的,软引用,弱引用,幽灵引用. 而 对强引用来说,任何正常赋值的引用都是强引用(例如 String strong = new String(""), strong 就是一个强引用). 所以无需一个新的Reference子类来表示它.

FinalReference这个类,应该不是上面提到的强引用对应的类,这个类感觉就只是用来实现对对象的finalize()方法调用的.

Final references, used to implement finalization

java的Reference提供一种对象回收提示机制.Reference提供两种构造方式:

  // 普通构造, 直接接收一个对象,包装成某一种引用类型
  Reference(T referent) {this(referent, null);}

  // 对象回收构造, 接收待包装对象,以及一个ReferenceQueue队列
 Reference(T referent, ReferenceQueue<? super T> queue) {
     this.referent = referent;
     this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
 }

其中上面的第二种构造方式,会需要传入一个ReferenceQueue对象, 这个队列对象的作用是,如果本Reference实例包装的对象被回收掉了,则JVM会将对象引用放入这个队列中,从而Reference客户端代码,就可以感知到对象被回收事件,从而可做出对应处理.

对象被回收,对应Reference进入ReferenceQueue

Reference包装对象被回收时,对应的Reference对象会被加入ReferenceQeueue队列中.以下为Reference类中,被回收对象对应的Reference入队列操作.ReferenceHandlerReference的内部类,用来处理入列操作.

    // 引用处理线程和垃圾回收器共用锁对象.每次垃圾回收开始的时候
    // 都会去获取这个锁,所以任何获取这个锁的线程,都要尽快完成并释放这个锁.
     static private class Lock { };
     private static Lock lock = new Lock();

    // 这是一个已被回收对象的链表,里面都是等待被`ReferenceHandler`线程处理的`Reference`对象
    // 由垃圾回收线程,将回收对象加入这个链表中.
     private static Reference pending = null;


    // 引用处理线程,用来将所有已回收对象加入对应的`ReferenceQueue`对象中去.
     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 {
                          // 如果没有已回收对象需要添加入`ReferenceQueue`中,则将线程挂起,等待垃圾回收线程唤醒
                            try {
                                lock.wait();
                            } catch (InterruptedException x) { }
                            continue;
                        }
                    }

                    // Fast path for cleaners
                    if (r instanceof Cleaner) {
                        ((Cleaner)r).clean();
                        continue;
                    }

                    // 将被回收对象对应的`Reference`对象加入 对应的`ReferenceQueue`中,
                    // 从而客户端代码,可以感知到,对象被回收
                    ReferenceQueue q = r.queue;
                    if (q != ReferenceQueue.NULL) q.enqueue(r);
                }
            }
        }

        // 创建引用处理线程,将优先级设置为最高,并且设置为守护线程
        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();
        }

Finalizer 执行覆盖了Object.finalize()方法的待回收对象的finalize()方法

JVM会将所有覆盖了Object.finalize()方法的对象,包装为一个Finalizer并放入一个需执行finalize()方法的队列中,如果对象需要被回收,则会将它放入另一个ReferenceQueue队列中,然后,有一个守护线程,在不断的从ReferenceQueue队列中获取Finalizer对象,并执行它的finalize()方法.

至于JVM如何保证一个对象的finalize()方法,只会被执行一次,而不会被执行多次呢?

前面说到每一个需要执行finalize()方法的对象都会被包装成一个Finalizer对象,这个Finalizer对象会被放入一个双向链表中.如果一个Finalizer对象的已经执行过finalize()方法了,则会将它从双向链表中删除.从而JVM在发现一个需执行finalize()方法的对象已经不在链表中了,就可以判定为该对象的finalize()已经执行过了,无需再次执行,直接回收.

在下面的remove()方法中,会将Finalizer对象的next和pre都指向自己,从而表明它已经执行过finalize()

    private void remove() {
            synchronized (lock) {
                if (unfinalized == this) {
                    if (this.next != null) {
                        unfinalized = this.next;
                    } else {
                        unfinalized = this.prev;
                    }
                }
                if (this.next != null) {
                    this.next.prev = this.prev;
                }
                if (this.prev != null) {
                    this.prev.next = this.next;
                }
                this.next = this;   /* Indicates that this has been finalized */
                this.prev = this;
            }
        }

下面是执行所有对象finalize()方法的线程实现, 就是不断的从等待被回收,但需执行finalize()方法的队列中获取对象.然后执行其finalize()方法.(调用queue.remove(),若队列无元素,则会阻塞).

    private static class FinalizerThread extends Thread {
            private volatile boolean running;
            FinalizerThread(ThreadGroup g) {
                super(g, "Finalizer");
            }
            public void run() {
                if (running)
                    return;

                // Finalizer thread starts before System.initializeSystemClass
                // is called.  Wait until JavaLangAccess is available
                while (!VM.isBooted()) {
                    // delay until VM completes initialization
                    try {
                        VM.awaitBooted();
                    } catch (InterruptedException x) {
                        // ignore and continue
                    }
                }
                final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
                running = true;
                for (;;) {
                    try {
                        Finalizer f = (Finalizer)queue.remove();
                        f.runFinalizer(jla);
                    } catch (InterruptedException x) {
                        // ignore and continue
                    }
                }
            }
        }

下面是创建上面说的线程的代码, 可以看见线程的优先级并不是最高的.所以有可能对象已经待回收了,但是一直无法执行该线程执行finalize(), 从而对象无法被回收,最终导致内存泄漏.其所持有的资源也无法释放

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

这篇讲finalizer的博客 (传送门) 也讲的蛮详细的 .