SoftReference,WeakReference,Phan

2020-06-20  本文已影响0人  whateverblake

背景

在java中引用分为四种,强引用,软引用,弱引用,虚引用。强引用是平时用到最多的,最常见的就是我们使用new去创建的对象然后赋值给到的引用就是强引用,例如Object obj = new Object(), obj就是一个强引用。对于SoftReference,WeakReference,PhantomReference 这三种类型的引用类型他们都继承自Reference这个抽象类。

引用方式

ReferenceQueue referenceQueue = new ReferenceQueue();
Object obj = new Object();
SoftReference sr = new SoftReference(obj,referenceQueue);
WeakReference wr = new WeakReference(obj,referenceQueue);
PhantomReference pr = new PhantomReference(obj,referenceQueue)
从上面的代码我们可以看出,这三种引用提供了基本相同的使用方式,当然,对于SoftReference,WeakReference来说ReferenceQueue在初始化引用的时候不是必须的,因为如果你不提供的话,SoftReference,WeakReference的父类Reference会提供默认的实现: image.png
image.png

作用

这三种引用类型引用的对象如果处于gc root链不可达,那么这些对象就有可能被随时被回收掉。对于SoftReference引用的对象来说,如果gc root链不可达,那么垃圾收集器会在内存不足的时候把它引用的对象回收掉。对于WeakReference和PhantomReference来说他们引用的对象,如果gc root链不可达那么在gc的时候会被立即回收。SoftReference和WeakReference引用的对象可以通过引用的get方法获取到相应的对象,但是对于PhantomReference来说没法通过这类引用去获取被引用的对象。当这三类引用引用的对象被回收的时候,那么这三类引用对象自身会被放在ReferenceQueue中,然后我们可以从ReferenceQueue获取相应的引用对象信息,基于这写信息我们可以做一些自己的逻辑。

回收过程

这个类是在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);
           }
       });
   }

它的run方法如下

 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;
                    // '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;
    }

这个是一个核心方法,如果明白这里面发生了什么,那么就能搞明白这三类引用类型引用的对象被回收后发生了什么。
我们看到同步代码块中上来就是pending是否为空的判断,如果为空那么这个线程就去wait了,我们看下pending在Reference的定义

 private static Reference<Object> pending = null;

可以看出初始化是null的,那么什么时候这个pending会被谁在什么时候赋值。
pending会被JVM直接赋值:当着三类引用类型引用的对象被回收后那么相应的引用会被加入到pending中,pending每次只会被赋值一个引用对象,那么剩下别的引用对象会被加入到pending的discover中形成一个链表。我们可以看到在tryHandlePending每次都是处理链表上的一个reference


image.png

那么对每一个reference是如何处理的呢,对于reference如果是一个Cleaner那么会执行它的clean方法


image.png
Cleaner是PhantomReference的子类,在DirectByteBuffer中就用到了它来做堆外内存的回收,在这里就不详细介绍了,如果不是Cleaner那么就是把这个reference加入到ReferenceQueue中
image.png
然后我们就可以从ReferenceQueue中获取相应的reference了

到此已经全部讲完了整个过程下面是我的实验代码

实验

        Object obj = new Object();
        ReferenceQueue referenceQueue = new ReferenceQueue();
        WeakReference weakReference = new WeakReference(obj,referenceQueue);
        WeakReference weakReference2 = new WeakReference(obj,referenceQueue);
        WeakReference weakReference3 = new WeakReference(obj,referenceQueue);

        System.out.println(weakReference.get());
        System.out.println(referenceQueue.poll());
        obj = null ;
        System.gc();
        System.out.println("before isEnqueue----"+weakReference.isEnqueued());

        Thread.sleep(1000);
        System.out.println("after isEnqueue----"+weakReference.isEnqueued());

        Reference t = referenceQueue.poll() ;
        System.out.println(t);

        System.out.println(weakReference.get());

代码比较简单,使用WeakReference做示例,new一个对象然后使用强引用obj引用它,接下来建立三个弱引用引用这个对象,这个时候我们通过weakReference.get()是可以获得这个对象的,同时通过referenceQueue.poll()是获取不到任何数据的,这个时候我们把强引用obj = null,然后通过System.gc()触发full gc,这个时候理论上新建的new Object()对象会被回收掉,然后在referenceQueue会得到执行这个对象的三个弱引用,但是因为ReferenceHandler是一个后台线程在运行,所以我们在before isEnqueue打印出的是false(reference.isEnqueue可以判断这个reference是不是被加入了ReferenceQueue中),所以我们sleep 1s等一下ReferenceQueue线程去执行,在after isEnqueue 打印出的就是true,然后我们可以通过referenceQueue.poll()获得一个weakReference,如果你poll三次那么会得到上面定义的三个weakReference。

Tips:如果想debug tryHandlePending方法看到上面我们定义的weakReference是如何加入referenceQueue的,那么最好是把debug设置condition


image.png

上图是我们在idea 中设置的condition(pending instanceof WeakReference && !(pending instanceof WeakHashMap.Entry))为什么这么设置呢,因为jvm内部也有很多这种类型的引用会被回收,如果不设置就需要执行这个方法很多遍才能看到自己定义的reference

上一篇下一篇

猜你喜欢

热点阅读