美文网首页java进阶干货
Java PhantomReference详解

Java PhantomReference详解

作者: searchworld | 来源:发表于2017-12-11 22:55 被阅读2037次

简单解释

jvm有四种引用: strong soft weak phantom(其实还有一种FinalReference,这个由jvm自己使用,外部无法调用到),主要的区别体现在gc上的处理,如下:

  1. Strong类型,也就是正常使用的类型,不需要显示定义,只要没有任何引用就可以回收
  2. SoftReference类型,如果一个对象只剩下一个soft引用,在jvm内存不足的时候会将这个对象进行回收
  3. WeakReference类型,如果对象只剩下一个weak引用,那gc的时候就会回收。和SoftReference都可以用来实现cache
  4. PhantomReference类型,这个比较特殊,可以用来实现类似Object.finalize功能,下面单独解释。

示例

SoftReference<Object> soft = new SoftReference<>(new Object());
WeakReference<Object> weak = new WeakReference<>(new Object());
WeakReference<String> weakString = new WeakReference<>("abc");
PhantomReference<Object> phantom = new PhantomReference<>(new Object(), new ReferenceQueue<>());
System.gc();
System.out.println(soft.get());
System.out.println(weak.get());
System.out.println(weakString.get());
System.out.println(phantom.get());

// 
java.lang.Object@73ae9565
null
abc
null

soft类型由于内存还充足,不会被回收;weak类型在gc的时候就回收;phantom总是返回null。weakString没被回收是引用常量池持有对"abc"的引用。

finalize方法

这里实际使用了FinalReference,真正的实现是Finalizer。大体的过程是jvm将实现了Object.finalize方法的实例标识为finalizer类型,注册为一个Finalizer对象,并加到到对象链中,有一个FinalizerThread线程负责从对象链获取Finalizer对象,执行runFinalizer函数,最终调用Object.finalize。详细过程可以参考JVM源码分析之FinalReference完全解读

但是finalization方式存在一定的缺陷,比如无法保证一定执行,gc压力比较大等。

PhantomReference

PhantomReference的get方法总是返回null,因此无法访问对应的引用对象;其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作。

PhantomReference的应用并不多,其中一个应用是sun.misc.Cleaner类,这个应用可以参考JVM源码分析之堆外内存完全解读

为什么要主动调用System.gc
既然要调用System.gc,那肯定是想通过触发一次gc操作来回收堆外内存,不过我想先说的是堆外内存不会对gc造成什么影响(这里的System.gc除外),但是堆外内存的回收其实依赖于我们的gc机制,首先我们要知道在java层面和我们在堆外分配的这块内存关联的只有与之关联的DirectByteBuffer对象了,它记录了这块内存的基地址以及大小,那么既然和gc也有关,那就是gc能通过操作DirectByteBuffer对象来间接操作对应的堆外内存了。DirectByteBuffer对象在创建的时候关联了一个PhantomReference,说到PhantomReference它其实主要是用来跟踪对象何时被回收的,它不能影响gc决策,但是gc过程中如果发现某个对象除了只有PhantomReference引用它之外,并没有其他的地方引用它了,那将会把这个引用放到java.lang.ref.Reference.pending队列里,在gc完毕的时候通知ReferenceHandler这个守护线程去执行一些后置处理,而DirectByteBuffer关联的PhantomReference是PhantomReference的一个子类,在最终的处理里会通过Unsafe的free接口来释放DirectByteBuffer对应的堆外内存块

另一个例子在 Finalizers and References in Java,貌似没翻墙无法访问,可以直接看代码https://github.com/aragozin/example-finalization。这里使用PhantomReference来实现半自动垃圾回收。主要的区别在ReferenceUsageCheck这个测试类。leaking_references_finalizerleaking_references_phantom两个方法依赖jvm的gc机制;low_leaking_references_finalizerlow_leaking_references_phantom两个方法有99%的对象显式回收,只有1%的对象依赖gc。对比可以看出使用finalizer的没有变化,但是使用PhantomReference的前后有一个数量级的下降。使用PhantomReference的时候一般是配合一个ReferenceQueue,在PhantomReference引用的对象要释放的时候加入到这个队列中,如果要回收需要调用clear方法,一些housekeeping工作也是在这个时候做,这个逻辑在ResourceDisposalThread实现:每次从队列中拿一个phantom对象,做一些记录工作,然后clear释放。

        public void run() {
            while(true) {
                try {
                    PhantomResourceRef ref = (PhantomResourceRef) queue.remove();
                    ref.dispose();
                    ref.clear();
                } catch (InterruptedException e) {
                    // ignore
                }
            }
        }

上面这个是依赖jvm的gc机制,如果对象比较多,gc压力会比较大,这个时候可以使用显示释放,这里通过调用PhantomResourceRef.dispose->PhantomHandle.dispose->ReferedResource.dispose,调用PhantomReference的clear方法,然后将其置为空:

        // 这里的handle是PhantomResourceRef,即一个PhantomReference
        public synchronized void dispose() {
            handle.clear();
            handle = null;
            super.dispose();
            GLOBAL_RESOURCES.remove(this);
        }

这样对jvm来说就不需要对这个PhantomReference对象做特别处理了(不会进入ReferenceQueue),性能上可以提升不少。

从第二个例子可以看到,PhantomReference的进入队列之后jvm不会自动回收,需要人为调用clear方法后才最终释放,这是使用PhantomReference的关键。

Reference类

Reference类是上面除了Strong类型引用外的其他引用的父类。存在四种不同的状态:

  • Active,这是Reference对象初创的时候的状态
  • Pending,对象在pending列表里,等待进入队列。没有注册队列的引用对象不存在这个状态
  • Enqueued,引用对象进入队列,没有注册队列的引用对象不存在这个状态
  • Inactive,没有注册队列的引用对象或者从队列中被移除之后所处的状态,这个状态下的对象不再变化,等待gc回收。

上面所说的pending列表是由一个静态字段和一个next对象组成,由gc负责维护:

Reference next;
private static Reference pending = null;

最关键的是Reference中有一个ReferenceHandler线程对引用对象进行处理:

        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 {
                            lock.wait();
                        } catch (InterruptedException x) { }
                        continue;
                    }
                }

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

                ReferenceQueue q = r.queue;
                if (q != ReferenceQueue.NULL) q.enqueue(r);
            }
        }

主要逻辑是从pending列表中取出一个引用对象(最后这个r.next=r不是很理解),如果是Cleaner对象(上面介绍PhantomReference的第一个例子里用到的)则调用其clean方法;否则加入到对应的队列中,这个用法在PhantomReference的第二个例子中用到。上面ReferedResource.dispose有个小细节handle = null;,即把一个PhantomReference对象置为null,这时候gc就不会将其加到pending列表交给ReferenceHandler处理,而是直接回收(跟普通对象是一样的),这里是性能提升的关键

相关文章

网友评论

    本文标题:Java PhantomReference详解

    本文链接:https://www.haomeiwen.com/subject/rffwixtx.html