JIT(Just-In-Time)は、バイトコードまたはプログラムの中間表現を実行中に直接マシン命令に変換する動的コンパイル技術です。Androidでは、JITコンパイラはバージョン2.2 FroyoでDalvik仮想マシンの一部として初めて登場し、アプリケーションの実行を2–5倍高速化しました。Google、2024年によると、ARTの最新JITは、インタープリタ実行とホットメソッドのプロファイルベースコンパイルを組み合わせています。
主要ポイント
Just-In-Time(JIT)は、ソースコードまたはバイトコードを事前に(AOTのように)ではなく、プログラムの該当セクションが最初に呼び出された時点でマシン命令に変換するコンパイル方法です。「Just-In-Time」という用語は、コンパイルが実行の直前に「ちょうど間に合うように」行われることを意味します。
JITの概念は1960年代から存在していましたが、1995年に Java仮想マシン の登場により広く採用されるようになりました。JITは、バイトコードの移植性(一度書けばどこでも実行可能)とネイティブコードに近いパフォーマンスを組み合わせることを可能にします。Java HotSpot VMでは、JITコンパイラが実行コードを分析し、最も重要なセクションのみをコンパイルして、時間とメモリを節約します。
JITコンパイラは、入力としてバイトコードを受け取り、それを解釈しながら並行して統計情報を収集します。コードの特定のセクション(メソッド、ループ)が十分に頻繁に呼び出されると、JITはそれをコンパイルすることを決定します。コンパイルされたマシンコードはキャッシュに保存され、後続の呼び出しでは既にコンパイルされたバージョンが使用されます。これにより、プログラム全体をコンパイルすることなく高速化が実現します。
// 例:メソッドが複数回の呼び出し後にホットになる
public class HotMethod {
private int compute(int n) {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += i * i;
}
return sum;
}
}
// ループ内で500回呼び出し — JITがcomputeをコンパイル
for (int t = 0; t < 500; t++) {
hot.compute(1000);
}
Androidでは、JITコンパイルは3つの進化段階を経ました。第1段階 — JITなしのDalvik(Android 1.0–2.1):DEXバイトコードの純粋な解釈。第2段階 — JITありのDalvik(Android 2.2–4.4):JITコンパイラの導入により、アプリケーションが2–5倍高速化。第3段階 — ハイブリッドJITを搭載したART(Android 7.0+):新たな能力でのJITの復活。
DalvikのJITは、トレースベースコンパイラとして実装されました。個々のメソッドではなく、頻繁に順次実行される命令の連鎖(トレース)を分析しました。これにより、複数のメソッドを含む実行パス全体をコンパイルできました。このアプローチは、命令キャッシュが小さい モバイルプロセッサ に効果的で、コンパイルされたトレースがL1キャッシュに収まりました。
Android 7.0 Nougat以降、ARTは メソッドベースJIT を使用しています。実行プロファイルに基づいて個々のメソッドをコンパイルします。このJITはDalvik JITよりも大幅に高速で、1メソッドあたりの標準コンパイル時間は0.5–1ミリ秒で、Dalvikの3–5ミリ秒と比較されます。コンパイルされたコードはアプリケーションヒープではなく、別のメモリ領域(JITコードキャッシュ)に保存され、断片化を低減します。
| パラメータ | Dalvik JIT | ART JIT |
|---|---|---|
| タイプ | トレースベース | メソッドベース |
| コンパイル速度 | 3–5 ミリ秒/メソッド | 0.5–1 ミリ秒/メソッド |
| コンパイルしきい値 | 約200回の呼び出し | 動的 |
| コードキャッシュ | アプリケーションヒープ内 | JITコードキャッシュ |
| プロファイリング | 内部 | 外部.profファイル |
JITの中心的なメカニズムは ホットメソッドの検出 です。メソッドが呼び出されるたびに内部カウンタが増加します。カウンタがしきい値を超えると、メソッドは「ホット」としてマークされ、コンパイルに送られます。Dalvikでは、しきい値は固定(約200回の呼び出し)でした。ARTでは、カウンタはデバイスの利用可能なリソースに応じて動的に設定されます。
コンパイルプロセスには複数のフェーズが含まれます。第1 — バイトコード分析:JITが命令ストリームを検査し、データフローグラフを構築します。第2 — 最適化:小さなメソッドのインライン化、デッドコードの削除、定数畳み込み。第3 — コード生成:最適化されたグラフを特定のCPUアーキテクチャ(ARM、ARM64、x86)のマシン命令に変換します。
// インライン化のデモ — JITがメソッド本体をインライン化
public int inlineExample() {
return square(5);
}
private int square(int x) {
return x * x;
} // JITが呼び出しをreturn 5 * 5;に置き換え
JITの特別な技術 — オンスタック置換(OSR)。メソッドに数百回の反復で終了しない長いループが含まれている場合、JITはループを「オンザフライ」でコンパイルし、実行中に解釈版をコンパイル版に置き換えることができます。OSRは、レンダリング、画像処理、暗号化などの計算タスクに特に効果的です。
JITとAOTは、相反するトレードオフを持つ2つのコンパイルアプローチです。JITは、コンパクトな配布サイズと適応性のために初回起動速度を犠牲にします。AOTは、最初の瞬間から最大のパフォーマンスを得るためにインストール時間とディスク容量を犠牲にします。どちらのアプローチが絶対的に優れているわけではなく、選択はシナリオに依存します。
JITの主な利点は 適応的最適化 です。JITはAOTでは利用できないプロファイル情報(正確なオブジェクトタイプ、実際の呼び出し頻度、実際の分岐パターン)を使用できます。これにより、静的コンパイルでは不可能な積極的な最適化を適用できます。例えば、JITは実際に1つのレシーバタイプのみが見つかった場合、メソッド呼び出しをデバーチャライズできます。
| 基準 | JIT | AOT |
|---|---|---|
| インストール時間 | 即時 | サイズに依存 |
| 初回起動 | 遅い(ウォームアップ) | 高速 |
| ディスク容量 | 最小 | +15–30% |
| 適応性 | 高い | 低い |
| CPU使用率 | コンパイル時にスパイク | 安定 |
JITコンパイルは、迅速な展開とディスク容量の節約が重要な場合に適しています。モバイル開発の文脈では、JITは頻繁に更新されるアプリケーション(A/Bテスト、ホットフィックス)に最適です。JITは開発段階でも便利で、コードが1日に何十回も再構築される場合、コンパイルで節約された毎秒がフィードバックループを高速化します。
JITは開発者にいくつかの実用的な利点を提供します。第1に — APKサイズが小さいこと。JITアプローチでは、APKにはバイトコード(DEX)のみがパッケージされ、コンパイルされたネイティブコードよりも20–30%少ない容量で済みます。内蔵ストレージが限られているユーザーにとって、これは大きな利点です。
第2の利点は デバイス適応 です。JITは実際のCPUアーキテクチャ、RAM容量、現在の負荷を考慮してコードをコンパイルします。例えば、2GB RAMのデバイスでは、JITはメモリを節約するために控えめにコンパイルし、12GBのフラッグシップでは可能な限りの最適化を適用できます。一方、AOTコンパイルはインストール時に決定を固定します。
バイトコードはプラットフォームに依存せず、アプリケーションの配布を簡素化します。1つのAPKがARM、ARM64、x86デバイスで動作し、JITが各アーキテクチャのネイティブコードを生成します。AOTアプローチでは、APKに複数のネイティブコードバリアントを含める(サイズ増加)か、アーキテクチャごとに個別のバージョンをコンパイルする必要があります。
JITの主な欠点は ウォームアップ遅延 です。JITがホットメソッドをコンパイルしている間、ユーザーはアプリケーションの最初の数秒間で遅延を体験します。ゲームでは、初期レベルでのスタッタリングとして現れます。アニメーションのあるアプリケーションでは、画面間の最初の遷移でカクつきが生じます。
第2の欠点 — 消費電力。コンパイルプロセスはCPUに大きな負荷をかけ、ウォームアップ期間中の消費電力を10–20%増加させます。バッテリー駆動のデバイスでは、これによりバッテリー寿命が短縮されます。これは、アプリケーションの頻繁な再起動(メモリが限られたマルチタスクでシステムがプロセスをアンロードしてリロードする)シナリオで特に顕著です。
もう1つの問題 — JITキャッシュの断片化。コンパイルされたコードは連続したメモリ領域に保存されます。新しいクラスがロードされ、追加のメソッドがコンパイルされると、キャッシュが断片化され、メモリ管理のオーバーヘッドが増加します。Dalvikでは、この問題は定期的なキャッシュクリアによって解決されていました。ARTでは、JITキャッシュはヒープとは別に割り当てられ、独自のデフラグメンテーション戦略を使用します。
ARTの最新アプローチ — ハイブリッドコンパイル は、JITとAOTの両方の長所を組み合わせます。アプリケーションのインストール中は、コンパイルは実行されず、バイトコードの検証(verify)のみが行われます。これにより、高速なインストールと最小限の容量使用が保証されます。最初の起動は、ホットメソッドのJITコンパイルとともにインタープリタモードで実行され、ユーザーは長い待ち時間なしに許容可能なパフォーマンスを得られます。
同時に、バックグラウンドプロファイラが実際の使用状況に関するデータを収集します。2–3回の完全なアプリケーション起動後、プロファイルが十分な完全性に達すると、システムはdex2oatを実行してホットメソッドをネイティブコードにコンパイルします。この操作は、デバイスに負荷がかかっていないとき(充電中、画面オフ)にバックグラウンドで実行されます。バックグラウンドAOTが完了すると、アプリケーションは完全なAOTコンパイルに匹敵するパフォーマンスを達成します。
# バックグラウンドコンパイルの強制開始
adb shell cmd package compile -m speed-profile -f com.example.app
# コンパイルステータスを表示
adb shell cmd package dump-profiles com.example.app
Google I/O 2017によると、ハイブリッドコンパイルにより、純粋なAOTと比較してアプリケーションのインストール時間が30–50%短縮されました。システムパーティションの占有ディスク容量は20–30%減少しました。同時に、バックグラウンドコンパイル後のパフォーマンスは完全なAOTのレベルに一致します。ハイブリッドがAOTに劣る唯一のシナリオは、インストール直後の初回起動です。アプリケーションはJITモードで動作し、10–15%遅くなる可能性があります。
よくある質問
JITは、コードを事前にではなく、実行中に部分的にマシン言語に変換することでプログラムを高速化する方法です。最も頻繁な部分はコンパイルされてキャッシュされ、まれな部分は元の形式のまま残ります。
JITは実行中にコードをコンパイルし、容量を節約してインストールを高速化します。AOTはすべてのコードを事前にコンパイルします — アプリケーションの起動は速くなりますが、より多くのディスク容量とインストール時間が必要です。
JITは削除されたのではなく、進化しました。Android 5.0では、JITを搭載したDalvikが純粋なAOTを搭載したARTに置き換えられました。Android 7.0では、JITはハイブリッドシステムの一部としてARTに戻り、バックグラウンドのAOTコンパイルと連携して最適なパフォーマンスを実現します。
JITはCPU負荷により、ウォームアップ期間中の消費電力を10–20%増加させます。ホットメソッドのコンパイルが完了すると、消費電力は通常レベルに戻ります。ARTのハイブリッドモードは、バックグラウンドコンパイルによってこれらのスパイクを最小限に抑えます。
はい、集中的な計算を伴うシナリオでは認識されます。ユーザーはアプリケーション操作の最初の数秒間やゲームの開始時に 遅延 に気づくことがあります。最新バージョンのAndroid(8.0+)では、ハイブリッドモードがプロファイルベースのコンパイルによりこの影響を最小限に抑えます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。