写点什么

解读 Reference

用户头像
浮白
关注
发布于: 2020 年 08 月 24 日
解读 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 字段指向 Referencepending,然后再将 Referencepending 指向这个 Reference 实例。也就是把这个 Reference 实例添加到 pending-Reference 链表的头部。Reference 类中有个静态代码块,启动了一个叫做 **Reference-handler** 的线程,该线程不断的从 **pending-Reference** 链表的头部取出 Reference 实例,然后再将其添加到其自身的 queue 中去。简而言之就是,不同的 Reference 实例注册了不同的 queue,垃圾回收线程将所有的 Reference 实例统一添加到 pending-Reference 链表中。Reference-handler 线程再将 pending-Reference 链表中的元素分别添加到各自的 queue 中。

Reference 实例的状态迁移



/* A Reference instance is in one of four possible internal states:
*
* Active: Subject to special treatment by the garbage collector. Some
* time after the collector detects that the reachability of the
* referent has changed to the appropriate state, it changes the
* instance's state to either Pending or Inactive, depending upon
* whether or not the instance was registered with a queue when it was
* created. In the former case it also adds the instance to the
* pending-Reference list. Newly-created instances are Active.
*
* Pending: An element of the pending-Reference list, waiting to be
* enqueued by the Reference-handler thread. Unregistered instances
* are never in this state.
*
* Enqueued: An element of the queue with which the instance was
* registered when it was created. When an instance is removed from
* its ReferenceQueue, it is made Inactive. Unregistered instances are
* never in this state.
*
* Inactive: Nothing more to do. Once an instance becomes Inactive its
* state will never change again.
*
* The state is encoded in the queue and next fields as follows:
*
* Active: queue = ReferenceQueue with which instance is registered, or
* ReferenceQueue.NULL if it was not registered with a queue; next =
* null.
*
* Pending: queue = ReferenceQueue with which instance is registered;
* next = this
*
* Enqueued: queue = ReferenceQueue.ENQUEUED; next = Following instance
* in queue, or this if at end of list.
*
* Inactive: queue = ReferenceQueue.NULL; next = this.
*
* With this scheme the collector need only examine the next field in order
* to determine whether a Reference instance requires special treatment: If
* the next field is null then the instance is active; if it is non-null,
* then the collector should treat the instance normally.
*
* To ensure that a concurrent collector can discover active Reference
* objects without interfering with application threads that may apply
* the enqueue() method to those objects, collectors should link
* discovered objects through the discovered field. The discovered
* field is also used for linking Reference objects in the pending list.
*
* 一个 Reference 实例处于如下四种可能的状态之一:
*
* Active: 被垃圾回收器特殊对待的状态。当垃圾回收器检测到 referent 不是强可达时,就会将 Reference 实
* 例的状态调整为 Pending 或 Inactive,具体调整为哪一种取决于创建 Reference 实例时是否注册了 queue。
* 如果注册了就进入 Pending 状态,也就是把 Reference 实例添加到 pending-Reference 链表中。新建的
* Reference 实例处于 Active 状态。
*
* Pending: 处于 pending-Reference 链表中,等待 Reference-handler 线程将它们放入到 queue 中。未
* 注册的实例永远也不会进入此状态。
*
* Enqueued: 处于 queue 中,即创建 Reference 实例时所注册的 ReferenceQueue,当 Reference 实例被
* ReferenceQueue 移除时,会进入Inactive 状态。未注册的实例永远也不会进入此状态。
*
* Inactive: 在此状态下不需要再做什么。一旦一个实例进入 Inactive 状态,其状态就不会再发生改变。
*
* Reference 实例的状态被编码在 queue 和 next 字段中:
*
* Active: queue 等于注册的 ReferenceQueue,若没注册就等于 ReferenceQueue.NULL。next 等于 null;
*
* Pending: queue 等于参与注册的 ReferenceQueue;next 等于 this;
*
* Enqueued: queue 等于 ReferenceQueue.ENQUEUED;next 等于 queue 中下一个实例,如果实例处于 queue
* 的末尾,那么 next 等于 this。
*
* Inactive: queue 等于 ReferenceQueue.NULL; next 等于 this。
*
* 有了上述方案,垃圾回收器只需要检查 next 字段就能决定是否要特殊对待一个 Reference 实例:如果 next 字段为
* 空,那其状态就是 Active;如果不是空,那就不用特殊对待。
*
*
* 应用线程会调用了 Reference 对象的 enqueue() 方法(这个方法修改了由 next 字段组织成的 ReferenceQueue),
* 为了保证并发的垃圾回收器不对应用线程产生干扰,垃圾回收线程在将 Active 状态的 Reference 对象迁移到 Pending
* 状态时,要使用 discovered 字段。discovered 字段就是用于组织 pending-Reference 链表的。
*/
private T referent; /* Treated specially by GC */
volatile ReferenceQueue<? super T> queue;
/* When active: NULL
* pending: this
* Enqueued: next reference in queue (or this if last)
* Inactive: this
*/
@SuppressWarnings("rawtypes")
volatile Reference next;
/* When active: next element in a discovered reference list maintained by GC (or this if last)
* pending: next element in the pending list (or null if last)
* otherwise: NULL
*/
transient private Reference<T> discovered; /* used by VM */
/* Object used to synchronize with the garbage collector. The collector
* must acquire this lock at the beginning of each collection cycle. It is
* therefore critical that any code holding this lock complete as quickly
* as possible, allocate no new objects, and avoid calling user code.
*/
static private class Lock { }
private static Lock lock = new Lock();
/* List of References waiting to be enqueued. The collector adds
* References to this list, while the Reference-handler thread removes
* them. This list is protected by the above lock object. The
* list uses the discovered field to link its elements.
*/
private static Reference<Object> pending = null;



本文源码都是基于 JDK 8。



从上面的 Java doc 可以得出 Reference 实例的状态迁移路径如下:





Reference-handler 线程将 Reference 实例从 Pending 状态迁移到 Enqueued 状态,先将其从 pending-Reference 列表中取出,然后再入队到 ReferenceQueue 中。 具体代码如下:



static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) {
r = pending;
// 'instanceof' might throw OutOfMemoryError sometimes
// so do this before un-linking 'r' from the 'pending' chain...
c = r instanceof Cleaner ? (Cleaner) r : null;
// unlink 'r' from 'pending' chain
pending = r.discovered;
r.discovered = null;
} else {
// The waiting on the lock may cause an OutOfMemoryError
// because it may try to allocate exception objects.
if (waitForNotify) {
lock.wait();
}
// retry if waited
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
// Give other threads CPU time so they hopefully drop some live references
// and GC reclaims some space.
// Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
// persistently throws OOME for some time...
Thread.yield();
// retry
return true;
} catch (InterruptedException x) {
// retry
return true;
}
// Fast path for cleaners
if (c != null) {
c.clean();
return true;
}
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}



注:如果 Reference 实例是一个 sun.misc.Cleaner(继承自 java.lang.ref.PhantomReference),那就不用入队了,直接调用其 clean() 方法即可。NIO 采用了 sun.misc.Cleaner 来回收直接内存的。

Finalizer 有什么用



java.lang.ref.Finalizer 继承自 java.lang.ref.FinalReferencejava.lang.ref.FinalReference 继承自 java.lang.ref.ReferenceFinalizer 利用了 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 对象作为参数:



final class Finalizer extends FinalReference {
/* A native method that invokes an arbitrary object's finalize method is
required since the finalize method is protected
*/
static native void invokeFinalizeMethod(Object o) throws Throwable;
private static ReferenceQueue queue = new ReferenceQueue();
private static Finalizer unfinalized = null;
private static final Object lock = new Object();
private Finalizer
next = null,
prev = null;
private Finalizer(Object finalizee) {
super(finalizee, queue);
add();
}
/* Invoked by VM */
static void register(Object finalizee) {
new Finalizer(finalizee);
}
private void add() {
synchronized (lock) {
if (unfinalized != null) {
this.next = unfinalized;
unfinalized.prev = this;
}
unfinalized = this;
}
}
...
}



注册逻辑如下:



  1. 创建 Finalizer 对象,并将其添加到 Finalizer.unfinalized 指向的双向链表中;

  2. Finalizer 对象的 referent 字段赋值为 f 对象;

  3. 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 的原理进行工作的



WeakHashMapEntry 继承自 WeakReferenceEntry 的 key 被 Referencereferent 字段引用,除此之外,再无其他引用。每一个 WeakHashMap 都有一个 ReferenceQueue<Object> 类型的 queue 字段,在创建 Entry 时,将 key 和 queue 传递给了 WeakReference 的构造函数。因为 key 仅有弱引用可达,所以只要发生 GC,key 就会被回收,于是其 Entry 实例就进入了 queue 中。WeakHashMap 对外提供的方法在内部都会调用其 expungeStaleEntries() 方法,该方法会将 queue 中的 Entry 实例从 table 中清除。



下面是 WeakHashMap 中跟 Reference 有关的逻辑,其中的 ... 表示被省略的代码:



public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> {
...
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
Entry<K,V>[] table;
/**
* The number of key-value mappings contained in this weak hash map.
*/
private int size;
...
/**
* Reference queue for cleared WeakEntries
*/
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
...
/**
* Expunges stale entries from the table.
*/
private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) { // 从 queue 中取出要被 GC 的 entry,通过下面的代码,将其从 table 中清理掉
synchronized (queue) {
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;
int i = indexFor(e.hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
if (prev == e)
table[i] = next; // entry 在 table[i] 链表的头部,直接将头部设置为 entry 的下一个元素
else
prev.next = next; // entry 在 table[i] 链表的中间,将 entry 的前一个元素的 next 指向 entry 的下一个元素
// Must not null out e.next;
// stale entries may be in use by a HashIterator
e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}
...
/**
* Removes all of the mappings from this map.
* The map will be empty after this call returns.
*/
public void clear() {
// clear out ref queue. We don't need to expunge entries
// since table is getting cleared.
while (queue.poll() != null)
;
modCount++;
Arrays.fill(table, null);
size = 0;
// Allocation of array may have caused GC, which may have caused
// additional entries to go stale. Removing these entries from the
// reference queue will make them eligible for reclamation.
while (queue.poll() != null)
;
}
...
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;
/**
* Creates new entry.
*/
Entry(Object key, V value, ReferenceQueue<Object> queue, int hash, Entry<K,V> next) {
super(key, queue); // 将 key 设置到 Reference 的 referent 字段;将 queue 设置到 Reference 的 queue 字段
this.value = value;
this.hash = hash;
this.next = next;
}
@SuppressWarnings("unchecked")
public K getKey() {
return (K) WeakHashMap.unmaskNull(get()); // 从 referent 字段上读取 key
}
...
}
...

ThreadLocal 与 Reference 有什么关系

什么是 ThreadLocal



在多线程环境下,可以使用一个共享的 ThreadLocal 实例来管理各个线程的独有变量。它能实现线程间的变量隔离;也能在单线程内,实现在不同类和方法之间无须传递即可访问同一个变量的目的。

ThreadLocal 的实现原理



每一个 Thread 对象上都一个 ThreadLocal.ThreadLocalMap 类型的字段,即 threadLocalsThreadLocalMap 是一个基于 hash 算法的 map,其键的类型是 ThreadLocal<?>。初始时,一个线程的 threadLocals 为空,当用户通过一个 ThreadLocal 实例来读取或设置数据时,就会触发对 threadLocals 的初始化,这部分代码如下:



...
/**
* 返回当前线程的 thread-local 变量的副本。如果当前线程没有这个
* 变量的值,就调用 initialValue 方法,用其返回值来初始化该变量。
*
* @return 当前线程的本 thread-local 变量的值
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
/**
* set() 方法的变体,用于设置初始值。如果用户重写了 set() 方法,
* 那么,该方法的存在还能确保初始值的成功设置。
*
* @return 初始值
*/
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value); // 读取数据触发 threadLocals 的初始化
return value;
}
/**
* 将本 thread-local 变量在当前线程中的副本设置为指定的值。大多数子类都不需
* 要重写该方法,如果有重写的需求,一般也是重写 initialValue 方法来为不同的
* thread-local 变量提供初始的值。
*
* @param value 要为当前线程存储到本 thread-local 变量中的副本的值。
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value); // 设置数据触发 threadLocals 的初始化
}
...
/**
* 获取当前线程的 threadLocals 字段的值。
*
* @param t 当前线程
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* 为当前线程初始化其 threadLocals 字段。
*
* @param t 当前线程
* @param firstValue 存储到 map 里的第一个值
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
...



每一个线程都有自己的 threadLocals 变量,ThreadLocal 借助这个变量,以自己的实例作为 key,将不同线程的 value 存储到了各个线程的 threadLocals 上。实现了在一个共享的 ThreadLocal 实例上管理了各个线程独有变量的功能。

ThreadLocal 的内存泄露问题



ThreadLocal 使用 ThreadLocalMap 来存储数据,ThreadLocalMapEntry 继承了 WeakReference,代码如下:



/**
* ThreadLocalMap 是一个自定义的哈希 map,只适用于维护线程本地变量。它的任何操作都没有暴露到
* ThreadLocal 类之外。该类是包私有的,以允许 Thread 类声明一个 ThreadLocalMap 类型的字段。
* 为了适应大的和长期存活的变量,其 Entry 使用了弱引用来指向其 key。但是,由于没有注册引用队列,
* 所以不保证一定会清除已经过期的 Entry,只有在哈希表满了,需要扩容且 rehash 的时候,才能保证
* 清除已过期的 Entry。
*/
static class ThreadLocalMap {
/**
* 本哈希 map 的 Entry 继承自 WeakReference,使用了 Reference 的 referent 字段作为
* key(它永远是一个 ThreadLocal 对象)。当 key 为 null(比如 entry.get() == null)
* 时,意味着不再存储该 key 了,所以其 entry 也应该从 table 中清除了。下面的代码把这样的
* entry 叫做 “stale entries”(不新鲜的 entry)。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // Entry 没有注册 queue
value = v;
}
}
...
}



由于 ThreadLocalMapEntry 没有注册 ReferenceQueue,所以垃圾回收线程无法通知 ThreadLocalMap 有哪些 entry 的 key 被回收了。ThreadLocalMap 中的部分操作会清理“不新鲜的 entry”,但是这种清理不是有目的性的,而是碰到了才清理,也只清理碰到的“不新鲜的 entry”。不像 WeakHashMap 那样,能根据 queue 一次性的清理掉所有“不新鲜的 entry”。所以在使用 ThreadLocalMap 时,是存在着内存泄露的风险的,而解决办法就是及时的调用其 remove() 方法。



由于 ThreadLocalMapEntry 没有注册 ReferenceQueue,所以 Entry 直接从 Active 状态迁移到了 Inactive 状态,其 key 也就被垃圾回收器回收了,所以就为 null 了。

参考

发布于: 2020 年 08 月 24 日阅读数: 59
用户头像

浮白

关注

写一写自己觉得还不错的东西 2018.03.23 加入

还未添加个人简介

评论

发布
暂无评论
解读 Reference