ART虚拟机 | Finalize的替代者Cleaner

源码分析基于Android 11(R)java

前言

C++中的对象释放由程序员负责,而Java中的对象释放则由GC负责。若是一个Java对象经过指针持有native对象,那么应该什么时候释放native对象呢?靠原有的GC天然搞不定,由于虚拟机没法得知这个Java对象的long型字段是否是指针,以及该指向哪一个native对象。android

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

目录

1. 须要解决的问题

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

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

1.1 Finalize的缺点

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

  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岁辞退的浮躁心态来看,他这么大年纪没混成个领导,还在一线写代码,真是失败。但看到他的我的简介,你还能说出这样的话么?ide

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.函数

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

扯得有点远,说完了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     }
复制代码