源码分析基于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个缺点,感兴趣的可以去油管上查看:链接,我在这里简单总结下。
- 如果两个对象同时变成unreachable,他们的finalize方法执行顺序是任意的。因此在一个对象的finalize方法中使用另一个对象持有的native指针,将有可能访问一个已经释放的C++对象,从而导致native heap corruption。
- 根据Java语法规则,一个对象的finalize方法是可以在它的其他方法还在执行时被调用的。因此其他方法如果正在访问它所持有的native指针,将有可能发生use-after-free的问题。
- 如果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的很多缺陷都是由于它太“能干”了。
- 如果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种。
- SoftReference,软引用,它具有两个特性。一是可以通过get获取到referent对象,二是referent在仅被它引用时,可以一直存活,直到堆内存真的被耗尽以至于马上要发生OOM时,referent才会被回收。常用于实现Cache机制。
- WeakReference,弱引用。当referent仅被WeakReference引用时,该referent对象在下次GC时会被回收。所以和SoftReference相比,二者的区别仅在于referent被回收的时机。它常用于需要用到referent,但又不希望自己的引用影响referent回收的场景。
- 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来执行它。
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 }