2026年1月19日

【Android/CS基礎 – メモリ管理③】ARTとGCの実装


Content
こんにちは、はるです!
今回はAndroidにおける基礎の一種であるメモリ管理についてお話ししていこうと思います。
※ 本記事はパート2の内容を前提としています。
まだ読んでいない方は、先にパート2をご覧ください。

⭐️ この記事で理解できること
・ARTとは
・最新のARTにおけるGCアルゴリズムについて
・メモリリークパターンとその対策

ART (Android Runtime) とは

⭐️概要
ART (Android Runtime) は、Android 5.0以降からAndroid端末に搭載されているランタイム環境です。
Android 5.0以前はDalvik VMという環境が使われていましたが、パフォーマンス向上のためARTに置き換えられました。
Dalvik VMは現在ではほとんど使われていないため、詳細は省略します。

⭐️ARTの目的
ARTの目的は、Androidアプリのパフォーマンスと効率性を向上させることです。

具体的には以下の観点から改善を図っています:

✅パフォーマンスの向上
・アプリの起動速度を高速化
・実行速度の向上
・UI操作のレスポンス改善(GC停止時間の短縮)
✅バッテリー効率の改善
・実行時コンパイルの削減 → CPU使用率低下
・効率的なメモリ管理 → GC頻度の削減
✅メモリ管理の最適化
・高度なGCアルゴリズムの導入(後述)
・メモリ断片化の解消
・メモリ使用効率の向上

⭐️ARTの役割
ARTはAndroidにおいて以下の役割を担っています。
✅アプリケーションの実行基盤
・DEXバイトコードのコンパイル(AOT/JIT)
・Java/Kotlinコードの実行
・メモリ管理(GC、ヒープ管理)
✅HAL層を経由したハードウェア操作

これはAOSP (Android Open Source Project) と呼ばれるAndroid OSのアーキテクチャ図です。
水色のレイヤーがARTで、その上下にSystem Services層とHAL層が存在します。

HAL (Hardware Abstraction Layer) とは、ハードウェア操作を抽象化したレイヤーです。
ARTはJNI (Java Native Interface) を通じてネイティブコードを実行し、HAL層経由でハードウェアにアクセスします。

✅System Services層との連携
System Servicesは、アプリの起動管理、通知、位置情報など、システム全体を管理する特権プロセス群です。
重要なポイント:
・System ServicesはARTの上で動作するJavaプロセス
・System ServicesもARTに依存して実行される

ARTはSystem Servicesを含む、すべてのJava/Kotlinコードの実行基盤となっています。

従来のGCの課題

現代のARTでは「Concurrent Copying GC」というアルゴリズムが採用されています。

⭐️従来のGCアルゴリズムの課題
前回、GCアルゴリズムの基本は「Mark & Sweep方式」ということを学びました。
しかし、これには様々な欠点がありました。

❌欠点1. STW時間が長い
アルゴリズム実行中、アプリはSTW(Stop The World)という状態に変化します。
これはその名の通り、アプリ自体が停止するのです。

一見、「まさかメインスレッドで処理してるのか?」と引っかかりますが実際は違います。(専用のGCスレッドで処理)
停止しないと、GC実行中にコードが実行されてさらに参照を変えられるとGC実行時点でのオブジェクトグラフと実体の依存関係の整合性が取れなくなるのです。

こうなると深刻です。
不要なのにマークされるのでメモリリークは発生するし、GC側はまだオブジェクトが存在するものとして扱ってるので何かしらの拍子にアクセスしたら実はnullでNPEクラッシュというオチがありえます。
ですから、STW状態に遷移する必要があるのです。
ただし、STW時間が長いと当然画面は止まるのでUXは最悪です。
なので、話の焦点は「どうやればSTW時間を短くできるか」に移動するというわけです。

❌欠点2.メモリの断片化
Mark & Sweepアルゴリズムによりオブジェクトが解放されたとしましょう。
しかし、そうなると問題が出てきます。
それは利用可能メモリの断片化(フラグメンテーション)が発生することです。

このように中途半端にメモリが解放されるので、実際は使えるのにサイズが合わず利用されないメモリ領域などが発生してしまうことも問題でした。

現代のGCアルゴリズム(Concurrent Copying GC)について

これらの欠点を補うために考案されたアルゴリズムが「Concurrent Copying GC」アルゴリズムです。
このアルゴリズム全体は4つのフェーズに分解され、下記のフローに沿って進行します。

✅Phase1.初期マーク

初期マークではそれぞれのGC Rootから直接参照できるノードのみをマークします。
マークされたノードははPhase2にて走査処理の起点とされます。

✅Phase2.並行マーク

並行マークでは、GCスレッドを使ってPhase1でマークしたノードを起点に順番に走査が走ります。
しかし、これはアプリスレッド(メインスレッド)ではないため、UXは低下しないのです。

Write Barrierも非常に重要な役割です。
マーク処理の最中に参照が変わったらオブジェクトグラフと実体が乖離して大問題となります。
これを防ぐのがWrite Barrierです。
Write Barrierは参照が書き換えられたノードに対し印を付与します。
Phase4で当該ノードは整合を保つため再走査します。

✅Phase3.並行コピー

並行コピーでは、Phase2までの段階で到達可能と判断されたノードを新たなスペースへコピーします。
こちらもGCスレッドで行うため、UXへ影響を及ぼしません。
本作業を行う目的は空きメモリの断片化によるメモリ不足(フラグメンテーション)を防ぐためです。
簡潔に言えば、空きスペースと使用スペースをしっかり区切ってヒープ領域を最大限活用しよう、ということですね。

では、新しいスペースへコピーしたあと、アプリが古いスペースへアクセスしてデータを取ろうとした場合どうなるかというと、転送ポインタという情報を辿ってしっかり新しいスペースまで導いてくれます。
転送ポインタは、データのお引越し先を記してくれたものです。
この役割をRead Barrierと言います。

✅Phase4.最終処理

最後ですね。
ここでは、Write / Read Barrierの精算を行います。具体的には、
Write Barrier:Phase2で印をつけたノードを新しいスペースへコピー
Read Barrier:Phase3で新しいスペースに移行したにも関わらず、まだ古いスペースのポインタを参照している箇所があればそれらを新しいスペースへのポインタに書き換え。そして古いスペースは解放する。

Read Barrierの精算の際、必ずSTWが発生します。
なぜならアプリが動いていると運が悪い場合に古いスペースのポインタを参照しないようにと書き換える前に参照処理が走ってしまう可能性が考えられるからです。そうなると、解放されたポインタ(つまりアクセスしてはいけない領域)にアクセスしてしまいアプリクラッシュの発生元になります。

ここまでの流れを通して1回分のGCが完了するというわけですね。

メモリリークパターン

メモリリークパターンを2つ紹介します。
本当はもっとあるのですが紹介しきれないので各自で調べてみてください。

❌Singletonでアクティビティ保持


class MyActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Singleton.activity = this // リーク!
}
}

// ❌ NGパターン
object Singleton {
var activity: Activity? = null
}

// ✅ OKパターン
object Singleton {
var activityRef: WeakReference? = null
}

解説:
Part2で説明したようにstatic領域はGC Rootになります。
そうすると、activityデータがnullにならない限り、staticのGC Rootからアクセスできてしまうからです。
つまり、到達可能フラグがONになるので回収されずリークします。
そこで、弱参照を用いることで参照元であるActivity自体が消えたら自動で回収されるようにすればリークを防げます。

❌companion object でactivity context保持


// ❌ NGパターン
class MyUtil {
companion object {
private var context: Context? = null

fun init(context: Context) {
this.context = context // ActivityのContextを渡すとリーク
}
}
}

class MyActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
MyUtil.init(this) // リーク!
}
}

// ✅ OKパターン
class MyUtil {
companion object {
private var appContext: Context? = null

fun init(context: Context) {
this.appContext = context.applicationContext // ApplicationContextを使う
}
}
}

解説:
companion object = staticなオブジェクトです。
そこに対し、Activityが生存している間しか使えないActivityContextを引き渡せば、先ほどと同じ原理でリークします。
そのため、ApplicationContextを渡すようにしましょう。
こちらは、アプリが生存している間はずっと使えるためリークに繋がりません。

まとめ

いかがでしたでしょうか。
ここまで、かなり長かったですね。しかしここを押さえるとメモリリークが発生した場合の対処方法が、より理論立てて検討できるようになったのではないでしょうか?
Java / Kotlinはランタイムがメモリ管理を隠蔽してくれる素晴らしい言語ですが、その内部を知ってるかどうかでメモリリークに対する意識一つとっても大きく変わるはずです。
現に、この記事を読む前と後ではみなさんのメモリ管理に対する意識も変化しているのではないでしょうか。

近年、さまざまなフレームワークやライブラリが登場しています。
一方で、プロセスやスレッド、GCなどの基礎(Fundamental)は不変です。基礎を抑えることで今後のITの変化にもついていきやすくなると思います!!

We are hiring!

現在、システムサポート株式会社ではAndroid/iOSエンジニアを募集しております。
一緒にモバイルアプリチームを発展していく仲間を心よりお待ちしておりますので、何なりとお問い合わせください!

キャリアサイト: コチラ
doda / リクルートなどでも公開しておりますのでご確認ください。

  • 必須要件
    • Swift、Kotlinいずれかを用いたモバイルアプリ開発の経験
  • 歓迎条件
    • Webアプリケーション開発経験をお持ちで自己研鑽にてSwift、Kotlinを用いたモバイルアプリ開発を経験されている方
  • 魅力
    • MAU200k+を誇る規模のモバイルアプリ開発なども行えるSIerとしては珍しいtoC向けアプリもあります!
    • また、DroidKaigiやiOSDCへの参加なども積極的に推奨しているため技術力向上も狙えちゃいます!
    • リモートワーク+フルフレックスなどWLBも充実してますので是非是非ご応募ください!

2026年1月19日 【Android/CS基礎 – メモリ管理③】ARTとGCの実装

Category モバイル

ご意見・ご相談・料金のお見積もりなど、
お気軽にお問い合わせください。

お問い合わせはこちら