Java Tutorials-13-引用

4种引用对比

  • 强引用: 只要强引用还在就不会被 GC,JVM 宁愿抛出 OutOfMemoryError 错误也不会回收;

  • 软引用(SoftReference): 用来描述非必需对象, GC 时发现内存不够时(Heap 内存超阈值)将会被回收。当垃圾回收器决定对其回收时,会先清空它的 SoftReference,也就是说 SoftReference 的 get() 方法将会返回 null,然后再调用对象的 finalize() 方法,并在下一轮 GC 中对其真正进行回收。

  • 弱引用(WeakReference): 也是用来描述非需对象的, 无论内存够不够,下次 GC 时一定都被回收, 但对象同时也被强引用持有,则不会回收;

  • 虚引用(PhantomReference): PhantomReference 无法用来获取对象,其 get 方法永远返回 null,为一个对象设置虚引用关联的唯一目的是跟踪对象被垃圾回收的状态,通过查看引用队列中是否包含对象所对应的虚引用来判断它是否即将被垃圾回收,当 PhantomReference 被放入队列时,说明 referent 的 finalize() 方法已经调用,并且垃圾收集器准备回收它的内存了。

  • FinalReference 以及 Finzlizer:@todo

相较于传统的引用计数算法,Java 使用可达性分析来判断一个对象是否存活。其基本思路是从 GC Root 开始向下搜索,如果对象与 GC Root 之间存在引用链,则对象是可达的。对象的可达性与引用类型密切相关。Java 有5中类型的可达性:

  1. 强可达(Strongly Reachable):如果线程能通过强引用访问到对象,那么这个对象就是强可达的。

  2. 软可达(Soft Reachable):如果一个对象不是强可达的,但是可以通过软引用访问到,那么这个对象就是软可达的

  3. 弱可达(Weak Reachable):如果一个对象不是强可达或者软可达的,但是可以通过弱引用访问到,那么这个对象就是弱可达的。

  4. 虚可达(Phantom Reachable):如果一个对象不是强可达,软可达或者弱可达,并且这个对象已经finalize过了,并且有虚引用指向该对象,那么这个对象就是虚可达的。

  5. 不可达(Unreachable):如果对象不能通过上述的几种方式访问到,则对象是不可达的,可以被回收。

How to Use:

ReferenceQueue<String> queue = new ReferenceQueue<>(); // 对象被回收后, 被放入q里

String str = new String("hello");
WeakReference<String> softRef = new WeakReference<String>(str, queue); // 这一步后, 有两个引用指向"Hello"
str = null; // 当String对象只有一个软引用指向它时, 才有可能被回收
System.out.print(softRef.get()); // 通过软引用调用对象用get方法

// 假设这里因内存不足发生了GC,被回收的软引用会被放入队列queue
// 从队列取出第一个:
Reference<? extends String> ref = queue.poll();

if(ref != null) { // 从队列里取出的ref是个引用地址,非空
if(ref.get() != null) { // 对ref再次get,肯定返回null
}
}

从 Soft、Weak、Phantom 三种引用的共同特点来看,都可以设置一个 ReferenceQueue,在 ref 关联的对象被回收时,ref 被放入 ReferenceQueue 中;

那么这个机制是如何实现的呢?

ReferenceQueue 的工作机制

class Reference {
private T referent;

// reference被回收后,当前 Reference实例会被添加到这个队列中
volatile ReferenceQueue<? super T> queue;

// 全局唯一的 pending-Reference 列表
private static Reference<Object> pending = null;

ReferenceHandler {
public void run() {
// 平时在 pending-Reference 上wait
// CG收集器 把引用放入 pending-Reference后,此线程开始工作:
}
}

static {
// Reference 的static 代码中,创建 ReferenceHandler 线程:
Thread handler = new ReferenceHandler();
}

}
  • Reference 其引用的对象被回收后,垃圾回收器将其加入到 Reference.pending 链表(所有的 Reference 共享一个链表)
  • Reference 类还包含一个 ReferenceHandler 线程(在 Reference 类的 static 代码中创建,同样是所有的 Reference 共享一个),此线程的从 pending-Reference 取出引用对象,将其加入它的 ReferenceQueue
  • 用户的代码里读取 ReferenceQueue,自行处理
/* High-priority thread to enqueue pending References
*/
private static class ReferenceHandler extends Thread {

private static void ensureClassInitialized(Class<?> clazz) {
try {
Class.forName(clazz.getName(), true, clazz.getClassLoader());
} catch (ClassNotFoundException e) {
throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
}
}

static {
// pre-load and initialize InterruptedException and Cleaner classes
// so that we don't get into trouble later in the run loop if there's
// memory shortage while loading/initializing them lazily.
ensureClassInitialized(InterruptedException.class);
ensureClassInitialized(Cleaner.class);
}

ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}

public void run() {
while (true) {
tryHandlePending(true);
}
}
}

/**
* Try handle pending {@link Reference} if there is one.<p>
* Return {@code true} as a hint that there might be another
* {@link Reference} pending or {@code false} when there are no more pending
* {@link Reference}s at the moment and the program can do some other
* useful work instead of looping.
*
* @param waitForNotify if {@code true} and there was no pending
* {@link Reference}, wait until notified from VM
* or interrupted; if {@code false}, return immediately
* when there is no pending {@link Reference}.
* @return {@code true} if there was a {@link Reference} pending and it
* was processed, or we waited for notification and either got it
* or thread was interrupted before being notified;
* {@code false} otherwise.
*/
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;
}

static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, "Reference Handler");
/* If there were a special system-only priority greater than
* MAX_PRIORITY, it would be used here
*/
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();

// provide access in SharedSecrets
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean tryHandlePendingReference() {
return tryHandlePending(false);
}
});
}

WeakHashMap

WeakHashMap 的特点:

  • WeakHashMap 会“被动的”清理放入 Map 的 Entry,适用于程序内缓存的场景;
  • WeakHashMap.Entry ,出了继承 Map.Entry<K,V>,还继承了 WeakReference ;

How to Use:

protected static void weakHashMapTest() {
Map<COREJBasicType, String> map = new WeakHashMap<>();

COREJBasicType key1 = new COREJBasicType();
COREJBasicType key2 = new COREJBasicType();

map.put(key1, new String("hello"));
map.put(key2, new String("world"));

System.gc();
for (Map.Entry<COREJBasicType, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + " " + entry.getValue());
}

key1 = null; // 帮助回收
System.gc();
for (Map.Entry<COREJBasicType, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + " " + entry.getValue());
}
}

第一次 gc 后,两个 Entry 都可以获取到,第二次 gc 后,遍历 WeakHashMap 只有 K2的 Entry 了;

WeakHashMap 自动回收的特性可以作为缓存来用, 例如 tomcat 的 ConcurrentCache。

WeakHashMap 源码解析

WeakHashMap 的主要属性:

public class WeakHashMap<K,V>
extends AbstractMap<K,V> implements Map<K,V> {

// 桶数组
Entry<K,V>[] table;

// 回收队列
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
}

put(k,val):过程与 HashMap 类似,但需要注意的是 WeakHashMap.Entry:

  • WeakHashMap 的桶数组定义是: Entry<K,V>[] table
  • WeakHashMap.Entry 继承自 WeakReference;
  • WeakHashMap.Entry 的成员只有 value,没有 key(也就意味着 Entry 对象不会对 key 有强引用);
  • put 方法传入的 key,被转换为了一个弱引用,同时该弱引用关联 WeakHashMap.queue :
Entry(Object key, V value,
ReferenceQueue<Object> queue,
int hash, Entry<K,V> next) {
super(key, queue); // super() 即 WeakReference(req,queue)
this.value = value;
this.hash = hash;
this.next = next;
}

当把一个 Entry put 后,再分析一下对象的可达性:

  • 如果 weakHashMap 是强可达的,那么 entry 对象也是强可达;
  • value 对象被 entry.value 引用,强可达;
  • key 对象只有一个弱引用关联(弱可达),所以会被 GC 掉,同时 GC 线程把 Key 关联的弱引用,放入 WeakHashMap 对象的 queue;

那么WeakHashMap 是如何回收 value 的?

get() put(), size() 方法里都会先调用一个 expungeStaleEntries() 方法,
此方法遍历 ReferenceQueue 取出每个弱引用(因为 Entry 继承自 WeakReference),把 Entry.val 设置为 null 帮助回收:

private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x; // (1)convert Key to Entry?

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;
else
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
e.value = null; //(2) Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}

代码(2)处把 Entry.value 设置为 null 帮助回收 @doubt: Entry<K,V> e = (Entry<K,V>) x ,x 是从 queue 里取出的,也即 Reference 类型,是如何转换为 Entry 类型的?


@ref: