Java的四大引用,大家都很熟悉吧:
- 强应用:正常代码中的引用。一个对象能通过强应用访问到,那它就永远不会被回收
- 软引用:比强引用弱一级的引用,内存不足时引用指向的对象会被回收
- 弱引用:比软引用弱一级的引用,下一次GC时指向对象会被回收
- 虚引用
最后一个虚应用是今天要讨论的。很多文章都是这么写的:
一个对象是否有虚引用存在,对其生存不会产生任何影响。
事实上,这个是错的。正确的表述是:
在Java 8以及之前的版本中,在虚引用回收后,虚引用指向的对象才会回收。在Java 9以及更新的版本中,虚引用不会对对象的生存产生任何影响。
一个示例
首先用Java 8,带上-Xmx10m -XX:+HeapDumpOnOutOfMemoryError
参数运行如下代码:
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");
}
}
也就是说,一个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下,用同样的参数运行这个程序,结果如下:
null
第一次gc
java.lang.ref.PhantomReference@5e91993f
第二次gc
ending
没有OOM了。
Java 9 引入的变更
翻了下变更记录,这个变化是在Java 9引入的:
修改的代码更是寥寥几行:
--- 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
的代码:
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
的逻辑:
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处理,这样在一定程度上保证了功能,又修复了这个问题。