解读 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】。文章转载请联系作者。
评论