1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Android/CS基礎 - メモリ管理③】ARTとGCの実装 - 最適化とリーク対策

Posted at

「Android/CS基礎 - メモリ管理」シリーズのPart3です。
先に、Part2を読むことを推奨します。
また、本記事は私がAndroidのメモリ管理やGCについて学んだ内容をまとめたものです。誤りや改善点がありましたら、コメントでご指摘いただけると嬉しいです🙇‍♂️

この記事で理解できること

  • ARTとは
  • 最新のARTにおけるGCアルゴリズムについて
  • メモリリークパターンとその対策

ART (Android Runtime) とは

概要

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

Dalvik VMとARTの比較

項目 Dalvik VM ART
コンパイル方式 JIT(実行時) AOT+JIT(事前+実行時)
コンパイルタイミング アプリ起動時・実行中 インストール時 + アイドル時
起動速度 遅い 速い
ストレージ使用量 少ない(DEXのみ) 多い(DEX + コンパイル済みコード)
バッテリー消費 大きい 小さい
GCアルゴリズム Mark & Sweep中心 Concurrent Copying GC
GC一時停止時間 長い(数十〜数百ms) 短い(数ms以下)
メモリ圧縮 なし あり(断片化解消)
世代別GC 限定的 完全実装
導入時期 Android 1.0〜4.4 Android 5.0以降

Dalvik VMは現在ではほとんど使われていないため、詳細は省略します。

ARTの目的

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

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

パフォーマンスの向上

  • アプリの起動速度を高速化
  • 実行速度の向上
  • UI操作のレスポンス改善(GC停止時間の短縮)

バッテリー効率の改善

  • 実行時コンパイルの削減 → CPU使用率低下
  • 効率的なメモリ管理 → GC頻度の削減

メモリ管理の最適化

  • 高度なGCアルゴリズムの導入(後述)
  • メモリ断片化の解消
  • メモリ使用効率の向上

ARTの役割

ARTはAndroidにおいて以下の役割を担っています。

アプリケーションの実行基盤

  • DEXバイトコードのコンパイル(AOT/JIT)
  • Java/Kotlinコードの実行
  • メモリ管理(GC、ヒープ管理)

HAL層を経由したハードウェア操作

449f3d0a2cf4-20230801.png

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

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

例:アプリでQRコードを読み込む場合

[アプリコード (Kotlin/Java)]
  ↓ ARTで実行
[JNI経由でネイティブコード呼び出し]
  ↓ HAL層経由
[カメラハードウェア制御]

System Services層との連携

System Servicesは、アプリの起動管理、通知、位置情報など、システム全体を管理する特権プロセス群です。

重要なポイント:

  • System ServicesはARTの上で動作するJavaプロセス
  • System ServicesもARTに依存して実行される

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

最新のARTにおけるGCアルゴリズムについて

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

従来のGCアルゴリズムの課題

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

欠点1. STW時間が長い

アルゴリズム実行中、アプリはSTW(Stop The World)という状態に変化します。
これはその名の通り、アプリ自体が停止するのです。(某アニメに登場するザ・ワールドを使うスタンドみたいなやつ)

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

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

欠点2.メモリの断片化

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

svgviewer-png-output.png

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

現代のGCアルゴリズムについて

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

Phase1. 初期マーク

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

Phase2. 並行マーク

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

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

Phase3. 並行コピー

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

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

Phase4. 最終処理

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

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

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

メモリリークパターン

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

Singletonでアクティビティ保持

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

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

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

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

Companion object で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の変化にも着いていきやすくなると思います!!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?