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     }
複製代碼
相關文章
相關標籤/搜索