ガベージコレクションによる自動メモリ管理は、ART仮想マシンに基づくAndroidプラットフォームの重要なメカニズムです。Google Android Documentation、2026によると、ガベージコレクタは開発者を手動のメモリ管理から解放し、参照がなくなったオブジェクトを自動的に削除します。GCがなければ、オブジェクトを割り当てるたびに明示的にfreeやdeleteを呼び出す必要があり、1秒間に数百万のオブジェクトを扱うJavaエコシステムでは物理的に不可能です。
重要ポイント
Garbage Collection (GC) は、プログラムがもう使用しなくなったオブジェクトが占有するメモリを検出して解放する自動プロセスです。モバイル開発の文脈では、GCはART仮想マシンを介してAndroidプラットフォームで使用されるほか、標準のJava Virtual Machineでも使用されます。
手動メモリ管理言語(C、C++)とは異なり、プログラマーが明示的にfreeやdeleteを呼び出す必要があるのに対し、GCはオブジェクトのライフサイクル追跡を完全に引き受けます。開発者はnew演算子で新しいオブジェクトを作成し、コレクタはオブジェクトが到達不能になったタイミング — つまり、アクティブな参照がなくなったタイミングを判断します。
GC効率の主な指標は、ポーズ時間(pause time)とスループット(throughput)です。ポーズとは、コレクションのためにアプリケーションの実行が停止される期間です。モバイル環境では、8–16ミリ秒を超えるポーズはドロップフレーム(jank)として認識されます。
Google I/O 2019によると、Android 10のARTは一般的なGCポーズを2–4msに削減し、Android 4.4のDalvikと比較して70%減少しました。それでも、不適切なメモリ管理 — ループ内での頻繁なオブジェクト割り当て、不必要な一時インスタンスの作成 — はパフォーマンス問題の主な原因であり続けています。
JavaとAndroidのすべてのGC実装は、ポーズ時間とコレクションの完全性のバランスを達成するために組み合わされるいくつかの基本アルゴリズムに基づいています。これらのアルゴリズムを理解することは、GCフレンドリーなコードを書くために不可欠です。
Mark-and-Sweep は最も単純なアルゴリズムで、2段階で動作します。Markフェーズでは、コレクタはルート参照(ルートセット)— ローカル変数、静的フィールド、スレッドスタック — からオブジェクトグラフをトラバースします。到達可能な各オブジェクトには生存フラグがマークされます。Sweepフェーズでは、コレクタはヒープ全体をスキャンし、マークされていないオブジェクトのメモリを解放します。
欠点はメモリの断片化です。Sweep後、空き領域と占有領域が交互に現れ、大きなオブジェクトの割り当てが困難になります。モバイルシナリオでは、ヒープが通常小さい(Androidでは64–512MB)ため、これは重要です。
Copying Collection はヒープを2つの半領域(semi-space)に分割します。アクティブなオブジェクトは、隙間なく一方の半領域から他方にコンパクトにコピーされます。コピー後、古い半領域は完全に解放されたと宣言されます。このアルゴリズムは断片化を完全に排除しますが、2倍のメモリが必要です。
モバイル環境では、Copying Collectionは、統計的に早期に死亡する若いオブジェクト(弱い世代仮説)の高速クリーンアップのために、世代別コレクタによって使用されます。
Generational Collection はヒープを世代に分割します:Young Generation(若いオブジェクト)とOld Generation(複数のコレクションを生き延びた古いオブジェクト)。若い世代のコレクション(Minor GC)は頻繁かつ迅速に実行されます。これは、ほとんどのオブジェクトが若いうちに死ぬためです。古い世代のコレクション(Major GCまたはFull GC)は頻度は低いですが、より長時間かかります。
// ジェネレーショナルGCのデモ:若いオブジェクトはすぐに死ぬ
void processItems(List<Item> items) {
List<Result> results = new ArrayList<>(); // メソッド全体で生存
for (Item item : items) {
Result r = new Result(item.getValue()); // 即座に死ぬ
if (r.isValid()) {
process(r); // rがガベージになる
}
}
saveResults(results); // resultsがOld Genに移行
}
この例では、Resultオブジェクトがループ内で作成され、すぐにガベージになります — これらはYoung GCの理想的な候補です。resultsオブジェクトはより長く生存し、Old Generationに移行します。世代の分離により、Minor GCは古いヒープに触れることなく、ミリ秒単位で若いオブジェクトをクリアできます。
AndroidはDalvik VMからART(Android Runtime)へと進化し、GCの実装は両者の主要な違いの1つです。AndroidのGCアーキテクチャを理解することで、実際のデバイスでポーズを最小化するコードを書くことができます。
| 特性 | Dalvik(4.4まで) | ART(5.0+) |
|---|---|---|
| GCタイプ | Concurrent Mark付きMark-and-Sweep | Generational + Concurrent |
| 典型的なポーズ | 10–30 ms | 2–4 ms |
| コンパクション | なし(断片化が増加するのみ) | あり(バックグラウンドで、アプリ停止なし) |
| AOTコンパイル | JIT(Just-In-Time) | AOT + JIT(ハイブリッド) |
Dalvik は、Mark-and-Sweepとコンカレントフェーズの組み合わせを使用していました。Concurrent Markにより、オブジェクトグラフのトラバース中もアプリケーションは動作を継続できましたが、Sweepフェーズではすべてのスレッドを停止する必要がありました(Stop-The-World)。RAMが少ないデバイス(512MB — 1GB)では、ポーズは30msに達し、インターフェースに顕著な遅延を引き起こしました。さらに、Dalvikはヒープをコンパクト化しなかったため、長時間の使用後には断片化が進み、大きなオブジェクト(Bitmapなど)の割り当て時に、十分な総空きメモリがあるにもかかわらずOutOfMemoryErrorが発生する可能性がありました。
ART(Android Runtime) は、コンカレントコンパクションを備えた世代別コレクタを導入しました。ヒープは3つの領域に分割されます:Young、Mature(Old Generationに類似)、Large Object Space(12KBを超えるオブジェクト用)。ほとんどの場合、Young Regionのコレクションはスレッドを停止せずに並行して行われます。Android 10+では、Concurrent Copyingが導入されました — コンパクションはStop-The-Worldなしでバックグラウンドスレッドで実行されます。
ARTのアーキテクチャのおかげで、典型的なGCポーズは2–4msに削減され、若いオブジェクトが大半を占めるシナリオでは0.5–1msにまで短縮されました。これにより、Androidデバイスはアクティブなメモリ操作中でも安定した60FPSを維持できるようになりました。
Javaエコシステムには、それぞれ独自のパフォーマンスプロファイルを持つ複数のGC実装があります。Android開発では選択肢はARTに限定されますが、Java GCの知識はモバイルアプリケーションのサーバーサイドコードを作成する際や、Kotlin Multiplatformでの開発時に役立ちます。
Serial GC は、アプリケーションを完全に停止(Stop-The-World)させるシングルスレッドコレクタです。Mark、Sweep、Compactの各操作は1つのスレッドで実行されます。パフォーマンスは低く — モバイルサーバーには使用されません。最大100MBのヒープを持つ小さなアプリケーションにのみ適しています。
Parallel GC(スループットコレクタとしても知られる)は、すべてのコレクションフェーズで複数のスレッドを使用します。最大スループット — アプリケーション実行時間に対するGC時間の最小化 — を目指しています。JVMで-XX:+UseParallelGCフラグを使用して有効化します。
G1(Garbage-First)GC はJava 9+のデフォルトコレクタです。ヒープは1–32MBのリージョンに分割されます。G1はポーズ時間を予測し、指定された制限(デフォルト200ms)内に収まるよう努めます。優先順位:最もガベージの多いリージョンから先にクリーンアップされます(名前の由来)。G1は、予測可能なポーズを持つ大規模ヒープのサーバー(4–64GB)に効果的です。
// ターゲットポーズ100msでG1 GCを有効化
// java -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar app.jar
public class MemoryMonitor {
private static final long THRESHOLD = 512 * 1024 * 1024; // 512 MB
public void checkHeapUsage() {
Runtime rt = Runtime.getRuntime();
long used = rt.totalMemory() - rt.freeMemory();
if (used > THRESHOLD) {
System.out.println("Heap usage exceeded threshold: " + used);
System.out.println("Consider reducing allocations");
}
}
}
Runtimeを介したヒープの監視により、メモリリークを早期に検出できます。安定した動作中にusedが最大ヒープの80%を超えた場合 — これはリークの可能性、またはアプリケーションによる過剰なメモリ消費のシグナルです。
現代のART GCでさえすべての問題を解決できるわけではありません — 不適切なメモリ使用は、jankとANR(Application Not Responding)の主な原因であり続けています。主要なシナリオと最適化方法を見ていきましょう。
GCポーズ — コレクション中のアプリケーションスレッドの停止です。画面上では、2つのフレーム間の時間が16.6ms(60FPS)を超えたときのドロップフレームとして現れます。GCが30ms続くと、2つの代わりに1つのフレームしか描画されず — ユーザーはインターフェースのスタッタリングを目にします。
長時間ポーズの主な原因:Old Generationの多数の生存オブジェクト、ヒープの断片化、頻繁なFull GC。診断にはAndroid Studio Profilerとsystraceが使用されます。
GCフレンドリーなコードの主なルール は、割り当てられるオブジェクトの数を最小限にすることです。新しいオブジェクトはそれぞれ、メモリ割り当てだけでなく、その後のコレクションも必要とします。GCが高速でも、毎秒1000の余分な割り当ては、コレクタにとって1000のチェックを生み出します。
メモリリーク は、オブジェクトがもはや必要なくなっても到達可能なままの場合に発生します。GCはそのようなオブジェクトを削除できず、メモリは徐々に枯渇します。典型的な原因:未登録のリスナー、Activityへの静的参照、外部コンテキストをキャプチャする匿名クラス、閉じられていないCursor/InputStream。
// メモリリーク:匿名クラスがActivityへの参照を保持
public void startTask() {
new Thread(new Runnable() { // 暗黙的にthis(Activity)を保持
@Override
public void run() {
// 長時間の操作...
System.out.println("Done");
}
}).start();
}
// 修正:静的ネストクラス + WeakReference
private static class TaskRunnable implements Runnable {
private WeakReference<Activity> activityRef;
TaskRunnable(Activity activity) {
this.activityRef = new WeakReference<>(activity);
}
@Override
public void run() {
Activity act = activityRef.get();
if (act != null) {
// Activityでの安全な作業
}
}
}
この例では、匿名のRunnableがActivityへの暗黙的な参照をキャプチャしています。スレッドが生きている限り — ユーザーが画面を閉じても、ActivityはGCによってコレクションされません。WeakReference + 静的クラスによる修正はこのチェーンを断ち切り、Activityの解放を可能にします。
よくある質問
Android(ART)のGCは、メモリが限られたモバイルデバイス向けに最適化された、コンカレントコンパクションを備えた世代別コレクタです。Java GC(G1、ZGC)は、大きなヒープと予測可能なポーズを持つサーバーサイドコレクタです。ART GCはJVMフラグを使用しません — すべてのチューニングはOSレベルで自動的に行われます。
Stop-The-World とは、コレクタがオブジェクトグラフを安全にトラバースしたりメモリを解放したりするために、アプリケーションのすべてのスレッドを一時停止する瞬間です。STWが長いほど、jankが顕著になります。ARTは、その世代別アーキテクチャのおかげで、典型的なSTW時間を2–4msに短縮しました。
Android Studio Memory Profilerを使用します — ヒープの成長、割り当て数を表示し、Heap Dumpを取得できます。詳細な分析には、LeakCanaryを使用します — ライブラリがリークを自動的に検出し、GCコレクションを妨げる参照チェーンを表示します。
Full GC は、Old Generationを含むすべてのヒープ世代の完全なコレクションです。モバイルアプリケーションでは、Full GCは50–200ms続く可能性があり、顕著なjankやANRを引き起こします。主な原因:ヒープの断片化、メモリリーク、Old Generationのしきい値超過。
Kotlinは、構造化された並行性を備えたコルーチンを提供します — スコープのキャンセルにより、すべての子コルーチンが自動的にキャンセルされ、リークが防止されます。Kotlinには、遅延初期化のためのlazyデリゲートや、一時オブジェクトの数を減らすスコープ関数もあります。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。