OutOfMemoryErrorは、Java仮想マシン(JVM)またはAndroid Runtime(ART)がヒープ(Heap)の容量不足により新しいオブジェクトにメモリを割り当てられない場合に発生する致命的な例外です。Square Engineeringによると、モバイルアプリケーションにおけるOutOfMemoryErrorの70%は、実際の制限超過ではなくメモリリークが原因です。OOMの原因を理解することが、アプリケーションの安定性の鍵です。
重要なポイント
OutOfMemoryError(OOM)は、Java/KotlinのVirtualMachineErrorファミリーに属する例外で、新しいオブジェクトにメモリを割り当てられないことを示します。チェック例外とは異なり、OOMはErrorでありcatchによる処理は必要ありませんが、技術的にキャッチすることは可能です。OOMが発生した後、アプリケーションは通常不安定な状態になり、終了することが推奨されます。
Androidでは、各アプリケーションにデバイスメーカーが設定したHeap制限があります。6+ GB RAMの最新スマートフォンの場合、制限は256~512 MB、低価格デバイスでは128~192 MBです。すべての生存オブジェクトの総量がこの制限を超えると、ARTがOutOfMemoryErrorをスローします。
重要なのは、OOMは必ずしもデバイスの物理メモリが不足したことを意味しないということです。これは、アプリケーションがシステムによって設定されたHeap制限を使い果たしたことを意味します。他のアプリケーションには空きメモリがあるかもしれませんが、Androidのプロセス分離のため、あなたのアプリケーションはそれを使用できません。
5つのシナリオが定期的にモバイルアプリケーションでOOMを引き起こします。各シナリオは特定のデータ型または操作に関連しています。
BitmapはAndroidアプリケーションにおける主要なメモリ消費者です。FullHD画像(1920×1080)を元のサイズで読み込むと、ARGB_8888形式で8.3 MBを占有します。RecyclerViewにそのような画像が50枚ある場合、415 MBになり、どのデバイスのHeapも超えます。inSampleSizeなしでの画像読み込みは、低スペックデバイスでのOOMを確実に引き起こします。
自動スケーリングにはGlideまたはCoilを使用してください。これらのライブラリは、元の解像度ではなくViewに合わせたサイズで画像を読み込みます。BitmapFactory.Optionsを直接使用する場合は、inSampleSizeを適用します。2の累乗として計算し、最終的なサイズが2048×2048ピクセルを超えないようにします。さらに、透明度のない画像にはARGB_8888の代わりにRGB_565を使用すると、メモリ消費が半分になります。
fun loadScaledBitmap(path: String, reqWidth: Int): Bitmap? {
val opts = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(path, opts)
opts.inSampleSize = calculateSampleSize(opts.outWidth, reqWidth)
opts.inJustDecodeBounds = false
return BitmapFactory.decodeFile(path, opts)
}
1つの数KBのリークはOOMを引き起こしません。しかし、各画面での数十のリークが蓄積されます。画面遷移ごとにリークが追加され、GCがオブジェクトを解放できず、Heapが満杯になります。典型的なパターン:ユーザーがプロフィール画面を20回開閉する→Heapが200 MB増加する→アプリケーションがOOMでクラッシュする。
プロジェクトにLeakCanaryをインストールして、リークを自動検出してください。正確なスタックトレースとともに、リークした各オブジェクトを表示します。すべてのリークを修正した後、Heap消費は安定します。画面を閉じた後、メモリはベースラインレベルに戻ります。
ファイル全体をbyte[]に読み込むことは、OOMへの直接的な道です。50 MBのJSONファイルは、解析中に同じサイズの文字列とDOMモデルを作成します。メモリに読み込まれた動画ファイル、オーディオバッファ、大規模なprotobufデータセットは、すべて1回の操作でHeap制限を超える可能性があります。
大規模データはストリームで処理してください。4~8 KBバッファのInputStream、ストリーミングJSONパーサー(JacksonまたはGson + JsonReader)、動画にはMediaCodec。利用可能なHeapの10%を超えるファイルでFile.readBytes()を呼び出さないでください。
ループ内で中間GCなしにオブジェクトを大量に生成すると、特にHeapの小さいデバイスでOOMを引き起こす可能性があります。例:GCが収集できる前にHeapに収まらない100,000個のオブジェクトをfor-loopで生成する。これはゲームやグラフィックエディタでより一般的です。
大量に作成・破棄されるオブジェクトにはObject Poolを使用してください。数値データにはプリミティブを使用します(List<Float>の代わりにFloatArray)。ViewHolder Poolを備えたRecyclerViewは、UIコンポーネントでこの問題を解決します。
断片化とは、合計では十分な空きメモリがあるが、新しいオブジェクト用の連続したブロックがない状態です。ARTはGC中にHeapを圧縮しますが、常に成功するとは限りません。大きな配列(Bitmap、byte[])は断片化の影響を最も受けやすくなります。
Android 8+のARTは世代別GCを使用しており、若いオブジェクトと古いオブジェクトを分離することで断片化を低減します。それでも、同じプール内で異なるサイズの断片を割り当てることは避け、事前に割り当てられた固定サイズのバッファを使用するようにしてください。
AndroidのHeap制限は定数ではありません。メーカー、デバイスモデル、OSバージョンに依存します。GoogleはCompatibility Definition Document(CDD)を通じて最小要件を定めていますが、実際の値はメーカーが設定します。
| デバイスカテゴリ | 標準Heap | largeHeap |
|---|---|---|
| 低価格帯(1~2 GB RAM) | 128~192 MB | 256~384 MB |
| ミッドレンジ(3~4 GB RAM) | 256~384 MB | 512 MB |
| フラッグシップ(6+ GB RAM) | 384~512 MB | 768 MB~1 GB |
| タブレット(4+ GB RAM) | 256~512 MB | 768 MB |
| Wear OS | 32~64 MB | なし |
マニフェストでandroid:largeHeap="true"を使用して、拡張された制限をリクエストできます。注意して使用してください。Heapを増やしてもリークの問題は解決されず、システムがあなたのアプリケーションのためにメモリを解放するために他のアプリケーションを強制終了する必要がある場合、ユーザー体験が悪化する可能性があります。Wear OSの場合、Heap制限は最小の32~64 MBのみで、largeHeapは利用できず、メモリ節約は二重に重要です。
OOMの診断には、Heap Dumpの分析と、どのオブジェクトがメモリを消費しているかの理解が必要です。Android Studioは必要なツールをすべて提供します。
ステップ1:OOMの瞬間をキャプチャします。Android Memory Profilerで、Record memory allocationsをクリックし、クラッシュを引き起こすシナリオを実行します。ProfilerはOOMの前に割り当ての急増を示します。OOMが再現できない場合は、デバッグビルドでandroid:smallHeapを使用してHeapを減らすか、手動GC呼び出しでDDMSを使用します。
ステップ2:ピーク負荷時(OOMの前)にHeap Dumpを取得します。Android StudioでDumpを開きます。ClassesタブはRetained Sizeでソートされています。最大のオブジェクトはBitmap、byte[]、Stringです。各Bitmapについて、サイズ(幅×高さ×4バイト)とStack Traceによる読み込みパスを確認します。
ステップ3:重複オブジェクトの数を分析します。200個の同一のFragmentまたはActivityが見られる場合、それはリークです。同じサイズの500個のBitmapがある場合、画像キャッシュの問題です。MAT(Memory Analyzer Tool)は、Dominator Treeを使用して、どのオブジェクトがHeapの80%を保持しているかを示すより深い分析を提供します。
// adb経由のHeap Dumpコマンド
adb shell am dumpheap com.example.app /data/local/tmp/dump.hprof
adb pull /data/local/tmp/dump.hprof.
包括的なOOM防止戦略には、アーキテクチャ上の決定から本番環境での監視まで、5つのレベルの保護が含まれます。
ViewModel + Repositoryパターンは、データをUIから分離し、画面回転時のViewの保持を防ぎます。ViewModelはActivityよりも長生きし、そのデータは失われず、Viewはメモリ内のデータを複製せずに再作成できます。明示的な状態管理にはLiveDataの代わりにStateFlowを使用してください。
Glideは画像を扱うための必須ライブラリです。自動的にスケーリング、キャッシュ(ディスク+メモリ)、Bitmapのリサイクルを行います。大規模なリストの場合は、diskCacheStrategyとskipMemoryCacheを設定してください。アニメーション画像には、GIF/WebPとともにGlideを使用します。これらはBitmapのシーケンスよりも少ないメモリしか消費しません。
Firebase Performance Monitoringは、メモリ消費をリアルタイムで追跡します。Heap使用率が制限の80%を超えた場合にアラートを設定してください。これは調査のシグナルです。CrashlyticsはOOMを例外として収集し、クラッシュ前の最終既知のHeap状態を表示します。Android 11+では、ApplicationExitInfoを使用してOOM終了を検出します。
必ず最小Heap(128~192 MB)のデバイスでアプリケーションをテストしてください。小さな画面と小さなHeapのエミュレータは低価格デバイスをエミュレートします。そのようなデバイスでアプリケーションが動作すれば、フラッグシップでOOMの問題は発生しません。さまざまな価格帯の実デバイスでFirebase Test Labを使用してください。
// 高負荷操作前の利用可能Heapの確認
fun canAllocate(requiredBytes: Long): Boolean {
val runtime = Runtime.getRuntime()
val free = runtime.freeMemory()
return free > requiredBytes * 2 // 50%バッファ
}
よくある質問
技術的には可能ですが、推奨されません。OOM後、アプリケーションは不安定な状態にあり、新しい割り当てが失敗したり、一部のオブジェクトが部分的に作成されたりする可能性があります。catchで合理的な唯一のアクションは、ロギングとActivityの再起動です。
Heap制限はデバイスによって異なります。300 MBを必要とする操作は、192 MBの制限があるデバイスではクラッシュしますが、512 MBのフラッグシップでは成功します。OOMシナリオを検出するには、最小スペックのデバイスでテストしてください。
largeHeapは制限を増やしますが、アプリケーションを高速化しません。大きなHeapの収集にはより時間がかかるため、GC一時停止が長くなります。システムはメモリを確保するためにバックグラウンドアプリケーションを強制終了する可能性があります。largeHeapは、客観的に多くのメモリを必要とするアプリケーション(カメラ、エディタ)にのみ使用してください。
OOMはHeapが不十分な場合のアプリケーション内部の例外です。システムキル(Low Memory Killer)は、他のアプリケーションにメモリを解放するためにプロセスを強制終了するLinuxカーネルの決定です。システムキルの場合、アプリケーションは例外を受け取らず、プロセスは単に終了します。
計算式:幅×高さ×bytesPerPixel。ARGB_8888 = 4 B/ピクセル、RGB_565 = 2 B/ピクセル。ARGB_8888のFullHD Bitmap(1920×1080) = 8.3 MB。4K Bitmap(3840×2160) = 33 MB。画像は常に画面表示に必要なサイズにスケーリングしてください。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。