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;
}
根据上面的代码可以大致知道:
- 首先计算出了最大堆内存和上次 GC 时剩余的内存;
- 再用(剩余内存 / 最大内存 )* SoftRefLRUPolicyMSPerMB 得出到下次 GC 期间软引用的最大 idle 时间;
- 最后用 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
JDK源码分析(8)之 Reference 完全解读的更多相关文章
- JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue
JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue 目的:本文通过分析JDK源码来对比ArrayBlockingQueue 和LinkedBlocki ...
- JDK 源码分析(4)—— HashMap/LinkedHashMap/Hashtable
JDK 源码分析(4)-- HashMap/LinkedHashMap/Hashtable HashMap HashMap采用的是哈希算法+链表冲突解决,table的大小永远为2次幂,因为在初始化的时 ...
- JDK源码分析(三)—— LinkedList
参考文档 JDK源码分析(4)之 LinkedList 相关
- JDK源码分析(一)—— String
dir 参考文档 JDK源码分析(1)之 String 相关
- JDK源码分析(2)LinkedList
JDK版本 LinkedList简介 LinkedList 是一个继承于AbstractSequentialList的双向链表.它也可以被当作堆栈.队列或双端队列进行操作. LinkedList 实现 ...
- 【JDK】JDK源码分析-LinkedHashMap
概述 前文「JDK源码分析-HashMap(1)」分析了 HashMap 主要方法的实现原理(其他问题以后分析),本文分析下 LinkedHashMap. 先看一下 LinkedHashMap 的类继 ...
- 【JDK】JDK源码分析-HashMap(1)
概述 HashMap 是 Java 开发中最常用的容器类之一,也是面试的常客.它其实就是前文「数据结构与算法笔记(二)」中「散列表」的实现,处理散列冲突用的是“链表法”,并且在 JDK 1.8 做了优 ...
- 【JDK】JDK源码分析-TreeMap(2)
前文「JDK源码分析-TreeMap(1)」分析了 TreeMap 的一些方法,本文分析其中的增删方法.这也是红黑树插入和删除节点的操作,由于相对复杂,因此单独进行分析. 插入操作 该操作其实就是红黑 ...
- 【JDK】JDK源码分析-Vector
概述 上文「JDK源码分析-ArrayList」主要分析了 ArrayList 的实现原理.本文分析 List 接口的另一个实现类:Vector. Vector 的内部实现与 ArrayList 类似 ...
- 【JDK】JDK源码分析-ArrayList
概述 ArrayList 是 List 接口的一个实现类,也是 Java 中最常用的容器实现类之一,可以把它理解为「可变数组」. 我们知道,Java 中的数组初始化时需要指定长度,而且指定后不能改变. ...
随机推荐
- 【工作手札】Nginx接口代理可跨域
接口代理nginx配置 location /api/ { proxy_set_header Host api.shenjian.io; proxy_set_header X-Forwarded-For ...
- Android Keystore 对称-非对称加密
Android数据加密: Anroid数据加密方式 Android 提供了 KeyStore 等可以长期存储和检索加密密钥的机制,Android KeyStore 系统特别适合于存储加密密钥. “An ...
- 关于VB里判断逻辑的说明
如上图,当进行连续判断的时候,即使第一个已经不符合条件了,后面的依然会计算.这点一定要记住,除非你所有的函数都有必要执行,否则会导致效率降低. 减代码不一定能提高效率,对于IIF和连续判断写法,貌似很 ...
- JUC笔记
3个售票员,卖30张票 package com.javase.thread; import java.util.concurrent.locks.Lock; import java.uti ...
- IOS开发中将定时器添加到runLoop中
runLoop主要就是为线程而生的.他能够让线程在有任务的时候保持工作状态,没有任务的时候让线程处于休眠待备状态. 主线程的runloop默认是开启的.主线程上创建的定时器已经默认添加到runLoop ...
- group by 多个字段
众所周知,group by 一个字段是根据这个字段进行分组,那么group by 多个字段的结果是什么呢?由前面的结论类比可以得到,group by 后跟多个子段就是根据多个字段进行分组 注:下面的例 ...
- Objective-C 优秀文章分享
1. Objective-C Runtime 2.KVO + Block 3.Method Swizzling 和 AOP 实践
- QEMU KVM Libvirt手册(7): 硬件虚拟化
在openstack中,如果我们启动一个虚拟机,我们会看到非常复杂的参数 qemu-system-x86_64 -enable-kvm -name instance-00000024 -S -mach ...
- rem布局完成响应式开发,通俗且详细的原理解析和代码实现
一.rem布局基本原理 原理:rem可以理解为一个长度单位,单位rem的值等于网页font-size的值.如果网页的字体大小为默认值16px,那么1rem就等于16px,0.5rem等于8px. 根据 ...
- Java实现堆排序和计数排序
堆排序代码: 思想:每次都取堆顶的元素,将其放在序列最后面,然后将剩余的元素重新调整为最小堆,依次类推,最终得到排序的序列. import java.util.Arrays; /** * 思路:首先要 ...