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 java.lang.ref.PhantomReference@6d06d69c 第二次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 java.lang.ref.PhantomReference@5e91993f 第二次gc ending
没有OOM了。
Java 9 引入的变更 翻了下变更记录,这个变化是在Java 9引入的 :
修改的代码更是寥寥几行:
1 2 3 4 5 6 7 8 9 10 11 @@ -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 ) ; 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 )); if (clear_referent) { iter.clear_referent (); } else { 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处理,这样在一定程度上保证了功能,又修复了这个问题。
一些参考