LeakCanaryの仕組みをある程度理解したいマン
LeakCanaryとは何か
https://corner.squareup.com/2015/05/leak-canary.html
Square社が開発したメモリリーク検知ライブラリ。
導入はとてもシンプル。
1 |
LeakCanary.install(this); |
これだけでメモリリークが発生したときに通知してくれる。便利。
では、メモリリークの検知をどのように実現しているのだろうか。
気になったので読んでみた。
どのタイミングでメモリリークの有無の確認を実行しているのか?
カギはActivityRefWatcherにあった。
ActivityLifecycleCallbacksをApplicationに登録することでアプリ内に登録されてるActivityのonXXXをListenできるようになる。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
private final Application.ActivityLifecycleCallbacks lifecycleCallbacks = new Application.ActivityLifecycleCallbacks() { /// blahblah... @Override public void onActivityDestroyed(Activity activity) { ActivityRefWatcher.this.onActivityDestroyed(activity); } }; void onActivityDestroyed(Activity activity) { refWatcher.watch(activity); } |
↑を見てみると、Activityが破棄されたタイミングでRefWatcherクラスのwatchメソッドが呼ばれている。
画面が破棄されたタイミングでメモリリークが無いかwatchして、メモリリークが確認できれば通知する。
っていう流れだろうか。
詳しく見てみる。
RefWatcherクラスの役割
watch
watchではどんな処理が実行されているのだろうか。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/** * Watches the provided references and checks if it can be GCed. This method is non blocking, * the check is done on the {@link Executor} this {@link RefWatcher} has been constructed with. * * @param referenceName An logical identifier for the watched object. */ public void watch(Object watchedReference, String referenceName) { checkNotNull(watchedReference, "watchedReference"); checkNotNull(referenceName, "referenceName"); if (debuggerControl.isDebuggerAttached()) { return; } final long watchStartNanoTime = System.nanoTime(); String key = UUID.randomUUID().toString(); retainedKeys.add(key); final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue); watchExecutor.execute(new Runnable() { @Override public void run() { ensureGone(reference, watchStartNanoTime); } }); } |
1 2 |
final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue); |
1 2 3 4 5 |
watchExecutor.execute(new Runnable() { @Override public void run() { ensureGone(reference, watchStartNanoTime); } }); |
ensureGoneメソッドで与えられた参照がGCによって葬り去れるか調べてる感じ。
メインスレッドで実行するとブロッキングが発生するのでwatchExecutorが専用のThread上でensureGoneしてる。
KeyedWeakReferenceクラスが活躍するのはこれから。
このクラスのおかげで与えられた参照がGCされたかどうか検知できるようになる。
その辺は後ほど。
とりいそぎ、ensureGoneメソッドを見る。
ensureGone
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) { long gcStartNanoTime = System.nanoTime(); long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime); removeWeaklyReachableReferences(); if (gone(reference) || debuggerControl.isDebuggerAttached()) { return; } gcTrigger.runGc(); removeWeaklyReachableReferences(); if (!gone(reference)) { long startDumpHeap = System.nanoTime(); long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime); File heapDumpFile = heapDumper.dumpHeap(); if (heapDumpFile == HeapDumper.NO_DUMP) { // Could not dump the heap, abort. return; } long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap); heapdumpListener.analyze( new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs, gcDurationMs, heapDumpDurationMs)); } } private boolean gone(KeyedWeakReference reference) { return !retainedKeys.contains(reference.key); } private void removeWeaklyReachableReferences() { // WeakReferences are enqueued as soon as the object to which they point to becomes weakly // reachable. This is before finalization or garbage collection has actually happened. KeyedWeakReference ref; while ((ref = (KeyedWeakReference) queue.poll()) != null) { retainedKeys.remove(ref.key); } } |
ざっくりと流れは
- GCを実行する
- removeWeaklyReachableReferences()を実行して、与えられた参照がGCによって回収されているか確認しつつ、参照のキーを保持してるSetクラス変数retainedKeysからキーを抹消する(メソッド名と実際の処理内容のマッチングが微妙な感じではあるが。。)
- retainedKeysに該当するキーが抹消されてるか確認して、残っていればGCで回収できない=誰かに参照されてる=メモリリークと判断する
- メモリリークと判断されたらヒープをダンプして通知UIやら詳細情報表示する用Activityを表示する処理を実行する
と、こんな感じ。
上記の処理をあるActivityがDestroyされるたびに実行してる。
なんとなく流れは分かった気分になってる。
removeWeaklyReachableReferencesでKeyedWeakReferenceクラスが活躍する。
↑の実装を見つつ、こちらの記事を見ると大変分かりやすい。
http://www.ne.jp/asahi/hishidama/home/tech/java/weak.html
複数のWeakReferenceを使う場合、どれがクリアされたかを知るには、全WeakReferenceをリストか何かに入れておいて一つ一つget()してnullが返ってくるかどうかをチェックすればいいような気がする。
しかしこれは無駄が多い。そこで、もう少しエレガントな方法が用意されている。
参照キュー(ReferenceQueueクラス)というものを用意し、WeakReferenceのコンストラクターに渡す。
すると、WeakReferenceから値が消されると、参照キューにそのWeakReferenceが追加される。
これを踏まえて、再びremoveWeaklyReachableReferencesを見てみる
1 2 3 |
while ((ref = (KeyedWeakReference) queue.poll()) != null) { retainedKeys.remove(ref.key); } |
WeakReferenceを作るときにReferenceQueueを渡しておくと、GCが実行されたときにReferenceQueueに参照(ここでいうとKeyedWeakReference)が格納される。
で、queueのpollメソッドを呼び出すとqueueに詰まってる参照がとりだせる。
とりだせない、ということは参照先のオブジェクトに対してGCができなかった。ということ。
別のオブジェクトが参照先のオブジェクトを参照している=メモリリーク、なので集合retainedKeysからキーを取り除けないので、goneでfalseを返す。で、メモリリーク時の処理が実行される。
だいたいこんな感じ。