虚引用真的不影响对象的生命周期吗?

Java的四大引用,大家都很熟悉吧:

  • 强应用:正常代码中的引用。一个对象能通过强应用访问到,那它就永远不会被回收
  • 软引用:比强引用弱一级的引用,内存不足时引用指向的对象会被回收
  • 弱引用:比软引用弱一级的引用,下一次GC时指向对象会被回收
  • 虚引用

最后一个虚应用是今天要讨论的。很多文章都是这么写的:

一个对象是否有虚引用存在,对其生存不会产生任何影响。

事实上,这个是错的。正确的表述是:

在Java 8以及之前的版本中,在虚引用回收后,虚引用指向的对象才会回收。在Java 9以及更新的版本中,虚引用不会对对象的生存产生任何影响。

一个示例

首先用Java 8,带上-Xmx10m -XX:+HeapDumpOnOutOfMemoryError参数运行如下代码:

Main.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public final class Main {

public static void main(String[] args) throws InterruptedException {
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
PhantomReference<byte[]> ref = new PhantomReference<>(new byte[1024 * 1024 * 5], queue);

System.out.println(queue.poll());
System.out.println("第一次gc");
System.gc();
Thread.sleep(300L);
System.out.println(queue.poll());
System.out.println("第二次gc");
System.gc();
byte[] bytes1 = new byte[1024 * 1024 * 6];
System.out.println("ending");
}
}

你猜猜结果是什么?

1
2
3
4
5
6
7
8
9
null
第一次gc
[email protected]
第二次gc
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid17738.hprof ...
Heap dump file created [6153765 bytes in 0.010 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at Main.main(Main.java:17)

也就是说,一个5M的数组,只被虚引用指向了,但是在OOM之前,它也不能被回收。

再看看heapdump:

从这张图可以看到,正是由于虚引用的存在,导致这个对象无法回收掉。

再去看看虚引用的文档,里面有这么一段:

An object that is reachable via phantom references will remain so until all such references are cleared or themselves become unreachable.

翻译过来就是:

被虚引用指向的对象会一直存在,直到这些引用被清除或者这些引用不可达。

也就是说,只要有虚引用指向这个对象,那这个对象就会一直存在。

Java 11下的表现

更加奇怪的是,在Java 11下,用同样的参数运行这个程序,结果如下:

1
2
3
4
5
null
第一次gc
[email protected]
第二次gc
ending

没有OOM了。

Java 9 引入的变更

翻了下变更记录,这个变化是在Java 9引入的

修改的代码更是寥寥几行:

1
2
3
4
5
6
7
8
9
10
11
--- a/src/share/vm/gc/shared/referenceProcessor.cpp	Thu Dec 24 07:35:18 2015 -0800
+++ b/src/share/vm/gc/shared/referenceProcessor.cpp Mon Dec 28 13:48:43 2015 -0500
@@ -243,7 +243,7 @@
// Phantom references
{
GCTraceTime(Debug, gc, ref) tt("PhantomReference", gc_timer);
- process_discovered_reflist(_discoveredPhantomRefs, NULL, false,
+ process_discovered_reflist(_discoveredPhantomRefs, NULL, true,
is_alive, keep_alive, complete_gc, task_executor);

// Process cleaners, but include them in phantom timing. We expect

从代码来看,就是在处理虚引用的时候,将第三个参数clear_referent从false变为了true。

为了理清楚这个逻辑,我们来看看process_discovered_reflist的代码:

process_discovered_reflist函数
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
size_t
ReferenceProcessor::process_discovered_reflist(
DiscoveredList refs_lists[],
ReferencePolicy* policy,
bool clear_referent,
BoolObjectClosure* is_alive,
OopClosure* keep_alive,
VoidClosure* complete_gc,
AbstractRefProcTaskExecutor* task_executor)
{
// 省略了无关逻辑

// 阶段三:
// 切断剩余引用指向的对象
if (mt_processing) {
RefProcPhase3Task phase3(*this, refs_lists, clear_referent, true /*marks_oops_alive*/);
task_executor->execute(phase3);
} else {
for (uint i = 0; i < _max_num_q; i++) {
// 我们关注这个逻辑
process_phase3(refs_lists[i], clear_referent,
is_alive, keep_alive, complete_gc);
}
}

return total_list_count;
}

接下来看看process_phase3的逻辑:

process_phase3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void
ReferenceProcessor::process_phase3(DiscoveredList& refs_list,
bool clear_referent,
BoolObjectClosure* is_alive,
OopClosure* keep_alive,
VoidClosure* complete_gc) {
ResourceMark rm;
DiscoveredListIterator iter(refs_list, keep_alive, is_alive);
while (iter.has_next()) {
iter.update_discovered();
iter.load_ptrs(DEBUG_ONLY(false /* allow_null_referent */));
// 这儿,如果clear_reference为true,就会清理指向的对象
// 否则,就会将指向的对象标记为alive
if (clear_referent) {
// NULL out referent pointer
iter.clear_referent();
} else {
// keep the referent around
iter.make_referent_alive();
}

可以看到,在Java 8之前的逻辑中,会调用make_referent_alive方法,导致虚引用指向的对象无法回收。

而在Java 9之后的逻辑中,会调用clear_referent,回收掉执行的对象。

于此同时,Java 9中,PhantomReference的文档说明也变了:

Phantom reference objects, which are enqueued after the collector determines that their referents may otherwise be reclaimed.

在确定指向的对象会被回收后,虚引用会被放到队列( ReferenceQueue)中。

为什么Java 8不回收虚引用的对象呢

PhantomReference是为了追踪对象GC、回收对象关联的资源的。在Java 8的实现中,确保对象在真正GC前能被对应的ReferenceQueue处理,所以将对象标记为活跃,不回收对象。

显然,在这种情况下,会导致本可以回收的对象无法回收的问题,所以在Java 9中,确保PhantomReference指向的对象在回收后(而不是原来的回收前),会被对应的ReferenceQueue处理,这样在一定程度上保证了功能,又修复了这个问题。

一些参考

虚引用真的不影响对象的生命周期吗?

https://www.robberphex.com/misunderstanding-about-phantom-reference/

作者

Robert Lu

发布于

2020-11-22

许可协议

评论