はじめに
本記事は私がAndroidのメモリ管理やGCについて学んだ内容をまとめたものです。誤りや改善点がありましたら、
コメントでご指摘いただけると嬉しいです🙇♂️
昨今、新たな技術が目まぐるしく登場していますが私はそんな時代だからこそ「基礎」や「仕組みなどの本質」が非常に大切であると考えています。
この記事で理解できること
- メモリ管理の重要性
- メモリリークの正確な定義
LeetCodeなどでData structure and Algorithmを学んでる方や低レイヤーのリバースエンジニアリングを経験している方は想像つきやすいかもしれません。
全記事構成
一つの記事で全てまとめると非常に長くなることに気がつきました。
私自身、長い記事は読みにくくて苦手なので3つに分割します。
- Part1(本記事):メモリ管理の全体像 - GC/kswapd/lmkの役割
- Part2:GCの仕組み - 到達可能性と参照の種類
- Part3:ARTとGCの関連性 - Android実装の詳細
メモリ管理の基礎
本セクションは下記の知識が必要になります。
- LIFOなどの特性
- ポインタ概念
- オーダ記法を用いた時間計算量の概念
LeetCodeなどでData structure and Algorithmを学んでる方や低レイヤーのリバースエンジニアリングを経験している方は想像つきやすいかもしれません。
スタックは飛ばしてもらっても構いませんが、ヒープは軽く目を通してください。
スタックとヒープ
データ領域には2種類(スタックとヒープ)あります。
それぞれについて説明します。
スタック
スタックとは?
LIFO特性を持つデータ構造です。
関数を呼ぶとスタックにスタックフレームが追加されます。

上記画像のsub1/mainがスタックフレームです。
そして、当該関数が終了するとスタックフレームはpopされます。
スタックフレームの構造

各スタックフレームには上記画像のように約3種類のデータが含まれています。(厳密にはまだまだあります)
スタックの強み
- 時間計算量O(1)でpush/popが可能
理由は割愛します。rbpやアライメントなどもはや低レイヤーのお話になるので。
ただ、各スタックフレームの先頭ポインタなどを使ってpush/popをしてO(1)を実現しているということは認識ください。
スタックの弱み
- 可変サイズデータは格納が不可能
一般的にInt型などいわゆるプリミティブは既にサイズが決められています。(4バイトなど)
しかし、一部のデータはコンパイル時点ではサイズが分かりません。
例えば、APIのレスポンス結果を格納するリストだとレスポンスに応じて要素数が変化しますよね。
こうなるとコンパイル時ではサイズが確定できません。
そういった可変データは後述するヒープへ保存されます。
その代わりに、スタックではヒープに保管したデータの先頭ポインタを保持しておきます。
スタックまとめ
スタックでO(1)のpush/popを実現するには、コンパイル時にサイズが確定している必要があります。そのため、プリミティブ型の値やオブジェクトへの参照(ポインタ)のみをスタックに格納し、サイズが可変なオブジェクトの実体はヒープに配置するという設計になっています。
ヒープ
ヒープについて深掘りすると記事の量が増えるので浅くします
ヒープとは?
動的にメモリを確保・解放できる領域です。
スタックと異なり、サイズや寿命(いつ破棄するか)がコンパイル時に確定していないデータを格納します。
ヒープの特徴
強み
- サイズが可変(リストが伸び縮みする等)
- 関数を超えて生存できる
- ポインタ経由でのアクセスはO(1)
弱み
- メモリ管理(GC)が必要
- 確保・解放にスタックよりコストがかかる
ListやClassなど、非プリミティブ型のオブジェクトは基本的にヒープに格納されます。
ヒープまとめ
ヒープは可変データを格納できる柔軟なデータ構造で、ポインタ経由のアクセスはO(1)と高速です。一方で、メモリ管理にコストがかかり、不要なオブジェクトを回収するためにGCが必要になります。
メモリリークとは何か(より正確に)
まず、メモリは「使ってる間だけは参照を通じて領域を確保。使わなくなったら解放して他に譲る」これがベースです。オンデマンド方式のようなものです。
これに対しメモリリークとは、「もう使わないのに何かしらの要因で参照が残ってしまっており、GCに回収及び解放されず占有している状態」を指します。
メモリ管理の手法の枠組み
全体について
基本的に下記の流れで管理していきます。
ヒープ単位の管理
⇩
ページ単位の管理
⇩
プロセス単位の管理
ヒープ単位で十分なRAMを確保できなければページ単位の管理が発動して、それさえ無理だったらプロセス単位の管理になります。
詳細を話していきます。
ヒープ単位
まず、基本的に1アプリ毎に1プロセス付与されます。
そして、1プロセス毎に1ヒープ与えられます。(厳密には複数あるようですが)
GCはヒープ内での最適化を図るものです。
つまり、GCはヒープ単位での最適化を図る方式ですね。
ただし、メモリリークが頻発したり他アプリ(他プロセス)による圧迫を受けてGCだけでは立ちいかなくなった場合にページ単位で発動するのが次です。
ページ単位
kswapd(カーネルスワップデーモン)が呼ばれます。
これは、RAMが不足してきたら使用頻度の低いメモリページを退避させます。
また、退避先としてzRAMという圧縮領域を使用します。
zRAMはRAMの一部を圧縮専用領域として確保したもので、ページを圧縮して格納することで実質的な容量を増やします。
軽く、圧縮方式についても言及します。
例えば、AAAAAABBCCみたいなデータがあったとしたらこれをA6B2C2みたいに規則性を利用して短縮化して全体データサイズを削減します。
実際にはLZ4などの高速な圧縮アルゴリズムが使われます。
因みに、LZ4が採用される理由について話すと、圧縮率は低いが圧縮/解凍速度が非常に高速なためです。
現代のデバイスはRAMが多いので圧縮率よりも速度重視ですから、圧縮率の低さは然程問題にはならなかったようです。
そしてこれでもRAMが確保できなかった場合、プロセス単位の処理が走ります。
プロセス単位
lmk(LowMemoryKiller)が呼ばれます。
これが発動するとプロセスを強制終了させます。
先ほど、1アプリ=1プロセスという話をしましたがつまりlmkが発動するとアプリが強制終了されるのです。
どのプロセスが終了されるか?というのはoom_adj_scoreという重みづけで確定されます。

上から順番に重みが高い(優先度が高い)です。
基本的にはフォアグラウンドのアプリほど保護され、バックグラウンドのアプリから順に終了されます。
重み付はユーザーへの影響が少ない順(つまりUXに響きにくい)へ沿って行われていますね。
なぜメモリ管理が必要か
メモリ管理の目的はマシンのリソースを効率的かつ安全に使用するためです。
観点について整理しましょう。(Android端末を例に)
効率性の観点
- メモリ節約:不要なオブジェクトを解放し限られたRAMを効率的に利用
- パフォーマンス:メモリが不足するとGCやkswapd/lmkが発生
- バッテリー消費削減:GCやkswapd/lmkの頻発や不要なメモリ確保はバッテリー消費量上昇に影響
安全性の観点
- クラッシュ防止:OutOfMemoryErrorの発生防止
- アプリの安定性1:lmkを最小限に抑制
- アプリの安定性2:メモリ関連による再現性の低いバグを抑制
まとめ
本記事では以下を理解しました:
- 1アプリ=1プロセス
- スタックはO(1)で操作できるが固定サイズしか配置できない
- ヒープは可変サイズを置ける上にO(1)でアクセス可能
- メモリ管理はヒープ/ページ/プロセス単位に分かれる
- kswapdでは使わないページはzRAMに退避
- lmkではバックグラウンド寄りのプロセスからキルされる
- メモリ管理は効率性および安全性の観点から重要
以上をここで知ってもらえればと思います!!
次回はGCの仕組みについて記載していく予定です!!
次回(Part2)では、GCの仕組みについて詳しく解説します。
具体的には到達可能性やGC Roots、参照の種類などを扱います。
関連記事
- Part2:GCの仕組み(執筆予定)
- Part3:ARTとGCの関連性(執筆予定)