解读 Reference

什么是 Reference
从 JDK 1.2 开始,Java 升级了对引用的支持,将其分为四类,分别是强引用、软引用、弱引用和虚引用。
强引用:JDK 中没有代表强引用的类。我们最常使用的引用就是强引用,比如
Object sf = new Object()中的sf就是对Object实例的强引用;软引用:
java.lang.ref.SoftReference,如果一个对象只被SoftReference实例引用,那在内存不足时,它还是会被回收的;弱引用:
java.lang.ref.WeakReference,如果一个对象只被WeakReference实例引用,那在发生垃圾回收时,它就会被回收,不管内存是否充足;虚引用:
java.lang.ref.PhantomReference,在垃圾回收方面表现的和WeakReference一样,而且无法从其get()方法上得到被引用的对象。
简而言之就是,不可达的对象会被垃圾回收;仅软引用可达的对象在内存不足时就会被回收(适合用来实现内存敏感的高速缓存);仅弱引用可达的对象在发生 GC 时就会被回收;仅虚引用可达的对象不但在发生 GC 时就会被回收,而且还无法通过其 get() 方法访问到被引用的对象。
什么是 ReferenceQueue
Reference 里有一个 next 字段,通过这个字段可以将多个 Reference 实例组织成一个单向链表,一个 ReferenceQueue 就是用来管理这样的一个链表的。
在创建一个 Reference 实例时,可以在其构造方法上传入一个 ReferenceQueue 实例,它会被设置到 Reference 实例的 queue 字段上,JDK 称这个操作为 「为 Reference 实例注册 ReferenceQueue」。
Reference 里还有一个 discovered 字段,这个字段用于形成 pending-Reference 链表,它是一个全局链表,Reference 的静态字段 pending 指向了它。
如果 Reference 实例引用的对象不是强可达的,JVM 就可能会回收它。如果待回收对象的 Reference 实例注册了 ReferenceQueue,那么垃圾回收线程就会将其 discovered 字段指向 Reference 的 pending,然后再将 Reference 的 pending 指向这个 Reference 实例。也就是把这个 Reference 实例添加到 pending-Reference 链表的头部。Reference 类中有个静态代码块,启动了一个叫做 **Reference-handler** 的线程,该线程不断的从 **pending-Reference** 链表的头部取出 Reference 实例,然后再将其添加到其自身的 queue 中去。简而言之就是,不同的 Reference 实例注册了不同的 queue,垃圾回收线程将所有的 Reference 实例统一添加到 pending-Reference 链表中。Reference-handler 线程再将 pending-Reference 链表中的元素分别添加到各自的 queue 中。
Reference 实例的状态迁移
本文源码都是基于 JDK 8。
从上面的 Java doc 可以得出 Reference 实例的状态迁移路径如下:

Reference-handler 线程将 Reference 实例从 Pending 状态迁移到 Enqueued 状态,先将其从 pending-Reference 列表中取出,然后再入队到 ReferenceQueue 中。 具体代码如下:
注:如果
Reference实例是一个sun.misc.Cleaner(继承自java.lang.ref.PhantomReference),那就不用入队了,直接调用其clean()方法即可。NIO 采用了sun.misc.Cleaner来回收直接内存的。
Finalizer 有什么用
java.lang.ref.Finalizer 继承自 java.lang.ref.FinalReference,java.lang.ref.FinalReference 继承自 java.lang.ref.Reference。Finalizer 利用了 Reference 与垃圾回收相关的特性,为特殊的对象提供了一种在被回收前执行其 finalize() 方法的机制。
什么是 f 类
f 类是重写了 finalize() 方法且重写的方法体不为空的类。
f 类有什么特殊
JVM 在加载一个类时,会标记其是否为
f类。JVM 在创建
f类的实例时,会将其注册为finalizable对象。一个
f对象在第一次被 GC 回收时,JVM 会调用其finalize()方法。
假定
f类的实例叫做f对象。
JVM 是怎么注册 finalizable 对象的?
JVM 有一个叫做 RegisterFinalizersAtInit 的参数,默认为 true,会在 f 类的构造方法返回之前,让 JVM 注册 finalizable 对象。如果将其设置为 false,那么 JVM 会在分配好对象的内存空间之后就进行注册。hotspot 的具体实现是将 Object 类的构造方法的最后一个指令 return 替换为 returnregister_finalizer。
具体的注册逻辑在 Finalizer 的静态方法 register(Object finalizee) 中,_return_register_finalizer 指令会让 JVM 调用该方法,并使用新建的 f 对象作为参数:
注册逻辑如下:
创建
Finalizer对象,并将其添加到Finalizer.unfinalized指向的双向链表中;将
Finalizer对象的referent字段赋值为f对象;将
Finalizer对象的非静态字段queue赋值为Finalizer的静态字段queue。
垃圾回收线程决定要回收 f 对象时,将 f 对象的 Finalizer 实例添加到 pending-Reference 链表中。Reference-handler 线程将其从 pending-Reference 链表中取出,再添加到 Finalizer.queue 链表中。Finalizer 在静态代码块中启动了一个 FinalizerThread 线程。该线程以死循环的方法不断的从 Finalizer.queue 中取出 Finalizer 实例,并将其从 Finalizer.unfinalized 双向链表中移除(这个 Finalizer 实例变成不可达了),最后调用该实例的 f 对象的 finalize() 方法。
JVM 在调用 f 对象的 finalize() 方法之前,就会将其 Finalizer 实例变的不可达。假如 f 对象在 finalize() 方法中将自己变的重新可达,那在一段时候之后,当 f 对象再次不可达时,因为没有关联的 Finalizer 实例,所以其 finalize() 方法就不会再被执行了。
WeakHashMap 是如何利用 Reference 的原理进行工作的
WeakHashMap 的 Entry 继承自 WeakReference,Entry 的 key 被 Reference 的 referent 字段引用,除此之外,再无其他引用。每一个 WeakHashMap 都有一个 ReferenceQueue<Object> 类型的 queue 字段,在创建 Entry 时,将 key 和 queue 传递给了 WeakReference 的构造函数。因为 key 仅有弱引用可达,所以只要发生 GC,key 就会被回收,于是其 Entry 实例就进入了 queue 中。WeakHashMap 对外提供的方法在内部都会调用其 expungeStaleEntries() 方法,该方法会将 queue 中的 Entry 实例从 table 中清除。
下面是 WeakHashMap 中跟 Reference 有关的逻辑,其中的 ... 表示被省略的代码:
ThreadLocal 与 Reference 有什么关系
什么是 ThreadLocal
在多线程环境下,可以使用一个共享的 ThreadLocal 实例来管理各个线程的独有变量。它能实现线程间的变量隔离;也能在单线程内,实现在不同类和方法之间无须传递即可访问同一个变量的目的。
ThreadLocal 的实现原理
每一个 Thread 对象上都一个 ThreadLocal.ThreadLocalMap 类型的字段,即 threadLocals。ThreadLocalMap 是一个基于 hash 算法的 map,其键的类型是 ThreadLocal<?>。初始时,一个线程的 threadLocals 为空,当用户通过一个 ThreadLocal 实例来读取或设置数据时,就会触发对 threadLocals 的初始化,这部分代码如下:
每一个线程都有自己的 threadLocals 变量,ThreadLocal 借助这个变量,以自己的实例作为 key,将不同线程的 value 存储到了各个线程的 threadLocals 上。实现了在一个共享的 ThreadLocal 实例上管理了各个线程独有变量的功能。
ThreadLocal 的内存泄露问题
ThreadLocal 使用 ThreadLocalMap 来存储数据,ThreadLocalMap 的 Entry 继承了 WeakReference,代码如下:
由于 ThreadLocalMap 的 Entry 没有注册 ReferenceQueue,所以垃圾回收线程无法通知 ThreadLocalMap 有哪些 entry 的 key 被回收了。ThreadLocalMap 中的部分操作会清理“不新鲜的 entry”,但是这种清理不是有目的性的,而是碰到了才清理,也只清理碰到的“不新鲜的 entry”。不像 WeakHashMap 那样,能根据 queue 一次性的清理掉所有“不新鲜的 entry”。所以在使用 ThreadLocalMap 时,是存在着内存泄露的风险的,而解决办法就是及时的调用其 remove() 方法。
由于
ThreadLocalMap的Entry没有注册ReferenceQueue,所以Entry直接从 Active 状态迁移到了 Inactive 状态,其 key 也就被垃圾回收器回收了,所以就为 null 了。
参考
版权声明: 本文为 InfoQ 作者【浮白】的原创文章。
原文链接:【http://xie.infoq.cn/article/1817914961ef7570c1d079a06】。文章转载请联系作者。











评论