0%

深入理解强引用、软引用、弱引用、虚引用

Java 包含四种引用类型(引用强度从上到下依次逐渐减弱):

  • 强引用(StrongReference):只要对象存在强引用,就不会被GC。
  • 软引用(SoftReference):垃圾回收器会在内存不足时回收弱引用指向的对象。
  • 弱引用(WeakReference):垃圾回收器在GC时会回收此对象
  • 虚引用(PhantomReference):对象是否有虚引用的存在都不会对生存时间都构成影响,也无法通过虚引用来获取对一个对象的真实引用。唯一的用处:能在对象被GC时收到系统通知。

强引用

强引用是使用最普遍的引用。通过反射、new Object() 等方式创建的都是强引用。

强引用的对象只要引用还存在,GC就不会回收这个对象。即使在内存不足的情况下,JVM 宁愿抛出OutOfMemory 错误也不会回收这种对象。

软引用

软引用用来描述一些还有用,但非必需的对象。Java 提供了 java.lang.ref.SoftReference 类来实现软引用。

只有在内存不足,发生内存溢出异常之前 JVM 才会回收该对象。如果回收了软引用的对象还是没有足够的内存,才会抛出内存溢出异常。

软引用的特性比较适合用来实现缓存:比如网页缓存、图片缓存等,不过缺点是会造成大量的内存浪费

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被 JVM 回收,这个软引用就会被加入到与之关联的引用队列中。

弱引用

对于弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否够,都会回收该对象的占用内存。Java 提供了 java.lang.ref.WeakReference 类来实现软引用。

弱引用的也比较适合用来实现缓存,JDK 中的 WeakHashMapThreadLocal 也使用了弱引用。

弱引用也可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被 JVM 回收,这个弱引用就会被加入到与之关联的引用队列中。

虚引用

对象是否有虚引用的存在都不会对生存时间都构成影响,也无法通过虚引用来获取对一个对象的真实引用。唯一的用处:能在对象被GC时收到系统通知。Java 提供了 java.lang.ref.PhantomReference 类来实现软引用。

虚引用必须和引用队列(ReferenceQueue)关联使用。

ReferenceQueue使用示例

WeakReference 使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ReferenceQueueTest {

public static void main(String[] args) throws InterruptedException {
// 创建一个引用队列
ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
// 实现一个弱引用,将强引用类型hello和是实例化的rq放到弱引用实现里面
WeakReference<String> weakReference = new WeakReference<>(new String("hello"), referenceQueue);
// 通过弱引用get方法获取强引用中创建的内存空间hello值
System.out.println(weakReference.get());
// 通知JVM的gc进行垃圾回收
System.gc();
while (true) {
System.out.println("weakReference.get() :" + weakReference.get());
Reference<? extends String> reference = referenceQueue.poll();
if (reference == null) {
System.out.println("reference :" + null);
} else {
System.out.println("reference == weakReference:" + (reference == weakReference));
}
TimeUnit.SECONDS.sleep(1);
}
}
}

输出如下:

1
2
3
4
5
hello
weakReference.get() :null
reference == weakReference:true
weakReference.get() :null
reference :null

PhantomReference 使用示例

1
2
3
4
5
6
7
8
public class ReferenceQueueTest {
public static void main(String[] args) throws InterruptedException {
// 创建一个引用队列
ReferenceQueue<String> rq = new ReferenceQueue<>();
PhantomReference<String> pr = new PhantomReference<>(new String("hello"), rq);
System.out.println(pr.get());
}
}

输出如下:

1
null

引用类型和可达性

对象的可达性与引用类型密切相关。Java 有 5 中类型的可达性:

  • 强可达(Strongly Reachable):如果线程能通过强引用访问到对象,那么这个对象就是强可达的。
  • 软可达(Soft Reachable):如果一个对象不是强可达的,但是可以通过软引用访问到,那么这个对象就是软可达的
  • 弱可达(Weak Reachable):如果一个对象不是强可达或者软可达的,但是可以通过弱引用访问到,那么这个对象就是弱可达的。
  • 虚可达(Phantom Reachable):如果一个对象不是强可达,软可达或者弱可达,并且这个对象已经finalize过了,并且有虚引用指向该对象,那么这个对象就是虚可达的。
  • 不可达(Unreachable):如果对象不能通过上述的几种方式访问到,则对象是不可达的,可以被回收。

Reference

Reference及其子类有两大功能:

  1. 实现特定的引用类型

    JVM 垃圾回收器硬编码识别 SoftReferenceWeakReferencePhantomReference 等这些具体的类,对其reference变量进行特殊处理,才有了不同的引用类型的效果。

  2. 用户可以对象被回收后得到通知

    将被回收的对象的 Reference 添加到一个队列中,用户后续自己去从队列中获取并使用。这里没有采用回调的机制,可能是担心回调执行阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public abstract class Reference<T> {
// 引用的对象
private T referent; /* Treated specially by GC */
// 自定义的引用队列,如果引用的对象被回收,这可以从该队列中获取相应的引用
volatile ReferenceQueue<? super T> queue;
// 如果处于Active状态:next = null
// 如果处于Pending状态:next = this
// 如果处于Enqueued状态:next = ReferenceQueue中的下一个元素
// 如果处于Inactive转改:next = this
volatile Reference next;
// pending 引用链表的指针,指向下一个节点
transient private Reference<T> discovered; /* used by VM */
// 用于控制垃圾收集器操作是否与Pending状态的Reference enqueue操作不冲突的全局锁
static private class Lock { }
private static Lock lock = new Lock();
// pending 引用链表的头结点
private static Reference<Object> pending = null;

Reference(T referent) {
this(referent, null);
}

Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
// ...
}

引用状态

Reference 对象是有状态的。一共有4中状态:

  • Active:新创建的 Reference 的状态。垃圾回收器会根据其可达性状态将它切换为 Pending 或 Inactive 状态。
  • Pending:在 pending 引用链表中的 Reference 的状态,这些 Reference 等待被加入 ReferenceQueue 中。
  • Enqueued:在 ReferenceQueue 队列中的 Reference 的状态,如果 Reference从队列中移除,会进入Inactive 状态。
  • Inactive:Reference的最终状态。

状态转化

pending 引用链表

Reference 引用的对象被回收后,Reference 实例会被添加到 ReferenceQueue 中,这不是垃圾回收器来完成的,如果垃圾回收器还需要执行这个操作,会降低其效率。从另外一方面想,Reference 实例会被添加到ReferenceQueue中的实效性要求不高,所以也没必要在回收时立马加入 ReferenceQueue。

所以垃圾回收器做的是一个更轻量级的操作:把 Reference 添加到 pending 引用链表中。Reference 对象中有一个 pending 成员变量,它就是这个pending 引用链表的头结点,还有一个 discovered 成员变量,它指向下一个节点。

ReferenceHandler线程

回收 Reference 引用的对象后,垃圾回收器只是将 Reference 添加到 pending 引用链表中。加入 ReferenceQueue 无法在垃圾回收线程中执行,而是由 ReferenceHandler 线程负责。

Reference 中定义了 ReferenceHandler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
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() {
// 一直将pending 引用链表中的Reference放入ReferenceQueue中
while (true) {
tryHandlePending(true);
}
}
}
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...
// 判断引用是否为 Cleaner
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(); // 如果Reference 为Cleaner,则调用其clean方法
return true;
}

ReferenceQueue<? super Object> q = r.queue;
// 将 Rference 放入 RerenceQueue
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);
}
});
}

ReferenceQueue

如果 Reference 所引用的对象被 JVM 回收,这个 Reference 就会被加入到与之关联的 ReferenceQueue 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class ReferenceQueue<T> {
// 头结点,说明ReferenceQueue基于链表
private volatile Reference<? extends T> head = null;
// 用于保证线程安全
static private class Lock { };
private Lock lock = new Lock();
// 链表长度
private long queueLength = 0;

// 只能被Reference类调用
boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
synchronized (lock) { // 加锁保证线程安全
// 判断是否需要加入ReferenceQueue
ReferenceQueue<?> queue = r.queue;
if ((queue == NULL) || (queue == ENQUEUED)) {
return false;
}
assert queue == this;
r.queue = ENQUEUED;
r.next = (head == null) ? r : head; // 这里的next可以看下上面的注释
head = r; // 头插法
queueLength++;
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(1);
}
lock.notifyAll();
return true;
}
}
}

总结

一个使用 Reference + ReferenceQueue 的完整流程如下:

Cleaner

Cleaner 继承自 PhantomReference

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public class Cleaner extends PhantomReference<Object> {
// 这个属性用不到,ReferenceHandler 中也不会将元素放入该队列中
// 因为 PhantomReference 的构造方法必须要传ReferenceQueue
private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();
// 链表的表头
private static Cleaner first = null;
// 双向链表
private Cleaner next = null;
private Cleaner prev = null;
// 用户自定义的需要执行的操作
private final Runnable thunk;
// 采用头插法插入双向链表中
private static synchronized Cleaner add(Cleaner var0) {
if (first != null) {
var0.next = first;
first.prev = var0;
}

first = var0;
return var0;
}
// 从双向链表中移除元素
private static synchronized boolean remove(Cleaner var0) {
if (var0.next == var0) {
return false;
} else {
if (first == var0) {
if (var0.next != null) {
first = var0.next;
} else {
first = var0.prev;
}
}

if (var0.next != null) {
var0.next.prev = var0.prev;
}

if (var0.prev != null) {
var0.prev.next = var0.next;
}

var0.next = var0;
var0.prev = var0;
return true;
}
}

private Cleaner(Object var1, Runnable var2) {
super(var1, dummyQueue);
this.thunk = var2;
}

public static Cleaner create(Object var0, Runnable var1) {
return var1 == null ? null : add(new Cleaner(var0, var1));
}
// 这个方法会由ReferenceHandler线程调用
public void clean() {
if (remove(this)) {
try {
this.thunk.run();
} catch (final Throwable var2) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.err != null) {
(new Error("Cleaner terminated abnormally", var2)).printStackTrace();
}

System.exit(1);
return null;
}
});
}

}
}
}

DirectByteBuffer 内存回收策略

DirectByteBuffer 初始化时通过 Unsafe 的本地方法 allocateMemory() 进行内存分配,底层调用的是操作系统的 malloc() 函数,除此之外,初始化时还会创建一个 Deallocator 线程,并通过 Cleaner 机制来调用 freeMemory() 方法来对直接内存进行回收操作,freeMemory() 底层调用的是操作系统的 free() 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
DirectByteBuffer(int cap) {                   // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}

private static class Deallocator implements Runnable {
// ....
public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}
  1. DirectByteBuffer 创建一个Cleaner,并把代表清理动作的Deallocator类绑定。
  2. DirectBuffer 如果没有强引用,JVM 就会将 Cleaner 放到 pending 引用链表里。
  3. ReferenceHandler线程会循环获取 pending 引用链表中的元素,如果是 Cleaner,则会执行其 clean 方法,进而执行 Deallocator 中的run方法调用 freeMemory() 释放内存。

FinalReference

FinalReference 继承自 Reference

1
2
3
4
5
6
7
8
9
/**
* Final references, used to implement finalization
*/
class FinalReference<T> extends Reference<T> {

public FinalReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}

FinalReference 类仅仅是继承了 Reference 类而已,真正的逻辑位于 FinalReference 的唯一子类:java.lang.ref.Finalizer 中。

finalize

finalize() 是 Object 中的方法,当垃圾回收器将要回收对象所占内存之前被调用,即当一个对象被虚拟机宣告死亡时会先调用它 finalize() 方法,让此对象处理它生前的最后事情(这个对象可以趁这个时机挣脱死亡的命运)。

finalize() 只会在对象内存回收前被调用一次,但 finalize() 的调用具有不确定性,只保证方法会调用,但不保证方法里的任务会被执行完。

Finalizer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
final class Finalizer extends FinalReference<Object> {
// 引用队列
private static ReferenceQueue<Object> 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 */
// register方法仅会被虚拟机所调用,并且只有重写了java.lang.Object的finalize方法的类
// 才会被作为参数调用Finalizer#register方法
static void register(Object finalizee) {
new Finalizer(finalizee);
}
// 头插法插入元素
private void add() {
synchronized (lock) {
if (unfinalized != null) {
this.next = unfinalized;
unfinalized.prev = this;
}
unfinalized = this;
}
}
// ...
}

FinalizerThread

Finalizer 中定义了 FinalizerThread:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
private static class FinalizerThread extends Thread {
private volatile boolean running;
FinalizerThread(ThreadGroup g) {
super(g, "Finalizer");
}
public void run() {
// in case of recursive call to run()
if (running)
return;

// Finalizer thread starts before System.initializeSystemClass
// is called. Wait until JavaLangAccess is available
while (!VM.isBooted()) {
// delay until VM completes initialization
try {
VM.awaitBooted();
} catch (InterruptedException x) {
// ignore and continue
}
}
final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
running = true;
// 循环取出队首元素并执行runFinalizer方法
for (;;) {
try {
Finalizer f = (Finalizer)queue.remove();
f.runFinalizer(jla);
} catch (InterruptedException x) {
// ignore and continue
}
}
}
}
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread finalizer = new FinalizerThread(tg);
finalizer.setPriority(Thread.MAX_PRIORITY - 2); // 优先级不是最高
finalizer.setDaemon(true); // 守护线程
finalizer.start(); // 启动线程
}

private void runFinalizer(JavaLangAccess jla) {
synchronized (this) {
// hasBeenFinalized的作用就是保证finalize方法只会被调用一次
if (hasBeenFinalized()) return;
remove();
}
try {
// 注意这里和其他 Reference 不同
// 其他 Reference 在进入 pending 前 referent 属性为空(因为GC)
Object finalizee = this.get();
if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
jla.invokeFinalize(finalizee);

/* Clear stack slot containing this variable, to decrease
the chances of false retention with a conservative GC */
finalizee = null;
}
} catch (Throwable x) { }
super.clear();
}

public void invokeFinalize(Object var1) throws Throwable {
var1.finalize(); // 调用finalize()方法
}

这里和之前有些不同,Finalizer的 referent 属性在加入 pending 引用链表前不会被GC。

坚持原创技术分享,您的支持将鼓励我继续创作!