欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

JDK源码分析(8)之 Reference 完全解读

程序员文章站 2022-05-05 15:09:37
在阅读本文之前最好对 Reference 框架有一个整体的把握,可以参考我上一篇博客 "Reference 框架概览" ;本文主要讲了 的子类实现和应用( ); Java 引用的强弱关系 : 一、StrongReference 强引用 :我们通常使用的引用,形如 此时从 stack 中的 o,到 h ......

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

java 引用的强弱关系strongreference > softreference > weakreference > phantomreference

一、strongreference

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

JDK源码分析(8)之 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()
  • cleaner 的主要逻辑就是传入一个clean线程,在 referent 引用对象清除的时候,这行这个清楚操作;

总结

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

参考