ART虚拟机 | Finalize的替代者Cleaner

7,568 阅读11分钟

源码分析基于Android 11(R)

前言

C++中的对象释放由程序员负责,而Java中的对象释放则由GC负责。如果一个Java对象通过指针持有native对象,那么应该何时释放native对象呢?靠原有的GC自然搞不定,因为虚拟机无法得知这个Java对象的long型字段是不是指针,以及该指向哪个native对象。

早先的做法是在Java类中实现finalize方法,该方法会在Java对象回收的时候得到调用。这样我们便可以在finalize方法中去释放native对象,让Java资源和native资源在GC过程中同时释放。不过finalize方法有诸多缺陷,最终在JDK 9中被弃用。替代它的是Cleaner类。

目录

1. 需要解决的问题

如何在Java对象被回收的时候,自动释放其所关联的native对象和资源?

这是finalize和Cleaner想要解决的问题。纯Java层面的应用开发通常不会涉及到Java对象持有native对象指针的设计,但对一些复杂的类而言,这种设计不可或缺。譬如大家经常用到的Bitmap,就是通过这种方式将大部分内存消耗放到native堆而不是Java堆。

1.1 Finalize的缺点

Finalize用起来很方便。覆写一个方法,在方法里面释放资源,两步就可以搞定native资源的释放,所以也被广大开发者所喜爱。可是方便有时需要付出代价。性能的牺牲是一方面,在某些场景下导致的内存错误则更加无法忍受。Android Runtime团队的大佬Hans Boehm在Google IO 2017曾就这个问题专门做过演说,里面提到的finalize的3个缺点,感兴趣的可以去油管上查看:链接,我在这里简单总结下。

  1. 如果两个对象同时变成unreachable,他们的finalize方法执行顺序是任意的。因此在一个对象的finalize方法中使用另一个对象持有的native指针,将有可能访问一个已经释放的C++对象,从而导致native heap corruption。
  2. 根据Java语法规则,一个对象的finalize方法是可以在它的其他方法还在执行时被调用的。因此其他方法如果正在访问它所持有的native指针,将有可能发生use-after-free的问题。
  3. 如果Java对象很小,而持有的native对象很大,则需要显示调用System.gc()以提早触发GC。否则单纯依靠Java堆的增长来达到触发水位,可能要猴年马月了,而此时垃圾的native对象将堆积成山。

提到Hans Boehm,我有个感触想跟大家分享下。这位前辈74年上的本科(估算65岁左右),康奈尔博士毕业,然而至今仍然奋战在项目一线,ART中很多关键代码都是他提交的。我曾经邮件向他请教过问题,他为人十分和善,对于像我这种菜鸡提的问题也回答得十分详细。按照国内35岁辞退的浮躁心态来看,他这么大年纪没混成个领导,还在一线写代码,真是失败。但看到他的个人简介,你还能说出这样的话么?

I am an ACM Fellow, and a past Chair of ACM SIGPLAN (2001-2003). Until late 2017 I chaired the ISO C++ Concurrency Study Group (WG21/SG1), where I continue to actively participate.

在技术领域,很多卓越的贡献是需要时间来沉淀的。当然对于业务而言,技术的深度并不会在早期获利,因此时常被人忽略。但我相信随着国力的提升,那些沉下心来深耕的人总会得到回报。因为业务的红利是有技术创新这个上限的。技术创新需要务实,而浮躁的土壤只能滋生出概念和骗局。

扯得有点远,说完了finalize的缺点,下面介绍Cleaner的优点。

1.2 Cleaner的优点

33 /**
34  * General-purpose phantom-reference-based cleaners.
35  *
36  * <p> Cleaners are a lightweight and more robust alternative to finalization.
37  * They are lightweight because they are not created by the VM and thus do not
38  * require a JNI upcall to be created, and because their cleanup code is
39  * invoked directly by the reference-handler thread rather than by the
40  * finalizer thread.  They are more robust because they use phantom references,
41  * the weakest type of reference object, thereby avoiding the nasty ordering
42  * problems inherent to finalization.
43  *
44  * <p> A cleaner tracks a referent object and encapsulates a thunk of arbitrary
45  * cleanup code.  Some time after the GC detects that a cleaner's referent has
46  * become phantom-reachable, the reference-handler thread will run the cleaner.
47  * Cleaners may also be invoked directly; they are thread safe and ensure that
48  * they run their thunks at most once.
49  *
50  * <p> Cleaners are not a replacement for finalization.  They should be used
51  * only when the cleanup code is extremely simple and straightforward.
52  * Nontrivial cleaners are inadvisable since they risk blocking the
53  * reference-handler thread and delaying further cleanup and finalization.
54  *
55  *
56  * @author Mark Reinhold
57  */

根据源码中的注释可以知道,Cleaner是一种finalization的方式,它可以跟踪某个对象的生命周期,并且封装任意的cleanup代码。在GC释放完该对象后,reference-handler thread会运行封装的cleanup代码来完成资源释放。

由于Cleaner继承于PhantomReference(虚拟引用),相比于finalize的方式,它限定了很多能力,譬如访问跟踪对象的能力。由于这些能力的限定,所以它同时也避免了finalize的诸多缺陷。说白了,finalize的很多缺陷都是由于它太“能干”了。

  1. 如果Java对象很小,而持有的native对象很大,则需要显示调用System.gc()以提早触发GC。否则单纯依靠Java堆的增长来达到触发水位,可能要猴年马月了,而此时native对象产生的垃圾将堆积成山。

上文提到过的finalize的缺点3,在Cleaner这里依然得不到解决。主动触发GC是有缺陷的,因为开发者不知道怎么把控这个频率。频繁的话就会降低运行的性能,稀少的话就会导致native资源无法及时释放。因此,Android从N开始引入NativeAllocationRegistry类,一方面是简化Cleaner的使用方式,另一方面是将native资源的大小计入GC触发的策略之中,这样一来,原本需要用户主动触发的GC便可以自动了。这个话题后面会专门成文介绍,在此先按下不表。

2. 设计原理

2.1 Referent对象何时回收

Referent对象,俗称被引用对象,也即Cleaner需要追踪的对象。Cleaner类继承于PhantomReference类,原因在于它需要利用虚拟引用的特性:在跟踪对象回收时自己加入到ReferenceQueue中,继而可以自动完成native资源的回收。下图展示了一个PhantomReference对象加入到ReferenceQueue中的过程。

Referent对象在被强引用时,处于reachable状态,在GC阶段通过GC Root可以标记到这个对象,因此不会被回收。只有当没有任何强引用指向它时,它才会被允许回收。但允许回收和发生回收是两回事,这也导致Java中的弱引用类型被实现为3种。

  1. SoftReference,软引用,它具有两个特性。一是可以通过get获取到referent对象,二是referent在仅被它引用时,可以一直存活,直到堆内存真的被耗尽以至于马上要发生OOM时,referent才会被回收。常用于实现Cache机制。
  2. WeakReference,弱引用。当referent仅被WeakReference引用时,该referent对象在下次GC时会被回收。所以和SoftReference相比,二者的区别仅在于referent被回收的时机。它常用于需要用到referent,但又不希望自己的引用影响referent回收的场景。
  3. PhantomReference,虚引用。和SoftReference和WeakReference相比,它无法通过get获取到referent对象。这也就限定了它的使用场景不是为了操作referent,而只是在referent回收时可以触发一些事件。

2.2 PhantomReference对象如何入列和处理

PhantomReference对象的入列过程其实涉及到多个线程。而且Cleaner作为一种特殊的PhantomReference,它自己又有一套独立的入列规则。以下分开介绍。

2.2.1 Cleaner对象的入列和处理过程

Cleaner在ReferenceQueueDaemon线程的处理过程中被当作一种特殊对象,因此无需开发者新建线程来轮询ReferenceQueue。但是需要注意,所有的Cleaner都会放在ReferenceQueueDaemon线程进行处理,因此要保证Cleaner.clean方法中做的事情是快速的,防止阻塞其他Cleaner的清理动作。

2.2.2 PhantomReference对象的入列和处理过程

普通PhantomReference对象最后会加入构造时传入的ReferenceQueue中。对于这些ReferenceQueue有两种处理方式,一种是调用ReferenceQueue.poll方法进行非阻塞的轮询,另一种是通过调用ReferenceQueue.remove方法进行阻塞等待。通常而言,ReferenceQueue的处理需要开发者新开线程,因此如果同时处理的ReferenceQueue过多,则也会造成线程资源的浪费。

3. 源码分析

本文分析基于Android 11(R)版本的源码,侧重于阐释ART虚拟机对PhantomReference对象的特殊处理,其中会涉及到GC的部分知识。

3.1 GC运行和PhantomReference的关系

对于Concurrent Copying Collector而言,其GC可以粗略上分为Mark和Copy两个阶段。Mark结束后,所有被标记过的对象放到Mark Stack中,用于后续处理。

3.1.1 Mark阶段

art/runtime/gc/collector/concurrent_copying.cc

2205 inline void ConcurrentCopying::ProcessMarkStackRef(mirror::Object* to_ref) {
...
2292   if (perform_scan) {
2293     if (use_generational_cc_ && young_gen_) {
2294       Scan<true>(to_ref);
2295     } else {
2296       Scan<false>(to_ref);
2297     }
2298   }

Mark结束后,Collector会遍历Mark Stack中所有的对象,对每个对象都执行Scan的动作。Scan中最终会对每个Reference对象执行DelayReferenceReferent的动作,如果Reference指向的referent未被标记,则将改Reference对象加入相应的native队列中。

art/runtime/gc/reference_processor.cc

232 // Process the "referent" field in a java.lang.ref.Reference.  If the referent has not yet been
233 // marked, put it on the appropriate list in the heap for later processing.
234 void ReferenceProcessor::DelayReferenceReferent(ObjPtr<mirror::Class> klass,
...
243   if (!collector->IsNullOrMarkedHeapReference(referent, /*do_atomic_update=*/true)) {  <==== 如果referent未被标记,则表明其将被回收
...
257     if (klass->IsSoftReferenceClass()) {
258       soft_reference_queue_.AtomicEnqueueIfNotEnqueued(self, ref);
259     } else if (klass->IsWeakReferenceClass()) {
260       weak_reference_queue_.AtomicEnqueueIfNotEnqueued(self, ref);
261     } else if (klass->IsFinalizerReferenceClass()) {
262       finalizer_reference_queue_.AtomicEnqueueIfNotEnqueued(self, ref);
263     } else if (klass->IsPhantomReferenceClass()) {   <============== 如果当前reference为PhantomReference,则将其加入到native的phantom_reference_queue_中
264       phantom_reference_queue_.AtomicEnqueueIfNotEnqueued(self, ref);
265     } else {
266       LOG(FATAL) << "Invalid reference type " << klass->PrettyClass() << " " << std::hex
267                  << klass->GetAccessFlags();
268     }
269   }
270 }

PhantomReference加入到phantom_reference_queue_后,接着会怎么处理呢?

3.1.2 Copy阶段

art/runtime/gc/collector/concurrent_copying.cc

1434 void ConcurrentCopying::CopyingPhase() {
...
1645     ProcessReferences(self);

在GC的Copy阶段,collector会执行ProcessReferences函数。

art/runtime/gc/reference_processor.cc

153 void ReferenceProcessor::ProcessReferences(bool concurrent,
...
211   // Clear all phantom references with white referents.
212   phantom_reference_queue_.ClearWhiteReferences(&cleared_references_, collector);

ProcesssReferences函数中会将phantom_reference_queue_中的Reference添加到cleared_references_中。phantom_reference_queue_中只包含PhantomReference,而cleared_reference_则还包含有SoftReference和WeakReference。

3.1.3 后GC阶段

在GC执行完之后,会调用CollectClearedReferences生成处理cleared_references_的任务,紧接着通过Run来执行它。

art/runtime/gc/heap.cc

2671   collector->Run(gc_cause, clear_soft_references || runtime->IsZygote());   <======  真正执行GC的地方
2672   IncrementFreedEver();
2673   RequestTrim(self);
2674   // Collect cleared references.
2675   SelfDeletingTask* clear = reference_processor_->CollectClearedReferences(self);  <====== 生成处理cleared_references_的任务
2676   // Grow the heap so that we know when to perform the next GC.
2677   GrowForUtilization(collector, bytes_allocated_before_gc);
2678   LogGC(gc_cause, collector);
2679   FinishGC(self, gc_type);    <============================================== 这一轮GC结束
2680   // Actually enqueue all cleared references. Do this after the GC has officially finished since
2681   // otherwise we can deadlock.
2682   clear->Run(self);       <================================================== 指向刚刚生成的处理cleared_references_的任务

art/runtime/gc/reference_processor.cc

281   void Run(Thread* thread) override {
282     ScopedObjectAccess soa(thread);
283     jvalue args[1];
284     args[0].l = cleared_references_;
285     InvokeWithJValues(soa, nullptr, WellKnownClasses::java_lang_ref_ReferenceQueue_add, args);   <===== 调用Java方法
286     soa.Env()->DeleteGlobalRef(cleared_references_);
287   }

Run里面将cleared_references_作为参数,调用java.lang.ref.ReferenceQueue.add方法。这样一来,我们便从native世界回到了Java世界。

libcore/ojluni/src/main/java/java/lang/ref/ReferenceQueue.java

261     static void add(Reference<?> list) {
262         synchronized (ReferenceQueue.class) {
263             if (unenqueued == null) {
264                 unenqueued = list;
265             } else {
266                 // Find the last element in unenqueued.
267                 Reference<?> last = unenqueued;
268                 while (last.pendingNext != unenqueued) {
269                   last = last.pendingNext;
270                 }
271                 // Add our list to the end. Update the pendingNext to point back to enqueued.
272                 last.pendingNext = list;
273                 last = list;
274                 while (last.pendingNext != list) {
275                     last = last.pendingNext;
276                 }
277                 last.pendingNext = unenqueued;
278             }
279             ReferenceQueue.class.notifyAll();      //当cleared_references_中所有元素都添加进Java的全局ReferenceQueue中后,调用notifyAll唤醒ReferenceQueueDaemon线程
280         }
281     }

3.2 中转站ReferenceQueueDaemon线程

在没有任务到来时,ReferenceQueueDaemon线程处于挂起状态。

libcore/libart/src/main/java/java/lang/Daemons.java

211         @Override public void runInternal() {
212             while (isRunning()) {
213                 Reference<?> list;
214                 try {
215                     synchronized (ReferenceQueue.class) {
216                         while (ReferenceQueue.unenqueued == null) {
217                             ReferenceQueue.class.wait();    <========== 通过调用wait将本线程挂起
218                         }
219                         list = ReferenceQueue.unenqueued;
220                         ReferenceQueue.unenqueued = null;
221                     }
222                 } catch (InterruptedException e) {
223                     continue;
224                 } catch (OutOfMemoryError e) {
225                     continue;
226                 }
227                 ReferenceQueue.enqueuePending(list);
228             }
229         }

当新的任务到来时,ReferenceQueueDaemon线程从ReferenceQueue.class.wait中醒来。对于全局ReferenceQueue中的元素,Cleaner和其他的PhantomReference处理方式不同,下面将分别介绍。

3.2.1 Cleaner对象如何处理

全局的ReferenceQueue通过调用enqueuePending将内部的元素分发出去。每个Reference对象在构造时都传入了一个ReferenceQueue作为参数,这个参数就是分发后Reference对象最终所在的队列。

libcore/ojluni/src/main/java/java/lang/ref/ReferenceQueue.java

219     public static void enqueuePending(Reference<?> list) {
220         Reference<?> start = list;
221         do {
222             ReferenceQueue queue = list.queue;     <========== 取出每个Reference对象构造时传入的ReferenceQueue对象
223             if (queue == null) {
224                 Reference<?> next = list.pendingNext;
225 
226                 // Make pendingNext a self-loop to preserve the invariant that
227                 // once enqueued, pendingNext is non-null -- without leaking
228                 // the object pendingNext was previously pointing to.
229                 list.pendingNext = list;
230                 list = next;
231             } else {
232                 // To improve performance, we try to avoid repeated
233                 // synchronization on the same queue by batching enqueue of
234                 // consecutive references in the list that have the same
235                 // queue.
236                 synchronized (queue.lock) {
237                     do {
238                         Reference<?> next = list.pendingNext;
239 
240                         // Make pendingNext a self-loop to preserve the
241                         // invariant that once enqueued, pendingNext is
242                         // non-null -- without leaking the object pendingNext
243                         // was previously pointing to.
244                         list.pendingNext = list;
245                         queue.enqueueLocked(list);   <========= 将Reference对象从全局的ReferenceQueue中取出,加入到对象所属的ReferenceQueue中
246                         list = next;
247                     } while (list != start && list.queue == queue);
248                     queue.lock.notifyAll();
249                 }
250             }
251         } while (list != start);
252     }

对于Cleaner对象而言,它并没有真正地加入到构造时传入的ReferenceQueue中,而是直接在enqueueLocked中得到了处理。

libcore/ojluni/src/main/java/java/lang/ref/ReferenceQueue.java

66     private boolean enqueueLocked(Reference<? extends T> r) {
67         // Verify the reference has not already been enqueued.
68         if (r.queueNext != null) {
69             return false;
70         }
71 
72         if (r instanceof Cleaner) {
73             // If this reference is a Cleaner, then simply invoke the clean method instead
74             // of enqueueing it in the queue. Cleaners are associated with dummy queues that
75             // are never polled and objects are never enqueued on them.
76             Cleaner cl = (sun.misc.Cleaner) r;
77             cl.clean();       <============= 通过调用cl.clean()完成native资源的释放
78 
79             // Update queueNext to indicate that the reference has been
80             // enqueued, but is now removed from the queue.
81             r.queueNext = sQueueNextUnenqueued;
82             return true;
83         }
84 
85         if (tail == null) {
86             head = r;
87         } else {
88             tail.queueNext = r;
89         }
90         tail = r;
91         tail.queueNext = r;
92         return true;
93     }

3.2.2 其他PhantomReference对象如何处理

通过上面代码的85~92行可以知道,其他PhantomReference最终会加入对应的ReferenceQueue中,使其形成链表结构。添加完后,通过调用queue.lock.notifyAll来唤醒相应的处理线程。

libcore/ojluni/src/main/java/java/lang/ref/ReferenceQueue.java

219     public static void enqueuePending(Reference<?> list) {
236                 synchronized (queue.lock) {
237                     do {
...
245                         queue.enqueueLocked(list);   <========= 将Reference对象从全局的ReferenceQueue中取出,加入到对象所属的ReferenceQueue中
246                         list = next;
247                     } while (list != start && list.queue == queue);
248                     queue.lock.notifyAll();
...
252     }

[Cleaner和其他PhantomReference对比]

类型Cleaner其他PhantomReference
是否加入到构造时传入的ReferenceQueue中✔️
最后的处理放在ReferenceQueueDaemon中✔️
最后的处理放在自定义的线程中✔️

4. 实际案例

NativeAllocationRegistry内部就是利用Cleaner来主动回收native资源的。它传入两个参数给Cleaner.create,一个是需要追踪的Java对象,另一个是CleanThunk,用来指定回收的方法。

libcore/luni/src/main/java/libcore/util/NativeAllocationRegistry.java

243         try {
244             thunk = new CleanerThunk();
245             Cleaner cleaner = Cleaner.create(referent, thunk);
...
253         thunk.setNativePtr(nativePtr);

Cleaner继承于PhantomReference,其构造方法有两种。通过115行可以得知,其最终传入的ReferenceQueue为dummyQueue,dummy的意思为假的、虚拟的,表明这个dummyQueue不会有实际的作用。这个和我们上面3.2.1的分析是一致的。

libcore/ojluni/src/main/java/sun/misc/Cleaner.java

114     private Cleaner(Object referent, Runnable thunk) {
115         super(referent, dummyQueue);    <===== PhantomReference的构造方法需要传入ReferenceQueue参数
116         this.thunk = thunk;
117     }

CleanerThunk内部的nativePtr用于记录native对象的指针,freeFunction是Outer类NativeAllocationRegistry的实例字段,记录了native层资源释放函数的函数指针。有了这两个指针,便可以完成native资源的回收。

libcore/luni/src/main/java/libcore/util/NativeAllocationRegistry.java

259     private class CleanerThunk implements Runnable {
260         private long nativePtr;
261 
262         public CleanerThunk() {
263             this.nativePtr = 0;
264         }
265 
266         public void run() {
267             if (nativePtr != 0) {
268                 applyFreeFunction(freeFunction, nativePtr);   <======== applyFreeFunction最终会调用freeFunction,而传入freeFunction的参数就是nativePtr
269                 registerNativeFree(size);
270             }
271         }
272 
273         public void setNativePtr(long nativePtr) {
274             this.nativePtr = nativePtr;  <============== nativePtr是native对象的指针
275         }
276     }

当ReferenceQueueDaemon轮询到Cleaner对象时,会调用它的clean方法。可以看到,在143行调用了thunk.run最终进入native世界的资源释放函数中。

libcore/ojluni/src/main/java/sun/misc/Cleaner.java

139     public void clean() {
140         if (!remove(this))
141             return;
142         try {
143             thunk.run();   <=================== 其内部调用资源释放函数
144         } catch (final Throwable x) {
145             AccessController.doPrivileged(new PrivilegedAction<Void>() {
146                     public Void run() {
147                         if (System.err != null)
148                             new Error("Cleaner terminated abnormally", x)
149                                 .printStackTrace();
150                         System.exit(1);
151                         return null;
152                     }});
153         }
154     }