源碼分析基於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++
如何在Java對象被回收的時候,自動釋放其所關聯的native對象和資源?程序員
這是finalize和Cleaner想要解決的問題。純Java層面的應用開發一般不會涉及到Java對象持有native對象指針的設計,但對一些複雜的類而言,這種設計不可或缺。譬如你們常常用到的Bitmap,就是經過這種方式將大部份內存消耗放到native堆而不是Java堆。markdown
Finalize用起來很方便。覆寫一個方法,在方法裏面釋放資源,兩步就能夠搞定native資源的釋放,因此也被廣大開發者所喜好。但是方便有時須要付出代價。性能的犧牲是一方面,在某些場景下致使的內存錯誤則更加沒法忍受。Android Runtime團隊的大佬Hans Boehm在Google IO 2017曾就這個問題專門作過演說,裏面提到的finalize的3個缺點,感興趣的能夠去油管上查看:連接,我在這裏簡單總結下。app
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的優勢。源碼分析
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即可以自動了。這個話題後面會專門成文介紹,在此先按下不表。
Referent對象,俗稱被引用對象,也即Cleaner須要追蹤的對象。Cleaner類繼承於PhantomReference類,緣由在於它須要利用虛擬引用的特性:在跟蹤對象回收時本身加入到ReferenceQueue中,繼而能夠自動完成native資源的回收。下圖展現了一個PhantomReference對象加入到ReferenceQueue中的過程。
Referent對象在被強引用時,處於reachable狀態,在GC階段經過GC Root能夠標記到這個對象,所以不會被回收。只有當沒有任何強引用指向它時,它纔會被容許回收。但容許回收和發生回收是兩回事,這也致使Java中的弱引用類型被實現爲3種。
PhantomReference對象的入列過程其實涉及到多個線程。並且Cleaner做爲一種特殊的PhantomReference,它本身又有一套獨立的入列規則。如下分開介紹。
Cleaner在ReferenceQueueDaemon線程的處理過程當中被看成一種特殊對象,所以無需開發者新建線程來輪詢ReferenceQueue。可是須要注意,全部的Cleaner都會放在ReferenceQueueDaemon線程進行處理,所以要保證Cleaner.clean方法中作的事情是快速的,防止阻塞其餘Cleaner的清理動做。
普通PhantomReference對象最後會加入構造時傳入的ReferenceQueue中。對於這些ReferenceQueue有兩種處理方式,一種是調用ReferenceQueue.poll
方法進行非阻塞的輪詢,另外一種是經過調用ReferenceQueue.remove
方法進行阻塞等待。一般而言,ReferenceQueue的處理須要開發者新開線程,所以若是同時處理的ReferenceQueue過多,則也會形成線程資源的浪費。
本文分析基於Android 11(R)版本的源碼,側重於闡釋ART虛擬機對PhantomReference對象的特殊處理,其中會涉及到GC的部分知識。
對於Concurrent Copying Collector而言,其GC能夠粗略上分爲Mark和Copy兩個階段。Mark結束後,全部被標記過的對象放到Mark Stack中,用於後續處理。
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_後,接着會怎麼處理呢?
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。
在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 }
複製代碼
在沒有任務到來時,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處理方式不一樣,下面將分別介紹。
全局的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 }
複製代碼
經過上面代碼的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中 | ✔️ | ❌ |
最後的處理放在自定義的線程中 | ❌ | ✔️ |
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 }
複製代碼