在阅读本文之前最好对 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 适合实现内存敏感的缓存;

  1. public class SoftReference<T> extends Reference<T> {
  2. /**
  3. * Timestamp clock, updated by the garbage collector
  4. */
  5. static private long clock;
  6. /**
  7. * Timestamp updated by each invocation of the get method. The VM may use
  8. * this field when selecting soft references to be cleared, but it is not
  9. * required to do so.
  10. */
  11. private long timestamp;
  12. public SoftReference(T referent) {
  13. super(referent);
  14. this.timestamp = clock;
  15. }
  16. public SoftReference(T referent, ReferenceQueue<? super T> q) {
  17. super(referent, q);
  18. this.timestamp = clock;
  19. }
  20. public T get() {
  21. T o = super.get();
  22. if (o != null && this.timestamp != clock)
  23. this.timestamp = clock;
  24. return o;
  25. }
  26. }

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

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

回收策略

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

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

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

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

  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 启动参数;
  1. private static void test03() throws InterruptedException {
  2. ReferenceQueue queue = new ReferenceQueue();
  3. Object o = new Object() {
  4. @Override
  5. public String toString() {
  6. return "zhangsan";
  7. }
  8. };
  9. Reference softRef = new SoftReference(o, queue);
  10. new Monitor(queue).start();
  11. o = null;
  12. System.gc();
  13. log.info("o=null, referent:{}", softRef.get());
  14. byte[] bytes = new byte[3 * 1024 * 1024];
  15. System.gc();
  16. log.info("After GC, referent:{}", softRef.get());
  17. Thread.sleep(2000);
  18. System.gc();
  19. log.info("After GC, referent:{}", softRef.get());
  20. }
  21. private static class Monitor extends Thread {
  22. ReferenceQueue queue;
  23. public Monitor(ReferenceQueue queue) {
  24. this.queue = queue;
  25. }
  26. @Override
  27. public void run() {
  28. while (true) {
  29. try {
  30. log.info("remove reference:{}", queue.remove().toString());
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. }
  36. }

// 打印:

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

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

三、WeakReference

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

  1. public class WeakReference<T> extends Reference<T> {
  2. public WeakReference(T referent) {
  3. super(referent);
  4. }
  5. public WeakReference(T referent, ReferenceQueue<? super T> q) {
  6. super(referent, q);
  7. }
  8. }

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

  1. private static void test04() {
  2. ReferenceQueue queue = new ReferenceQueue();
  3. Object o = new Object() {
  4. @Override
  5. public String toString() {
  6. return "zhangsan";
  7. }
  8. };
  9. Reference ref = new WeakReference(o, queue);
  10. new Monitor(queue).start();
  11. o = null;
  12. log.info("Before GC, referent:{}", ref.get());
  13. System.gc();
  14. log.info("After GC, referent:{}", ref.get());
  15. }

// 打印:

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

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

四、PhantomReference

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

  1. public class PhantomReference<T> extends Reference<T> {
  2. public T get() {
  3. return null;
  4. }
  5. public PhantomReference(T referent, ReferenceQueue<? super T> q) {
  6. super(referent, q);
  7. }
  8. }

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

同样简单测试一下,

  1. private static void test06() {
  2. ReferenceQueue queue = new ReferenceQueue();
  3. Object o = new Object() {
  4. @Override
  5. public String toString() {
  6. return "zhangsan";
  7. }
  8. };
  9. Reference ref = new PhantomReference(o, queue);
  10. new Monitor(queue).start();
  11. o = null;
  12. log.info("Before GC, referent:{}", ref.get());
  13. System.gc();
  14. log.info("After GC, referent:{}", ref.get());
  15. }

// 打印:

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

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

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

  1. private static void test07() {
  2. ReferenceQueue queue = new ReferenceQueue();
  3. Object o = new Object() {
  4. @Override
  5. public String toString() {
  6. return "zhangsan";
  7. }
  8. };
  9. Reference ref = new PhantomReference(o, queue);
  10. new Monitor2(queue).start();
  11. o = null;
  12. log.info("Before GC, referent:{}", ref.get());
  13. System.gc();
  14. log.info("After GC, referent:{}", ref.get());
  15. }
  16. private static class Monitor2 extends Thread {
  17. ReferenceQueue queue;
  18. public Monitor2(ReferenceQueue queue) {
  19. this.queue = queue;
  20. }
  21. @Override
  22. public void run() {
  23. try {
  24. while (true) {
  25. Reference ref = queue.poll();
  26. log.info("remove reference:{}", ref);
  27. if (ref != null) {
  28. Field field = Reference.class.getDeclaredField("referent");
  29. field.setAccessible(true);
  30. log.info("ReferenceQueue get Referent:{}", field.get(ref));
  31. ref.clear();
  32. break;
  33. }
  34. }
  35. } catch (Exception e) {
  36. e.printStackTrace();
  37. }
  38. }
  39. }

// 打印:

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

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

Cleaner

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

  1. public class Cleaner extends PhantomReference<Object> {
  2. private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();
  3. static private Cleaner first = null;
  4. private Cleaner
  5. next = null,
  6. prev = null;
  7. private final Runnable thunk;
  8. private Cleaner(Object referent, Runnable thunk) {
  9. super(referent, dummyQueue);
  10. this.thunk = thunk;
  11. }
  12. public static Cleaner create(Object ob, Runnable thunk) {
  13. if (thunk == null)
  14. return null;
  15. return add(new Cleaner(ob, thunk));
  16. }
  17. private static synchronized Cleaner add(Cleaner cl) {
  18. if (first != null) {
  19. cl.next = first;
  20. first.prev = cl;
  21. }
  22. first = cl;
  23. return cl;
  24. }
  25. private static synchronized boolean remove(Cleaner cl) { }
  26. public void clean() {
  27. if (!remove(this))
  28. return;
  29. try {
  30. thunk.run();
  31. } catch (final Throwable x) {
  32. AccessController.doPrivileged(new PrivilegedAction<Void>() {
  33. public Void run() {
  34. if (System.err != null)
  35. new Error("Cleaner terminated abnormally", x)
  36. .printStackTrace();
  37. System.exit(1);
  38. return null;
  39. }});
  40. }
  41. }
  42. }

从代码可以看到,

  • 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

JDK源码分析(8)之 Reference 完全解读的更多相关文章

  1. JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue

    JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue 目的:本文通过分析JDK源码来对比ArrayBlockingQueue 和LinkedBlocki ...

  2. JDK 源码分析(4)—— HashMap/LinkedHashMap/Hashtable

    JDK 源码分析(4)-- HashMap/LinkedHashMap/Hashtable HashMap HashMap采用的是哈希算法+链表冲突解决,table的大小永远为2次幂,因为在初始化的时 ...

  3. JDK源码分析(三)—— LinkedList

    参考文档 JDK源码分析(4)之 LinkedList 相关

  4. JDK源码分析(一)—— String

    dir 参考文档 JDK源码分析(1)之 String 相关

  5. JDK源码分析(2)LinkedList

    JDK版本 LinkedList简介 LinkedList 是一个继承于AbstractSequentialList的双向链表.它也可以被当作堆栈.队列或双端队列进行操作. LinkedList 实现 ...

  6. 【JDK】JDK源码分析-LinkedHashMap

    概述 前文「JDK源码分析-HashMap(1)」分析了 HashMap 主要方法的实现原理(其他问题以后分析),本文分析下 LinkedHashMap. 先看一下 LinkedHashMap 的类继 ...

  7. 【JDK】JDK源码分析-HashMap(1)

    概述 HashMap 是 Java 开发中最常用的容器类之一,也是面试的常客.它其实就是前文「数据结构与算法笔记(二)」中「散列表」的实现,处理散列冲突用的是“链表法”,并且在 JDK 1.8 做了优 ...

  8. 【JDK】JDK源码分析-TreeMap(2)

    前文「JDK源码分析-TreeMap(1)」分析了 TreeMap 的一些方法,本文分析其中的增删方法.这也是红黑树插入和删除节点的操作,由于相对复杂,因此单独进行分析. 插入操作 该操作其实就是红黑 ...

  9. 【JDK】JDK源码分析-Vector

    概述 上文「JDK源码分析-ArrayList」主要分析了 ArrayList 的实现原理.本文分析 List 接口的另一个实现类:Vector. Vector 的内部实现与 ArrayList 类似 ...

  10. 【JDK】JDK源码分析-ArrayList

    概述 ArrayList 是 List 接口的一个实现类,也是 Java 中最常用的容器实现类之一,可以把它理解为「可变数组」. 我们知道,Java 中的数组初始化时需要指定长度,而且指定后不能改变. ...

随机推荐

  1. Dagger2 notes

    Android:dagger2让你爱不释手-基础依赖注入框架篇 Android Dagger2 MVP架构 一看就明白 Dagger2教程六之Component的组织方法(原) Dagger 2从浅到 ...

  2. BZOJ1991 : Pku2422 The Wolves and the Sheep

    将每个不是障碍的格子标号,设三只狼的位置分别为$A,B,C$,羊的位置在$D$.合法状态中强行限制$A<B<C$,这样状态数只有$\frac{n^8}{6}\approx 1.6\time ...

  3. NEERC训练实录

    听说这里可以做一些idea比较好的题.. 那就做做吧 2017-2018 ACM-ICPC, NEERC, Northern Subregional Contest A. Auxiliary Proj ...

  4. Windows10用fiddler抓包Android应用(解决手机设置代理后无法上网,设置只抓app包)

    1.环境准备 1.电脑上安装fiddler 2.手机和电脑在同一个局域网内 2.设置 1.fiddler>Tools>Fiddler Options>Connections 勾选Al ...

  5. List集合和JSON互转工具类

    public class JsonListUtil { /** * List<T> 转 json 保存到数据库 */ public static <T> String list ...

  6. [转载]SSH框架搭建详细图文教程

    http://www.cnblogs.com/hoobey/p/5512924.html

  7. 一·PTA实验作业

    本周要求挑选3道题目写设计思路,调试过程.设计思路用伪代码描述.题目选做要求: 顺序表选择一题(6-2,6-3,7-1选一题) 单链表选择一题(6-1不能选) 有序表选择一题 一.题目 6-3 jmu ...

  8. [转]Setting Keystone v3 domains

    http://www.florentflament.com/blog/setting-keystone-v3-domains.html The Openstack Identity v3 API, p ...

  9. ArrayList源码理解

    ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用Col ...

  10. PHP_DOC php文档结构及注解浏览

    项目中的PHP文件比较多,为了方便查看,使用PHP写了个小工具,可查看PHP文件的所有类.函数 和特定注释. 显示PHP文件的 Class 和 Function 显示 /// 开头的注解 显示 /// ...