Java/Kotlin中如何监控实例被回收

cover

我们知道,Java程序的运行时内存由JVM管理,负责回收、释放内存的程序是GC(Garbage Collection)。开发者无法手动释放某个特定实例并回收内存,只能释放引用并等待GC回收;或者手动触发GC:System.gc()

假设有如下代码:

1
2
3
4
5
6
data class User(val name: String)

var user = User("User A")
user = User("User B") // 重新赋值

System.gc() // 手动触发GC

当user变量被重新赋值时,原来的 A 实例将失去强引用,在下一次GC执行时,占用的内存区域将被回收。我们也可以手动触发GC来回收A的内存,如上述代码所示。

使用弱引用检测实例被回收

如果我们希望在后续代码中检测 A 实例是否被回收,可以使用弱引用(Weak Reference)。弱引用是JVM的四种引用类型之一,这四种分别是:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference),引用强度依次变弱。

  • 强引用:只要存在强引用,对象将不会被回收。
  • 软引用:如果只有软引用,在内存不足时可能被回收。
  • 弱引用:弱引用不会影响对象的回收,如果没有更强的引用,对象将会被回收。
  • 虚引用:虚引用不会影响对象的回收,甚至无法通过虚引用获取对象实例。

所以,使用弱引用检测实例是否被回收,可以使用弱引用。如下面代码所示:

1
2
3
4
5
6
7
8
var user = User("User A")
val userRef = WeakReference(user)

user = User("User B")
println(userRef.get()) // User A

System.gc()
println(userRef.get()) // null

使用虚引用监控实例被回收

上述代码可以检测被弱引用引用的对象是否被GC回收,如果我们想在对象被回收时收到通知,就需要另一个工具:引用队列(Reference Queue)。在创建软引用、弱引用和虚引用时,我们都可以在构造方法中传入一个引用队列实例。当引用的对象将要被回收时,该引用会被添加到队列中,我们通过监听该队列是否为空,就可以实现检测对象是否将要被回收。

虽然这三种引用都可以实现相同的效果(三种引用都继承自 Reference ),但是软引用会影响GC逻辑,不应该使用。弱引用对比虚引用有获取引用实例的能力,而虚引用是专门为检测对象回收而设计的,所以推荐使用虚引用。(从实际效果来看,在这个场景中,弱引用和虚引用并没有明显区别)

参照这个原理,我们可以实现一个监听器来监控实例的回收,在实例即将被回收时收到回调。测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
val monitor = ReferenceRecyclingMonitor()
monitor.start()

var user = User("User A")
monitor.track(user) {
println("User A is going to be recycled.")
}

user = User("User B") // 重新赋值

System.gc() // 手动触发GC,命令行将输出:User A is going to be recycled.

monitor.stop()

一种 ReferenceRecyclingMonitor 的实现方式如下:

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
class ReferenceRecyclingMonitor : Runnable {

private var thread: Thread? = null
private val threadLock = ReentrantLock()
private val referenceQueue = ReferenceQueue<Any>()
private val references = mutableMapOf<PhantomReference<*>, () -> Unit>()

companion object {
private val MUTE_LISTENER = {}
}

fun track(any: Any, listener: () -> Unit = MUTE_LISTENER) {
references[PhantomReference(any, referenceQueue)] = listener
}

fun start() {
try {
threadLock.lock()
if (thread != null) {
return
}
thread = Thread(this).also {
it.start()
}
} finally {
threadLock.unlock()
}
}

fun stop() {
try {
threadLock.lock()
thread?.interrupt()
thread = null
} finally {
threadLock.unlock()
}
}

override fun run() {
while (!Thread.currentThread().isInterrupted) {
try {
val ref = referenceQueue.remove()
references[ref]?.invoke()
references.remove(ref)
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
} catch (e: Exception) {
// ignore other exception
}
}
}
}