メモリリーク(Memory Leak)とは、アプリケーションが不要になったオブジェクトへの参照を保持し続け、ガベージコレクタ(GC)が占有メモリを解放できない状態を指します。LeakCanaryによると、適切に記述されたアプリケーションでもコード10,000行あたり3~5件のリークが発生します。各リークは利用可能なメモリを徐々に減少させ、動作の遅延やOutOfMemoryErrorを引き起こします。
重要なポイント
メモリリーク(Memory Leak)とは、オブジェクトが強い参照(Strong Reference)の連鎖を通じて到達可能なままとなり、論理的にはアプリケーションにもはや必要ないにもかかわらず、ガベージコレクタ(GC)がそのオブジェクトを生存しているとみなし、占有メモリを解放しない状態を指します。その結果、利用可能なヒープメモリは常に減少し、GCポーズの頻度が増加します。
手動メモリ管理言語(C、C++)とは異なり、Java/Kotlinにおけるリークは忘れられたfree()ではなく、忘れられた参照です。GCルートからリークしたオブジェクトへの強い参照が存在する限り、GCはそれを必要とみなします。典型的なGCルート:静的フィールド、アクティブスレッド、コールスタック、JNIグローバル参照。
リークの危険性はその累積効果にあります。100KBのリーク1つは目立ちませんが、100個のリークは10MBを占有し、頻繁なGCによりアプリケーションが遅延し始めます。リークが臨界量に達すると、OutOfMemoryErrorとアプリケーションクラッシュを引き起こします。リークの症状:Profilerグラフでのメモリ消費の持続的増加、STW(Stop The World)を伴う頻繁なGCポーズ、UIパフォーマンスの低下。
5種類のリークでモバイル開発における症例の95%をカバーします。それぞれに独自の原因と特徴的なコードパターンがあります。
最もよく知られたAndroidのリークは、ActivityやContextへの静的参照を保存することです。典型的なコード:onDestroy()でnullにならない静的Activityフィールド。静的フィールドが生きている間、1~10MBを占有するViewツリー全体を含むActivity全体が生き続けます。これがLeakCanaryが最初に発見する古典的なリークです。
解決策:ActivityやContextを静的フィールドに決して保存しないでください。Activityより長生きするシングルトンにはApplication Contextを使用してください。Activityへの参照が必要な場合は、WeakReference<Activity>を使用してください。
object MySingleton {
private var weakActivity: WeakReference<Activity>? = null
fun attach(activity: Activity) {
weakActivity = WeakReference(activity)
}
}
匿名クラスと非静的内部クラスは、暗黙的に外部クラスへの参照を保持します。onDestroy()の後に実行されるHandlerに渡されたRunnableは、Activity全体を保持します。ActivityをキャプチャするRetrofitコールバックも同様です。これが最も厄介なタイプのリークです — 暗黙的参照はコードに可視化されません。
Kotlinのobject式とラムダも外部クラスへの参照をキャプチャします。内部クラスは静的(またはKotlinではトップレベル)にし、外部参照はWeakReferenceを介して渡します。ラムダの場合は、viewLifecycleOwnerを使用したLifecycle-awareアプローチを採用してください。
システムサービスへのサブスクライブを解除しないことは、直接的なリークです。onResume()で登録されonPause()でunregisterを呼び出さないSensorManager、LocationManager、NotificationListenerはActivityを保持します。同様に:CompositeDisposableに追加されないRxJavaのDisposable、GlobalScopeを通じて起動されるコルーチン。
Lifecycle-awareコンポーネントを使用してください:LifecycleOwnerを使用したobserve()はonDestroy()で自動的にサブスクライブを解除します。RxJavaの場合は — DisposableObserverと共にviewLifecycleOwner.lifecycle.addObserverを使用します。コルーチンの場合は — lifecycleScope.launch()がライフサイクルにバインドされます。
// Lifecycleによる自動サブスクライブ解除
viewModel.userData.observe(viewLifecycleOwner) { data ->
updateUI(data)
}
// lifecycleScopeを使用したコルーチン
lifecycleScope.launch {
viewModel.loadData().collect { render(it) }
}
Bitmapはヒープメモリのかなりの量を占有します:1つのフルHDビットマップは1920×1080×4バイト=8.3MBです。リストの各アイテムに対してBitmapが作成され、非表示時にrecycle()が呼び出されない場合、メモリは急速に枯渇します。古いAndroidバージョン(3.0以前)ではBitmapはネイティブメモリに保存されていましたが、現代のバージョンではDalvik/ARTヒープにあり、GCは強い参照がない場合にのみ解放できます。
画像の読み込みにはGlideまたはCoilを使用してください — これらのライブラリはキャッシングとリサイクルを自動的に管理します。Bitmapを直接扱う場合は、表示されなくなった大きな画像に対してbitmap.recycle()を呼び出し、縮小コピーを読み込むためにinSampleSizeを使用してください。
Fragmentには2つのライフサイクルがあります:Fragment自体とそのViewです。onDestroyView()の後、Viewツリーは破棄されますが、外部参照がある場合、Fragment自体はメモリに残り続ける可能性があります。典型的なエラーは、ViewPagerアダプターやナビゲーショングラフにFragmentへの参照を保存し、破棄時にクリアされないことです。
長寿命オブジェクトのフィールドにFragmentへの参照を決して保存しないでください。ネストされたフラグメントにはchildFragmentManagerを、それらの間のデータ転送にはLifecycleOwnerを使用したobserve()を使用してください。ViewPager2はAPIレベルでこの問題を解決しました:FragmentTransactionAdapterがライフサイクルを正しく管理します。
リークの検出には2つの事実の確認が必要です:期待されるライフタイム後にメモリが戻らないこと、特定のタイプのオブジェクト数が減少せずに増加すること。診断プロセスは3つの段階を含みます。
第一段階 — Android StudioのMemory Profilerによる視覚的確認。Memoryタブを開き、対象のアクション(画面を開いて閉じる)を実行し、GC(Garbage Collection)を押して、メモリが初期レベルに戻るか確認します。3~4回の開閉サイクル後もメモリが一貫して増加している場合 — リークがあります。
第二段階 — Heap Dumpの取得。Memory ProfilerでDump Java Heapを押します。結果の.hprofファイルをAndroid Studioで開くと、サイズと参照とともにヒープ内のすべてのオブジェクトが表示されます。画面を閉じた後に数がゼロになるべきクラスを探します。例えば、閉じた後にカウントが2のMainActivity — 明らかなリークです。
第三段階 — Retained SizeとGC Rootの分析。Android StudioでRetained Sizeを分析します:このオブジェクトを削除するとどのくらいのメモリが解放されるか。GC Rootからオブジェクトへのパスは、何がそれを保持しているかを示します:Static field → HashMap → Activity — リークポイントがわかります。Referenceウィジェットパネルはオブジェクトのすべての保持者を表示します。
4つのツールが自動検出から深層Heap Dump分析までリーク探索をカバーします。
| ツール | 方法 | 結果の形式 |
|---|---|---|
| LeakCanary | 自動監視 | Heap Dump + リークstack trace |
| Android Memory Profiler | 手動監視 | メモリグラフ + Heap Dump |
| MAT (Eclipse) | 深層分析 | Dominator Treeレポート + GC Rootパス |
| Perfetto | システム全体のトレース | タイムライン + ネイティブメモリ |
LeakCanaryはあらゆるAndroidプロジェクトに必須です。Activity/Fragmentのライフサイクル終了時にリークを自動検出し、stack traceとともに正確なリーク位置を表示します。統合:build.gradleに1行追加するだけ。LeakCanary 2.xは手動初期化が不要です — 自動的にApplication Watcherを登録します。
リークの予防は、各段階でコードをチェックする一連のルールとツールを通じて開発プロセスに組み込まれます。
Activity、Fragment、Viewへの参照を決して静的フィールド、シングルトン、または長寿命オブジェクトに保存しないでください。参照が避けられない場合は、WeakReferenceを使用するか、ViewModelを通じてデータを保存してください。ViewModelは必要な期間だけ存続し、Viewを直接保持しません。
Android Architecture ComponentsのViewModelとLiveDataは、アーキテクチャレベルでライフサイクルの問題を解決します。ViewModelは画面回転後も存続し、Viewへの参照を含みません。LiveDataはonDestroy()で自動的にオブザーバーのサブスクライブを解除します。システムサービスへの手動サブスクリプションの代わりにこれらを使用してください。
コードレビューでは以下に注意してください:Context/View型の静的フィールド、匿名クラス、Activityをキャプチャするラムダ、手動サブスクリプション、compositeなしのRxJava disposable、Bundleを介したFragmentの保存。Kotlinでは、ライフサイクルバインディングなしのlaunchを使用するコルーチンを追加で確認してください。
LeakCanaryはテストパイプラインの一部として機能します:LeakCanaryを使用して受け入れテストを実行し、リークが見つかった場合にビルドを失敗させます。これによりリークが本番環境に到達するのを防ぎます。Android LintのStaticFieldLeakルールでチェックを補完してください — 静的解析レベルで潜在的なリークを発見します。
// テストでのLeakCanary
class LeakTest {
@Test
fun activityShouldNotLeak() {
ActivityScenario.launch(MainActivity::class.java)
.close()
LeakAssertions.assertNoLeak() // リークがあれば失敗
}
}
よくある質問
リークが原因で、OutOfMemoryErrorは結果です。1つのリークではOOMに至りませんが、数十のリークの蓄積がヒープを使い果たします。OOMは致命的な例外であり、リークは時間とともにそれに至るパターンです。
Android Memory Profilerを使用:画面を5回開閉し、各閉鎖後にGCを呼び出します。メモリがベースラインレベルに戻らない場合 — リークがあります。Heap Dumpを取得し、閉鎖後にカウントが0より大きいActivityクラスをリストから見つけてください。
部分的に。Kotlinはnull-safety問題を解決しますが、強い参照は管理しません。lifecycleScopeやviewModelScopeを使用したコルーチンはバックグラウンドタスクからのリークを防止し、sealed classやdata classはリークにつながる状態の数を減らします。主な保護は言語機能ではなく、アーキテクチャパターンです。
LeakCanaryは時々誤検出を出すことがあります:オブジェクトがシステムによって一時的に保持される場合があります(例:InputMethodManagerが最後のViewを保持)。手動で確認してください:Retained Sizeが1KB未満でGC Rootがシステムサービスの場合、おそらく誤検出です。
いいえ。リークはGCを備えた任意のプラットフォームで発生する可能性があります:iOS(Swift/Objective-C)、Flutter(Dart)、Webブラウザ(JavaScript)。メカニズムは同じです — GC Rootからの強い参照。iOSではARCが自動的にメモリを管理しますが、オブジェクト間のretain cycleが同じリークを生み出します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。