Java
C++
Android
java8

Java/AndroidとJVM + Heap + Gabage Collectionのお話

More than 3 years have passed since last update.


Java/Androidとメモリのお話

社内LTで発表した資料です。


はじめに

最近AndroidでJava 8導入か?ということで、Jack Compilerのページが公開されたり、Android Nの動向が気になるところでして、社内の勉強会でJava 8とかJack/Jillとかについて発表しようとしていたのですが、そもそも「Dalvik / ARTの違いってなんだっけ?」「そもそもHeapやGCってなんだっけ?」となったので、調べてみました。

良くわかっていなかった箇所を整理するためにも書いています。なので、間違っているところやアドバイスがあれば、質問・指摘・コメントしていただけるとすごく嬉しいです :bow:


Keyword


  • JVM

  • Heap

  • Gabage Collection

  • Android


    • Application Heap

    • Linux Heap ( Native Heap )

    • Dalvik Heap ( Java Heap )

    • ART ( Android Runtime )




Outline


  • Java : 前提知識


    • そもそもJVMとは何か

    • そもそもHeapとは何か

    • そもそもガベージコレクションとは何か



  • Android : DalvikとARTについて


前提知識


Java : そもそもJVMとは何か

そりゃJava書いてるんだし、知らないとやばいでしょ、と思いつつ定義を述べると、


定義:Javaプログラムを実行するためのソフトウェア。


となる。HeapとかClass Loaderとか辛うじて知っている気がしていたけれど、多分知った気になって本質をつかめていない気がしたので、この際改めてまとめてみる。

インストールされているJavaのバージョンを知りたくてjava -versionを実行すると、Javaのバージョン以外にもJRE(Java Runtime Environment : Javaの実行環境)とJVMの情報も出力する。

$ java -version

java version "1.8.0_73"
Java(TM) SE Runtime Environment (build 1.8.0_73-b02)
Java HotSpot(TM) 64-Bit Server VM (build 25.73-b02, mixed mode)

この内"HotSpot"というのがよくわからなかったので調べてみると、


  • Oracleが公開しているJVM(またはJavaコード実行の高速化の技術)

  • C++約25万行で実装されている1

  • 特徴はJITコンパイラ(Just-in-time, a.k.a. Dynamic Transition)


    • ランタイムと並行して実行されるコンパイル方式で、頻繁に実行される箇所、いわゆる「ホットスポット」を分析し、効率的にJavaを実行できるように最適化できる

    • 対象的なのがAOTコンパイル(Ahead-of-time)



で、このHotSpotにおいては「ClientVMモード」と「ServerVMモード」という2つのモードが提供されている。2つの違いについてはこのSOFの議論が詳しい。


  • ClientVM mode


    • 最適化などの機構はなく、単純にコードをコンパイルする

    • より小規模な規模のコードをより早くコンパイルするためのモード



  • ServerVM mode


    • ClientVMとは異なり、プログラム実行速度が最大になるような最適化機能が備わっている


      • 具体的に言うと、最初にインタプリタモード(念のため補足すると、Rubyのirbとかみたいに、一行ずつコードを「解釈」していくこと)でコードを実行して、実行回数などの統計情報を収集し、それをもとにコンパイルしている





(下記図の赤線枠に注目)

Java Platform SE

なぜこんな風に分けていたかというと、『Javaパフォーマンス』によると、もともと個人用PCとサーバーに使用するようなマシンではパワーに有意な差があったため。しかし、CPU性能の向上によって、今では個人用PCでもServerVMが普通に使われるようになってきているとのこと。だから、自分のPCでjava -versionした上記の例でも「Server VM」と表示されている。

また、Java SE7以降、「階層型コンパイル(Tiered Compilation)」という仕組みが導入されている。コンパイル時に-XX:+TieredCompilationフラグを使用することでONにできる。

一言で言うと、ClientVMとServerVMのいいとこを組み合わせてより早いパフォーマンスを実現している。具体的に言うと、もともとはServerVMが最初にインタプリタモードでコードの統計情報を収集していたのだが、この過程にインタプリタモードよりはるかに高速らしいClientVMを使うことによって、全体的なパフォーマンスを上げているとのこと。ClientVMとServerVMを段階的に使用しているから、「階層型コンパイル」と呼ぶようだ。

なお、Java SE8では、デフォルトでこの階層型コンパイルをONにしているらしい。

(ちなみに、『Javaパフォーマンス』ではクライアントモード、サーバーモード、階層型コンパイルそれぞれのパフォーマンスを実験しているチャプターがある。ここでは省略。)


そもそもHeapとは何か

JVMの話はこれくらいにして、次に「Heap」とは何かについて調べてみる。


定義:JVMが作成するメモリ領域のこと


...というのは知っていたけど、下記の図のように世代に分かれていることを今回はじめて知った。

heap

主に以下の階層に分かれている。


  • Young


    • Eden

    • Survivor


      • from

      • to


        • ※ S0/S1領域がそれぞれを交互に入れ替える







  • Old (a.k.a. Tenured)

  • Permanent2

ヒープの初期値や最大値を細かく設定するためのフラグも用意されているらしい。使ってみたけどよく分からなかったのだが、パフォーマンスエンジニアは色々なフラグを試したりするのだろうか。

Xms512m -Xmx512m -XX:NewSize=128m -XX:MaxNewSize=128m -XX:PermSize=64m -XX:MaxPermSize=64m

フラグ
説明

-Xms
ヒープ全体の初期値

-Xmx
ヒープ全体の最大値

-XX:NewSize
New 領域の初期値

-XX:MaxNewSize
New 領域の最大値

-XX:PermSize
Permanent 領域の初期値

-XX:MaxPermSize
Permanent 領域の最大値


そもそもガベージコレクションは何か


理論編(1)


定義:不要になったメモリ領域を自動的に解放する機能。Javaで言うと、例えば参照されなくなったオブジェクトが使っていたメモリ領域を自動で判断して解放する機能


...というのは知っていたけど、それ以上はよく分からなかった。先ほどのHeapの各領域に即していうと、単純に「参照されなくなったオブジェクトが破棄される」にも、細分化されたプロセスがあることを知った。

gabage colleciton


  1. まず、newされたJavaオブジェクトはとりあえず全てEden領域に突っ込まれる

  2. 不要なオブジェクトがEden⇒From⇒To(New領域内)へと移行する(これを"Minor GC"と言う)


    • このとき、"Full GC"と呼ばれる、「Heapが満杯になったら一気にGCする」方式が取られる。GCにはG1などの種類もあるが、このMinor Gabageは基本的にConcurrent方式(GCと処理のスレッドが別々)ではなくFull方式(必ず処理がストップする)



  3. New領域からOld領域に移行する(これを"Major GC"と呼ぶ)


    • 上図で"Objects older than 15GC cycles"と書かれているが、これは参照カウント(reference counting)というアルゴリズムを用いた場合の例図であるため



となる。そもそもガベージコレクションの機構には


  • Minor GC (a.k.a. Scavenge GC) : Young領域(特にEden領域)をGCすること


    • New領域のみ対象、短期間で終了



  • Major GC : Old領域をGCすること


    • New + Old領域にて実施される、Minorよりは低い頻度で



という二種類があることも知らなかった。


理論編(2)

JavaのGCのアルゴリズムにはいくつか種類があるのだが、少し深掘ってみる。HotSpot JVMに実装されているメインの4つのアルゴリズムを、基本の順から紹介。


0. 用語

まず抑えておきたいのは以下の2つの用語。


  • Mark : 参照されていないオブジェクト(=生きているオブジェクト)を探す

  • Sweep : 参照されていないオブジェクト(=生きているオブジェクト以外)を削除する

また、Sweepによってオブジェクトを削除したとしても、隙間が多く新しいオブジェクトを格納しづらいため、


  • Compact : 空き領域をつなげる

という仕組みも働く。


1. Serial GC


  • GCを一つのスレッドで行う、最もシンプルなロジック


    • GCを行う時は他のスレッドを用いない、すなわちGC中は処理は完全停止状態

    • シングルコア環境ではデフォルトで使われるが、マルチコア化が進んできた現在、使われる場面はあまり多くない




2. Parallel GC


  • GCを複数のスレッドで並行して行う


    • 並行で行うとはいえ、GCスレッド間の同期の必要があり、完全無停止とは行かない




3. CMS (Concurrent Mark and Sweep) GC


  • 正式名称は"Mostly Concurrent Mark and Sweep Garbage Collector"

  • 2つのMarkが存在する


    • Initial Mark : Parallel GCでGCスレッド間の同期を行う際のMarkと同じ

    • Concurrent Mark : 他スレッドが処理を実行中にMarkを行う


      • それらが更に「Remark」段階で再チェックされる


        • 個人的にはこの停止を"Stop-the-world pause"と呼ぶのが好き







  • メリットは、「Initial」段階と「Remark」段階でしかアプリケーションが停止しないこと

  • デメリットはSerial GCやParallel GCよりCPUが必要であること

cms gc


4. G1 (Gabage First) GC


  • 「G1は長期的には、並行マークスイープ・コレクタ(CMS)の後継となる技術」3

  • ポイントを一言で言うと、「CMS同様リアルタイムに動作するGCが、リージョン化によってより粒度の細かいレベルで動作させる」


    • Heapをさらに「リージョン」と呼ばれる小さい領域に分割する

    • 今まで説明したYoung/Old領域間のオブジェクトの移動は一旦忘れて考えたほうが良い




コード編

まず、System#gc()を見てみる。一言で言うと、「justRanFinalization=trueの時だけGCを走らせて」いる。


/**
* Indicates to the VM that it would be a good time to run the
* garbage collector. Note that this is a hint only. There is no guarantee
* that the garbage collector will actually be run.
*/

public static void gc() {
boolean shouldRunGC;
synchronized(lock) {
shouldRunGC = justRanFinalization;
if (shouldRunGC) {
justRanFinalization = false;
} else {
runGC = true;
}
}
if (shouldRunGC) {
Runtime.getRuntime().gc();
}
}

では、Runtime#gc()は何しているかというと。。。


/**
* Indicates to the VM that it would be a good time to run the
* garbage collector. Note that this is a hint only. There is no guarantee
* that the garbage collector will actually be run.
*/

public native void gc();

nativeメソッドだった。今の技術力だと詰んだ。ここから先はまだ調べられていない。

ちなみに、Object#finalize()という、全オブジェクトが持っているメソッドがある。これは、そのクラスのインスタンスがGCによってもう使われていないと検知された時に実行するメソッド。オーバーライドして使う。


Classical use of finalizer are to de-allocate system resources when an object is garbage collected, e.g. release file handles, temporary files, etc. ( -> Reference )



Android : DalvikとARTについて

android and memory management


  • Linux Heap ( AndroidはベースがLinuxなので )


    • Application Heap ( アプリケーションごとのHeap領域 )


      • Native Heap ( ベースのLinuxが管理しているHeap領域 )


        • この領域が少なくなるとActivity#onLowMemory()が呼ばれ、更にある一定の閾値を超えるとLowMemoryKillerが実行されアプリが強制終了される



      • Dalvik Heap ( DalvikVMが管理しているHeap領域 )


        • Linux Heapからアプリごとに振り分けられる

        • 上限を超えるとOutOfMemoryが発生する

        • Android Studioのツールとかで確認しているのは、このDalvik Heap







なお、Android 4.4 KitKatよりAndroid Runtime(ART)がDalvikに替わるランタイム環境として使用(5.0以降はデフォルト)されている。開発者モードで切り替えられる。


DalvikとARTの共通点

ようやく(?)本題。


  • どちらもDexバイトコード(JVMがJavaバイトコードを使うようなもの)を利用している点では同じ


    • つまりDalvikに対してARTが後方互換性がある




DalvikとARTの違い


ART : Ahead-of-timeコンパイル

ARTは、コンパイルにJITではなくAOT方式を導入


  • DalvikVMにとって、実行時のコンパイルの必要を無くし、ソースコード(Javaコード)から中間言語コード(Javaバイト、Dexバイトコード)を生成した後に、ネイティブの機械語コードも生成してしまうので、アプリケーションの実行中の性能を早めることができる


ART : GCのアルゴリズムの違い

https://source.android.com/devices/tech/dalvik/


  • GC pause (Stop-the-world pause?)の回数が2回から1回に半減


  • GC_FOR_MALLOC(Heapが満杯になってしまった時に、メモリを再度割り当てる処理 = アプリの処理が停止してしまう)が滅多に起こらないよう工夫


    • "making concurrent garbage collections more timely"とあるが、詳細は不明



  • GC自体をコンパクトに


ARTのGCの仕組み

https://source.android.com/devices/tech/dalvik/gc-debug.html


  • デフォルトはCMS


最近発表されたJackとは?

以下にすでに詳しいQiita記事がありました。時間もなくそこまで調べられていないので、リンクだけ。

http://qiita.com/calciolife/items/faf6affc0f8cf70c4af9


終わりに

奥が深い。。。C++勉強して、nativeメソッドとか読めるようになりたいです。


参考資料


書籍


Javaパフォーマンス

この本が結構詳細に書いてあって参考になりました。まだ難しい箇所も多いのですが、Heapやコンパイラ、GCの仕組みを理解することが出来ました。


他参照リンク