JDK源码分析(8)之 Reference 实现和应用

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

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

一、StrongReference

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

此时从 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:[email protected]

根据不同的参数设置会出现不同的情况,大家可以自行调节参数,验证上面的计算规则;另外如果-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:[email protected]

可以看到在内存足够的时候,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:[email protected]

可以看到 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:[email protected]
[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()
  • Cleaner 的主要逻辑就是传入一个clean线程,在 referent 引用对象清除的时候,这行这个清楚操作;

总结

  • 对于上面讲的软引用、弱引用、虚引用,都有一套共同的事件通知机制,具体逻辑在 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

原文地址:https://www.cnblogs.com/sanzao/p/10343166.html

时间: 2024-10-10 07:37:08

JDK源码分析(8)之 Reference 实现和应用的相关文章

JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue

目的:本文通过分析JDK源码来对比ArrayBlockingQueue 和LinkedBlockingQueue,以便日后灵活使用. 1. 在Java的Concurrent包中,添加了阻塞队列BlockingQueue,用于多线程编程.BlockingQueue的核心方法有: boolean add(E e) ,把 e 添加到BlockingQueue里.如果BlockingQueue可以容纳,则返回true,否则抛出异常. boolean offer(E e),表示如果可能的话,将 e 加到B

JDK源码分析之String篇

------------------------------String在内存中的存储情况(一下内容摘自参考资料1)----------------------------------- 前提:先了解下什么是声明,什么时候才算是产生了对象实例 其中x并未看到内存分配,变量在使用前必须先声明,再赋值,然后才可以使用.java基础数据类型会用对应的默认值进行初始化 一.首先看看Java虚拟机JVM的内存块及其变量.对象内存空间是怎么存储分配的 1.栈:存放基本数据类型及对象变量的引用,对象本身不存放

【JDK源码分析】通过源码分析CyclicBarrier

前言 CyclicBarrier它是什么?一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点.类似于朋友之间联系要在中午聚个会,几个朋友全部到齐后才开始喝酒吃菜. 源码 CyclicBarrier属性和构造器 public class CyclicBarrier { // 互斥锁 private final ReentrantLock lock = new ReentrantLock(); // 条件等待 private final Condition trip = lock.new

【JDK】JDK源码分析-CountDownLatch

概述 CountDownLatch 是并发包中的一个工具类,它的典型应用场景为:一个线程等待几个线程执行,待这几个线程结束后,该线程再继续执行. 简单起见,可以把它理解为一个倒数的计数器:初始值为线程数,每个线程结束时执行减 1 操作,当计数器减到 0 时等待的线程再继续执行. 代码分析 CountDownLatch 的类签名和主要方法如下: public class CountDownLatch {} 常用方法为:await().await(long, TimeUnit) 和 countDow

【JDK】JDK源码分析-Semaphore

概述 Semaphore 是并发包中的一个工具类,可理解为信号量.通常可以作为限流器使用,即限制访问某个资源的线程个数,比如用于限制连接池的连接数. 打个通俗的比方,可以把 Semaphore 理解为一辆公交车:车上的座位数(初始的“许可” permits 数量)是固定的,行驶期间如果有人上车(获取许可),座位数(许可数量)就会减少,当人满的时候不能再继续上车了(获取许可失败):而有人下车(释放许可)后就空出了一些座位,其他人就可以继续上车了. 下面具体分析其代码实现. 代码分析 Semapho

JDK源码分析之concurrent包(三) -- Future方式的实现

上一篇我们基于JDK的源码对线程池ThreadPoolExecutor的实现做了分析,本篇来对Executor框架中另一种典型用法Future方式做源码解读.我们知道Future方式实现了带有返回值的程序的异步调用,关于异步调用的场景大家可以自行脑补Ajax的应用(获取返回结果的方式不同,Future是主动询问获取,Ajax是回调函数),这里不做过多说明. 在进入源码前,首先来看下Future方式相关的API: 接口Callable:有返回结果并且可能抛出异常的任务: 接口Future:表示异步

jdk源码分析总览

今天看到了一个源码分析按照重要性排序的例子, 这里拿过来用了,之后按照这个顺序不断的完善源码的内容. 引用的出处忘记了(对作者说声抱歉) 很多java开发的小伙伴都会阅读jdk源码,然而确不知道应该从哪读起.以下为小编整理的通常所需阅读的源码范围. 标题为包名,后面序号为优先级1-4,优先级递减 1.java.lang 1) Object 12) String 13) AbstractStringBuilder 14) StringBuffer 15) StringBuilder 16) Boo

JDK源码分析(9)之 WeakHashMap 相关

平时我们使用最多的数据结构肯定是 HashMap,但是在使用的时候我们必须知道每个键值对的生命周期,并且手动清除它:但是如果我们不是很清楚它的生命周期,这时候就比较麻烦:通常有这样几种处理方式: 由一个线程定时处理,可以是Timer或者ScheduledThreadPoolExecutor: 利用重写LinkedHashMap.removeEldestEntry(),实现 FIFOCache 或者 LRUCache:可以参考我之前写的一篇博客 LinkedHashMap 相关: 利用 WeakH

【jdk源码分析】ArrayList的size()==0和isEmpty()

先看结果 分析源码 [jdk源码解析]jdk8的ArrayList初始化长度为0 java的基本数据类型默认值 无参构造 size()方法 isEmpty()方法 原文地址:https://www.cnblogs.com/xiaostudy/p/10781148.html

JDK源码分析--HashMap

HashMap为大家常用的java数据结构工具类,下面对HashMap进行源码分析. 类图结构如下: 其中AbstractMap实现了 public V get(Object key) , public V remove(Object key), public Set<K> keySet(), public Collection<V> values()等常用Map操作方法. 下面先分析下HashMap中的常量定义: /** * The default initial capacit