zoukankan      html  css  js  c++  java
  • JDK源码分析(8)之 Reference 完全解读

    在阅读本文之前最好对 Reference 框架有一个整体的把握,可以参考我上一篇博客 Reference 框架概览 ;本文主要讲了 Reference 的子类实现和应用(SoftReference,WeakReference,PhantomReference);

    Java 引用的强弱关系StrongReference > SoftReference > WeakReference > PhantomReference

    一、StrongReference

    强引用:我们通常使用的引用,形如Object o = new Object();

    reference

    此时从 stack 中的 o,到 heap 中的 Object 就是强引用;其他引用强弱的判定规则,可以查看我上一篇博客 Reference 框架概览

    二、SoftReference

    软引用:可以用来表示一些有用但非必须的对象;JVM 会根据使用率和剩余堆空间大小来公共决定什么时候回收 SoftReference;JVM 保证在抛出 OOM 之前会再次扫描回收这些软引用,如果回收后内存仍不足才会抛出 OOM;所以在源码的注释中也写了 SoftReference 适合实现内存敏感的缓存;

    public class SoftReference<T> extends Reference<T> {
      /**
       * Timestamp clock, updated by the garbage collector
       */
      static private long clock;
    
      /**
       * Timestamp updated by each invocation of the get method.  The VM may use
       * this field when selecting soft references to be cleared, but it is not
       * required to do so.
       */
      private long timestamp;
    
      public SoftReference(T referent) {
        super(referent);
        this.timestamp = clock;
      }
    
      public SoftReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
        this.timestamp = clock;
      }
    
      public T get() {
        T o = super.get();
        if (o != null && this.timestamp != clock)
          this.timestamp = clock;
        return o;
      }
    
    }
    
    

    看上面的代码,SoftReference 与 Reference 相比多了两个时间戳 clock,timestamp,并且会在每次 get的时候更新时间戳;

    • clock:这个时间戳是static修饰的,是所有 SoftReference 共有,由 JVM 维护;
    • timestamp:主要用于记录当前对象的存活时间;

    回收策略

    上面提到 SoftReference 的回收是由使用率和剩余堆空间大小来公共决定的,那么它是怎么实现的呢?

    openjdk/hotspot/src/share/vm/memory/referencePolicy.cpp

    // Capture state (of-the-VM) information needed to evaluate the policy
    void LRUCurrentHeapPolicy::setup() {
      _max_interval = (Universe::get_heap_free_at_last_gc() / M) * SoftRefLRUPolicyMSPerMB;
      assert(_max_interval >= 0,"Sanity check");
    }
    
    // The oop passed in is the SoftReference object, and not
    // the object the SoftReference points to.
    bool LRUCurrentHeapPolicy::should_clear_reference(oop p, jlong timestamp_clock) {
      jlong interval = timestamp_clock - java_lang_ref_SoftReference::timestamp(p);
      assert(interval >= 0, "Sanity check");
    
      // The interval will be zero if the ref was accessed since the last scavenge/gc.
      if(interval <= _max_interval) {
        return false;
      }
    
      return true;
    }
    

    根据上面的代码可以大致知道:

    1. 首先计算出了最大堆内存和上次 GC 时剩余的内存;
    2. 再用(剩余内存 / 最大内存 )* SoftRefLRUPolicyMSPerMB 得出到下次 GC 期间软引用的最大 idle 时间;
    3. 最后用 clock 和 timestamp 两个时间戳差值得到 SoftReference 的 idle 时间(每次 get 的时候 this.timestamp = clock;,所以get 之后 idle 时间归零),如果大于最大 idle 时间则清除;

    我们可以简单测试一下,启动参数:-XX:SoftRefLRUPolicyMSPerMB=2 -Xmx10M -XX:+PrintCommandLineFlags -verbose:gc

    • -XX:SoftRefLRUPolicyMSPerMB=2:可以参照上面的计算过程调节 SoftReference 的回收频率;
    • -Xmx10M:为最大堆内存,同样可以自行调节,-verbose:gc:打开 GC 日志,-XX:+PrintCommandLineFlags:打印 JVM 启动参数;
    private static void test03() throws InterruptedException {
      ReferenceQueue queue = new ReferenceQueue();
      Object o = new Object() {
        @Override
        public String toString() {
          return "zhangsan";
        }
      };
      
      Reference softRef = new SoftReference(o, queue);
      new Monitor(queue).start();
      
      o = null;
      System.gc();
      log.info("o=null, referent:{}", softRef.get());
      
      byte[] bytes = new byte[3 * 1024 * 1024];
      System.gc();
      log.info("After GC, referent:{}", softRef.get());
      Thread.sleep(2000);
      System.gc();
      log.info("After GC, referent:{}", softRef.get());
    }
    
    private static class Monitor extends Thread {
      ReferenceQueue queue;
    
      public Monitor(ReferenceQueue queue) {
        this.queue = queue;
      }
      
      @Override
      public void run() {
        while (true) {
          try {
            log.info("remove reference:{}", queue.remove().toString());
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      }
    }
    

    // 打印:

    [main] o=null, referent:zhangsan
    [main] After GC, referent:zhangsan
    [main] After GC, referent:null
    [Thread-0] remove reference:java.lang.ref.SoftReference@bcffe9a
    

    根据不同的参数设置会出现不同的情况,大家可以自行调节参数,验证上面的计算规则;另外如果-XX:SoftRefLRUPolicyMSPerMB=0,那么 SoftReference 就应该和 WeakReference 差不多了,至于是否完全一致,就留到以后查看 JVM 的时候再确定了;

    三、WeakReference

    弱引用:被弱引用关联的对象只能生存到下一次 GC,当 GC 的时候无论内存是否足够,使用是否频繁都会被清除;同样源码注释里面也写了 WeakReference 适合实现 canonicalizing mappings,比如 WeakHashMap;

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

    简单测试,启动参数:-Xmx300M -XX:+PrintCommandLineFlags -verbose:gc

    private static void test04() {
      ReferenceQueue queue = new ReferenceQueue();
      Object o = new Object() {
        @Override
        public String toString() {
          return "zhangsan";
        }
      };
      
      Reference ref = new WeakReference(o, queue);
      new Monitor(queue).start();
      
      o = null;
      log.info("Before GC, referent:{}", ref.get());
      System.gc();
      log.info("After GC, referent:{}", ref.get());
    }
    

    // 打印:

    [main]     Before GC, referent:zhangsan
    [main]     After GC, referent:null
    [Thread-0] remove reference:java.lang.ref.WeakReference@67ac4ff0
    

    可以看到在内存足够的时候,referent 被清除,WeakReference 在下次 GC 的时候随机被清除,并且 ReferenceQueue 也收到了事件通知;

    四、PhantomReference

    虚引用:最弱的一种引用关系,虚引用对一个对象的生命周期完全没有影响,设置虚引用的唯一目的就是得到 referent 被回收的事件通知;

    public class PhantomReference<T> extends Reference<T> {
        public T get() {
            return null;
        }
    
        public PhantomReference(T referent, ReferenceQueue<? super T> q) {
            super(referent, q);
        }
    }
    

    从源码也能看到 get 的时候,永远返回 null;

    同样简单测试一下,

    private static void test06() {
      ReferenceQueue queue = new ReferenceQueue();
      Object o = new Object() {
        @Override
        public String toString() {
          return "zhangsan";
        }
      };
      
      Reference ref = new PhantomReference(o, queue);
      new Monitor(queue).start();
      
      o = null;
      log.info("Before GC, referent:{}", ref.get());
      System.gc();
      log.info("After GC, referent:{}", ref.get());
    }
    

    // 打印:

    [main]     Before GC, referent:null
    [main]     After GC, referent:null
    [Thread-0] remove reference:java.lang.ref.PhantomReference@661a5fff
    

    可以看到 PhantomReference.get() 始终为 null,并且当 referent 被回收的时候,并且 ReferenceQueue 也收到了事件通知;

    此外 PhantomReference 和其他引用还有一个很大的不同,在 ReferenceQueue 中 JVM 并不会帮我们把 referent 字段置为空;

    private static void test07() {
      ReferenceQueue queue = new ReferenceQueue();
      Object o = new Object() {
        @Override
        public String toString() {
          return "zhangsan";
        }
      };
      
      Reference ref = new PhantomReference(o, queue);
      new Monitor2(queue).start();
      
      o = null;
      log.info("Before GC, referent:{}", ref.get());
      System.gc();
      log.info("After GC, referent:{}", ref.get());
    }
    
    private static class Monitor2 extends Thread {
      ReferenceQueue queue;
      
      public Monitor2(ReferenceQueue queue) {
        this.queue = queue;
      }
      
      @Override
      public void run() {
        try {
          while (true) {
            Reference ref = queue.poll();
            log.info("remove reference:{}", ref);
            if (ref != null) {
            Field field = Reference.class.getDeclaredField("referent");
            field.setAccessible(true);
            
            log.info("ReferenceQueue get Referent:{}", field.get(ref));
            ref.clear();
            break;
            }
          }
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    }
    

    // 打印:

    [main]     Before GC, referent:null
    [main]     After GC, referent:null
    [Thread-0] remove reference:null
    [Thread-0] remove reference:java.lang.ref.PhantomReference@7b4cba2
    [Thread-0] ReferenceQueue get Referent:zhangsan
    

    这里可以看到从 ReferenceQueue 中取出来的 Reference 仍然可以取到引用对象,即 referent;但是在其他引用中打印为 null,这里可以将上面例子中的 Monitor 改为 Monitor2 测试;

    Cleaner
    Reference.tryHandlePending()里面提到的,主要用于替代Object.finalize();

    public class Cleaner extends PhantomReference<Object> {
      private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();
      static private Cleaner first = null;
    
      private Cleaner
        next = null,
        prev = null;
        
      private final Runnable thunk;
      
      private Cleaner(Object referent, Runnable thunk) {
        super(referent, dummyQueue);
        this.thunk = thunk;
      }
      
      public static Cleaner create(Object ob, Runnable thunk) {
        if (thunk == null)
          return null;
        return add(new Cleaner(ob, thunk));
      }
    
      private static synchronized Cleaner add(Cleaner cl) {
        if (first != null) {
          cl.next = first;
          first.prev = cl;
        }
        first = cl;
        return cl;
      }
    
      private static synchronized boolean remove(Cleaner cl) { }
    
      public void clean() {
        if (!remove(this))
          return;
        try {
          thunk.run();
        } catch (final Throwable x) {
          AccessController.doPrivileged(new PrivilegedAction<Void>() {
              public Void run() {
                if (System.err != null)
                  new Error("Cleaner terminated abnormally", x)
                    .printStackTrace();
                System.exit(1);
                return null;
              }});
        }
      }
    }
    
    

    从代码可以看到,

    • Cleaner 只能通过工厂方法创建,并且所有的 Cleaner 都共同属于同一个 Reference 链表;
    • 代码中的next、prev不同于 Reference 中的 next,他们组成了一个双向链表;
    • Cleaner 中没有入队操作,在创建之初就已经加入链表了,具体代码可以查看Reference.tryHandlePending()
    • ReferenceQueue(dummyQueue 域)的作用不再是提供入队和事件监听功能,而仅仅是保证 GC 不会自动将 Cleaner 给回收了;
    • Cleaner 的主要逻辑就是传入一个 clean 线程,在 referent 引用对象清除的时候,执行 clean 操作;

    总结

    • 对于上面讲的软引用、弱引用、虚引用,都有一套共同的事件通知机制,具体逻辑在 Reference 类中;主要的差别在于引用回收条件的判断,这部分代码在 JVM 里面;
    • 另外对于 Reference 类还有 FinalReference 没有写,主要用于当类重写finalize方法时,JVM 会将他包装在 FinalReference 里面,里面的细节比较多,并且一般不建议使用,所以暂时没写;
    • 此外《Effective Java》第三版的第八条也讲了避免使用finalizer和cleaner;详情可以自行查阅;

    参考

    http://www.importnew.com/21628.html
    https://www.jianshu.com/p/95a4931ebf01
    https://juejin.im/post/5bbfee46e51d450e5e0cba2f

  • 相关阅读:
    边工作边刷题:70天一遍leetcode: day 11-1
    边工作边刷题:70天一遍leetcode: day 11
    边工作边刷题:70天一遍leetcode: day 12-1
    边工作边刷题:70天一遍leetcode: day 12
    边工作边刷题:70天一遍leetcode: day 13-1
    边工作边刷题:70天一遍leetcode: day 13-2
    边工作边刷题:70天一遍leetcode: day 13
    边工作边刷题:70天一遍leetcode: day 14-1
    边工作边刷题:70天一遍leetcode: day 14
    边工作边刷题:70天一遍leetcode: day 15
  • 原文地址:https://www.cnblogs.com/sanzao/p/10343166.html
Copyright © 2011-2022 走看看