1. ホーム
  2. Android

Dalvik仮想マシンと学習プランの簡単な紹介

2022-02-12 04:07:50

        ご存知のように、AndroidアプリはDalvik仮想マシンの内部で動作し、各アプリはDalvik仮想マシンの個別のインスタンスを持っています。命令セットとクラスファイル形式が異なることを除けば、Dalvik VMはJava VMと多かれ少なかれ同じ機能を共有しています。例えば、両方ともインタプリタ実行で、ジャストインタイム(JIT)、ガーベッジコレクション(GC)、Java Native Method Invocation(JNI)、Java Remote Debugging Protocol(JDWP)をサポートしています。この記事では、Dalvik仮想マシンの簡単な紹介と、その学習計画について説明します。

ラオ・ルオの新浪微博。 http://weibo.com/shengyangluo とフォローを歓迎します

書籍「"Androidソースコードシナリオ解析"」が、着任プログラマーズサイト( http://0xcc0xcd.com をクリックすると入場できます。

        Dalvik仮想マシンは、以下のように構成されています。 ダン・ボーンステイン かつて彼の祖先が住んでいたアイスランドの同名の小さな漁村にちなんで名付けられたDan Bornstein氏によって開発されたDalvik仮想マシンの起源は アパッチハーモニー は、Apache Software Foundationが主導するプロジェクトで、スタンドアロンでJDK 5互換の仮想マシンを実装し、Apache License v2の下で公開することを目的としていました。このように、Dalvik仮想マシンは作成された当初からJavaと暗黙の了解のような関係を持っています。

        Dalvik仮想マシンとJava仮想マシンの最も大きな違いは、それぞれクラスファイルの形式と命令セットが異なることで、Dalvik仮想マシンはdex(Dalvik Executable)形式のクラスファイルを使用し、Java仮想マシンはclass形式のクラスファイルを使用します。dexファイルには複数のクラスが格納できるが、クラスファイルには1つのクラスしか格納できない。dexファイルは複数のクラスを含むことができるため、各クラスで繰り返される文字列やその他の定数を1回だけ保存することで容量を節約し、メモリやプロセッサ速度に制限のあるモバイルシステムでの使用に適している。一般に、同じクラスを含む非圧縮のdexファイルは、圧縮されたjarファイルよりわずかに小さくなります。

        Dalvik VMはレジスタベースの命令を使用し、Java VMはスタックベースの命令セットを使用します。スタックベースの命令はコンパクトで、例えばJava VMが使用する命令は1バイトしか取らないため、バイトコードと呼ばれる。レジスタベースの命令は、ソースアドレスとデスティネーションアドレスを指定する必要があるため、より多くの命令領域を必要とする。例えば、Dalvik仮想マシンの一部の命令は2バイトを必要とする。スタックベースとレジスタベースの命令セットには、それぞれメリットとデメリットがある。一般に、前者は同じ機能を実行するために多くの命令(主にロード命令とストア命令)を必要とし、後者はより多くの命令空間を必要とする。命令数が多いということはCPUの処理時間が増えるということであり、命令領域が多いということはデータキャッシュ(d-cache)が故障しやすくなるということである。

        また、スタックベースの命令はターゲットマシンのレジスタを仮定しないので、移植性が高いという議論もあります。しかし、レジスタベースの命令は、ターゲットとなるマシンのレジスタを仮定しているため、より移植性が高いのです。 AOT (ahead-of-time)最適化を行います。AOTとは、解釈言語のプログラムを実行する前に、ネイティブの機械語プログラムにコンパイルすることを意味します。AOTは基本的に静的コンパイルであり、JITと相対するもの、つまり前者はプログラムを実行する前にコンパイルし、後者はプログラムを実行しながらコンパイルするものである。ランタイムコンパイルは、実行時の情報を利用することで、スタティックコンパイルよりも最適化されたコードを得ることができるが、最適化処理に時間がかかりすぎるため、特定の高度な最適化が行えないことを意味する。一方、プリランタイムコンパイルは、プログラムの実行時間を取らないため、時間的コストに関係なくコードの最適化を行うことができる。AOTでもJITでも最終的な目的はインタプリタ言語をレジスタベースのネイティブな機械語にコンパイルすることなので、ある意味レジスタベースの命令の方がAOTのコンパイルや最適化には向いていると言えます。

        実は、レジスタベースとスタックベースの命令セットの議論は、コンパクト命令セット(RISC)と複雑な命令セット(CISC)の議論と同様に、結論が出ていないのだ。例えば、前述のようにスタックベースのJava VMはレジスタベースのDalvik VMよりも同じ機能を実行するのに必要な命令数が多く、その分遅いのですが、2010年にOracle社がARMデバイス上で非グラフィカルなJavaベンチマークを用いてJava SE EmbeddedとAndroid 2.2の性能を比較したところ、後者は2~3倍遅くなったという結果が出ています。上記の性能比較の結論とそのデータは、以下の2つの記事で見ることができます。

        1.  仮想マシン対決。スタック vs レジスタ

        2.  Java SE Embedded Performance Versus Android 2.2

        レジスタベースのDalvik VMとスタックベースのJava VMの比較・分析については、以下の記事もご参照ください。

        1.  http://en.wikipedia.org/wiki/Dalvik_(ソフトウェア)

        2.  http://www.infoq.com/news/2007/11/dalvik

        3.  http://www.zhihu.com/question/20207106

        結論はともかく、Dalvik VMは自己最適化のために最善を尽くしており、その対策は以下の通りです。

        1. 省スペースのため、複数のクラスファイルを同じdexファイルにまとめる。

        2. dexファイルの読み込みにリードオンリーメモリマッピング方式を採用し、複数のプロセスで共有できるようにし、プログラムの読み込み時間を短縮しました。

        3.命令実行速度を向上させるために、あらかじめバイトオーダーやワードアライメントをローカルマシンに適したものに調整する。

        4. バイトコードの検証を可能な限り事前に行い、プログラムの読み込み速度を向上させる。

        5. バイトコード書き換えが必要な最適化は、早めに行う。

        これらの最適化についてのより具体的な説明は、以下のページにあります。 dexoptによるDalvikの最適化および検証 の記事を参照してください。

        Dalvik VMとJava VMの違いを分析した後、Dalvik VMの他の機能(メモリ管理、ガベージコレクション、JIT、JNI、プロセスおよびスレッド管理など)を簡単に分析しましょう。

        I. メモリ管理 メモリ管理

        Dalvik VMのメモリは、大きく3つのタイプに分けられる。Javaオブジェクトヒープ、ビットマップメモリ、ネイティブヒープです。

        Javaオブジェクトヒープは、Javaオブジェクトを割り当てるために使用されます。つまり、コード内で新しく作成したオブジェクトは、Javaオブジェクトヒープ上に配置されます。Dalvik VMは、-Xmsおよび-Xmxオプションで起動し、Javaオブジェクトヒープの最小値と最大値を指定することができます。Dalvik VMの実行中にJavaオブジェクトヒープのサイズを変更することによるパフォーマンスへの影響を避けるために、-Xmsおよび-Xmxオプションでその最小値と最大値を同じに設定することができます。

        Java Object Heapの最小値と最大値は、デフォルトでは2Mと16Mですが、携帯電話の構成に応じてメーカーから調整されて出荷されており、例えば、G1、Droid、Nexus One、XoomのJava Object Heapの最大値はそれぞれ16M、24M、32M、48Mとなります。Dalvik VMのJava Object Heapの最大値は、ActivityManagerクラスのgetMemoryClassメンバ関数で得ることができます。

        ActivityManagerクラスのメンバ関数であるgetMemoryClassの実装は以下のとおりです。

public class ActivityManager {
    ......

    /**
     * Return the approximate per-application memory class of the current
     * This gives you an idea of how hard a memory limit you should
     This gives you an idea of how hard a memory limit you should * impose on your application to let the overall system work best.
     * returned value is in megabytes; the baseline Android memory class is
     The * returned value is in megabytes; the baseline Android memory class is * 16 (which happens to be the Java heap limit of those devices); some
     * device with more memory may return 24 or even higher numbers.
     */
    public int getMemoryClass() {
        return staticGetMemoryClass();
    }

    /* @hide */
    public int staticGetMemoryClass() {
        // Really brain dead right now -- just take this from the configured
        // vm heap size, and assume it is in megabytes and thus ends with "m".
        String vmHeapSize = SystemProperties.get("dalvik.vm.heapsize", "16m");
        return Integer.parseInt(vmHeapSize.substring(0, vmHeapSize.length()-1));
    }

    ......
}


        この関数は、frameworks/base/core/java/android/app/ActivityManager.javaに定義されています。

        Dalvik VMが起動時にJava Object Heapの最大値を取得するのは、システムプロパティdalvik.vm.heapsizeの値を読むためであり、ActivityManagerクラスのメンバー関数getMemoryClassが最終的にJava Object Heapの最大値を取得するのは、このシステムプロパティの値を読み出すためです。

        Java Object Heapの最大値は、通常、Androidアプリケーション・プロセスが使用できる最大メモリと呼ばれるものです。Androidアプリケーション・プロセスが使用できる最大メモリは、Javaオブジェクトを割り当てるために使用できるヒープを指していることに注意することが重要です。

        ビットマップメモリは、外部メモリとも呼ばれ、画像処理に使用されます。HoneyComb以前は、ビットマップメモリはネイティブヒープに割り当てられていましたが、そのメモリもJavaオブジェクトヒープにカウントされ、ビットマップが占有するメモリとJavaオブジェクトが占有するメモリを合わせてJavaオブジェクトヒープの最大値を超えることはできません。このため、大きな画像を扱うために BitmapFactory 関連インタフェースを呼び出すと OutOfMemoryError 例外が発生するのです。

java.lang.OutOfMemoryError: bitmap size exceeds VM budget

        HoneyComb以降では、Bitmap MemoryはJava Object Heapに直接割り当てられ、GCで直接管理できるようになりました。

        ネイティブヒープは、ネイティブコードでmallocなどを使って確保されるメモリです。このメモリは、Java Object Heapのサイズに制限されないため、システムによる制限はあるものの、自由に使用することができる。ただし、注意点としては、自由に使えるからといって、Native Heapを乱用しないことです。Native Heapを乱用すると、システムが利用可能なメモリを極端に減らし、その結果、システムが一部のプロセスを殺して利用可能なメモリを補充するという過激な措置を取るきっかけとなり、システム体験に影響を与えるからです。

        また、HoneyComb以降では、AndroidManifest.xmlのアプリケーションタグにandroid:largeHeap属性を追加し、その値を" true"としてDalvik VMに通知できます。実際、メモリ不足の機種では、アプリケーションのandroid:largeHeap特性を" true" にしても、そのJavaオブジェクトヒープを大きくできないことが分かっています。また、このプロパティによってJava Object Heapのサイズを大きくすることができたとしても、一般的には使用しない方がよいでしょう。システム全体のエクスペリエンスを向上させるためには、アプリケーションのJava Object Heapのサイズを大きくするのではなく、アプリケーションの必要メモリを減らすことに取り組む必要があります。

        II. ガベージコレクション(GC)

        Dalvik仮想マシンは、使用されなくなったJavaオブジェクト、すなわち参照されなくなったオブジェクトを自動的に回収する。自動ガベージコレクションは、開発者をメモリ問題から解放し、開発効率とプログラムの保守性を大幅に向上させる。

        CやC++では、開発者がヒープに割り当てられたメモリを手動で管理する必要があることは知っていますが、これはしばしば多くの問題を引き起こします。例えば、メモリを確保した後に解放するのを忘れてしまい、メモリリークが発生する。また、すでに解放されたメモリに不正にアクセスし、プログラムクラッシュを引き起こす例もある。優れたCまたはC++アプリケーション開発フレームワークがなければ、プログラムが大きくなったときに制御不能を引き起こしやすいため、平均的な開発者は単にメモリ問題を管理できないのです。最悪なのは、メモリが破壊されたとき、必ずしもプログラムがクラッシュするときではないことだ。いつ爆発するかわからない時限爆弾なので、原因究明が非常に困難なのです。

        AndroidがC/C++ではなくJavaを選択する理由は、メモリ問題から開発を遠ざけ、iOSに追いつくために、より良いアプリを開発するためにビジネスに集中するためである。結局のところ、C/C++プログラムの総合的な性能は、仮想マシンの上で動作するJavaプログラムよりもまだ優れているのです。しかし、メモリの問題を回避するために、Androidシステム内のC++コードは、以下のものを多用しています。 スマートポインタ オブジェクトのライフサイクルを自動的に管理するためです。Androidアプリの開発言語としてJavaを選択したのは、技術とビジネスの妥協点であり、それが成功したとも言える。

        話を戻すと、GingerBread以前のDalvik Virtualで使われていたガベージコレクション機構は、以下のような特徴を持っていました。

        1.stop-the-world、つまりガベージコレクションスレッドが実行されている間、他のすべてのスレッドが停止します。

        2. フルヒープコレクション、すべてのゴミを一度に収集する。

        3. 1回のガベージコレクションで、通常100msを超えるプログラム中断時間が発生します。

        GingerBread以降では、Dalvik仮想使用時のガベージコレクション機構が以下のように改善されました。

        1. コカレント、つまり、ほとんどの場合、ガベージコレクションスレッドは他のスレッドと同時に実行されます。

        2. 部分収集。これは、一度にゴミの一部しか収集されない可能性があることを意味します。

        3. ガベージコレクションは通常5ms未満でプログラムを中断させる。

        Dalvik仮想マシンがガベージコレクションを実行した後、通常、次のようなログ出力が表示されます。

D/dalvikvm(9050): GC_CONCURRENT freed 2049K, 65% free 3571K/9991K, external 4703K/5261K, paused 2ms+2ms

        このログでは、GC_CONCURRENTがGCの理由、2049Kが回収された総メモリ、3571K/9991KがJavaオブジェクトヒープの統計、すなわち9991KのJavaオブジェクトヒープのうち3571Kが使用中、4703K/5261Kが外部メモリの統計、すなわち5261K外部メモリのうち4703Kが使用中、2ミリ秒+2ミリ秒がガベージコレクションによるプログラム中断時間であることを示しています。

        III. ジャストインタイム(JIT)コンパイル

        前述したように、JITはAOTとの相対的な関係、つまり、JITはプログラムを実行しながらコンパイルするのに対し、AOTはプログラムを実行する前にコンパイルする。プログラム実行中にコンパイルすることには、利点と欠点があります。メリットは、プログラムの実行時情報を利用してコンパイルしたコードを最適化できること、デメリットは、プログラムの実行時間を奪うこと、すなわちコードのコンパイルと最適化に多くの時間を割くことができないことである。

        時間の問題を解決するために、JITはおそらくホットなコードだけを選んでコンパイルしたり、最適化したりするのでしょう。2-8の法則によれば、プログラムの時間の80%は、コードの20%を繰り返し実行することに費やされる可能性があります。したがって、JITはこの頻繁に実行される20%のコードを選択して、コンパイルと最適化を行うことができます。

        実行時の情報をうまく利用してコードを最適化するために、JITは抜本的なアプローチをとっています。jitは、プログラムがどのように実行されるかを仮定してコードをコンパイルし、その仮定に従ってコードを最適化します。プログラムがコーディングされる際、以前の仮定が維持されるなら、JITは何もしないので、プログラムの実行時性能を向上させることができます。しかし、これまでの前提が崩れた場合、JITはコンパイル・最適化されたコードを新しい状況に合わせて調整する必要があります。この調整にはコストがかかりますが、前提条件がほとんど成立しない限り、得られるメリットはデメリットを上回ります。JITは、コードをコンパイルして最適化する際に、プログラムがどのように実行されるかを仮定するため、JITが取る積極的な最適化措置は、ギャンブル、つまり博打とも呼ばれます。

        このギャンブルを例にとって説明しましょう。Javaの同期プリミティブにはLockとUnlock操作があることは知っています。LockとUnlock操作は非常に時間がかかるので、マルチスレッド環境でのみ本当に必要な操作なのです。しかし、同期関数や同期コードの中には、プログラムが実行されているときに、常に1つのスレッドで実行されるもの、つまり、これらの同期関数や同期コードが複数のスレッドで同時に実行されることはないものがある。そこで、JITはレイジーアンロックの仕組みを採用することができます。

        スレッドT1が同期コードCに入ると、やはり通常のフローに従ってレイジーロックL1を取得し、スレッドT1のIDがレイジーロックL1上に記録される。スレッドT1が同期関数や同期コードから離れるとき、先に取得した軽量ロックL1を解放しない。スレッドT1が再び同期コードCに入ったとき、軽量ロックLの所有者が自分であることがわかり、直接同期コードCを実行できるようになる。このとき、別のスレッドT2も同期コードCに入ると、軽量ロックLはすでにスレッドT1によって取得されていることがわかる。この場合、JIT はスレッド T1 のコールスタックをチェックして、まだ同期コード C を実行しているかどうかを確認する必要があります。もし実行している場合は、軽量ロック L1 を重量ロック L2 に変換して、重量ロック L2 の状態をロック状態に設定してからスレッド T2 を重量ロック L2 でスリープさせる必要があります。一方、スレッドT2が同期コードCに入っているときに、JITがスレッドT1のコールスタックをチェックして同期コードCから抜けたことを発見した場合、軽量ロックL1の所有者を直接スレッドT2と記録し、スレッドT2に同期コードCを実行させます。

        上記のLazy Unlockingの仕組みにより、プログラムの実行時情報をフルに活用し、静的コンパイル言語では不可能な最適化を行うことができます。このような観点から、静的にコンパイルされた言語(C++など)は、仮想マシン上で実行する言語(Javaなど)よりも、JITという強力な武器を持つことができるため、必ずしも高速とは言えないと言えるのです。

        Dalvik仮想マシンのJITサポートは、Androidバージョン2.2以降で、オプションとして提供されています。Dalvik VMをコンパイルするときに、WITH_JITマクロを使ってJITもコンパイルし、Dalvik VMを起動するときに、WITH_JITマクロを使うことができます。 -Xint:jit オプションを使用すると、Dalvik VM の起動時に JIT 機能を有効にすることができます。

        オフ 仮想マシンのJIT実装の仕組みについては、さらにこちらの記事を参照してください。 http://blog.reverberate.org/2012/12/hello-jit-world-joy-of-simple-jits.html .

        IV. Javaネイティブインヴォケーション(JNI)

        いずれにせよ、仮想マシンは最終的にターゲットマシンの上で動作するため、その命令をターゲットマシンの命令に変換して実行する必要があり、ターゲットマシンが動作しているOSのインターフェースを呼び出して行う必要がある機能もある。このため、Java層からC/C++層であるNative層へ関数呼び出しをトラバースさせる仕組みが必要になる。この機構をJava Native Invocation、またはJNIと呼びます。もちろん、Nativeコードを実行する際に、Javaの関数を呼び出す必要がある場合もありますが、これもJNI機構で実現できます。つまり、JNI機構は、Java関数の中でC/C++関数を呼び出したり、C/C++関数の中でJava関数を呼び出したりすることをサポートしています。

        実際、Dalvik仮想マシンが提供するJavaランタイムライブラリのほとんどは、ターゲットマシンのOSインタフェース、つまりLinuxのシステムインタフェースを呼び出すことで実装されている。例えば、android.os.Processクラスのメンバ関数であるstartを呼び出してプロセスを生成する場合、結局はLinuxシステムが提供するforkシステムコールを呼び出してプロセスを生成している。

        また、Androidでは、C/C++言語を使ってアプリケーションを開発しやすくするために、NDKを公式に提供しており、このNDKを通じて、JNI機構を使って、Javaの関数内でC/C++の関数を呼び出すことができるようになっています。しかし、Androidは公式にはNDKを使ったアプリケーション開発を推奨しておらず、NDKのサポートはSDKのそれに比べてはるかに劣っていることからも明らかです。

        V. プロセスおよびスレッド管理

        一般に、仮想マシンのプロセスやスレッドは、ターゲットマシンのローカルOSのプロセスやスレッドと1対1に対応し、ローカルOSがプロセスやスレッドをスケジュールできるという利点がある。プロセスおよびスレッドのスケジューリングはオペレーティングシステムのコアモジュールであり、その実装は特にマルチコアの場合を考えると非常に複雑であるため、仮想マシンでプロセスおよびスレッドのライブラリを提供する必要は全くないだろう。

        Dalvik仮想マシンは、Linuxオペレーティングシステムの上で動作しています。2つのプロセスが同じアドレス空間を共有している限り、それらは同じプロセスの2つのスレッドとみなされます。Linuxオペレーティングシステムは2つのforkとclone呼び出しを提供し、前者はプロセスを作成するために、後者はスレッドを作成するために使用されます。Linuxオペレーティング・システムにおけるプロセスとスレッドの実装については、前節の Android学習ブート編 記事中で紹介したLinuxカーネルの名著

        Androidのアプリプロセスについては、2つの大きな特徴がありますので、以下に簡単に説明します。

        第一の特徴は、Androidアプリの各プロセスがDalvik仮想マシンのインスタンスを持つことである。この利点は、Androidアプリのプロセスが互いに影響を及ぼさないことです。つまり、あるAndroidアプリのプロセスが予期せず中断しても、他のAndroidアプリのプロセスの正常な動作に影響を及ぼさないということです。

        2つ目の特徴は、各Androidアプリケーション・プロセスは、Zygoteというプロセスによってフォークされ、このプロセスはinitプロセスによって、つまりシステム起動時に開始されます。 がロードされることです。Zygoteプロセスは、Androidアプリケーションプロセスを作成する必要があるときはいつでも、自分自身をコピーすることによって、つまり、forkシステムコールによってそれを行います。フォークされたAndroidアプリケーションプロセスは、一方ではZygoteプロセスの仮想マシンインスタンスをコピーし、他方ではZygoteプロセスと同じJavaコアライブラリのセットを共有する。これにより、Androidアプリケーション・プロセスの作成が高速化されるだけでなく、すべてのAndroidアプリケーション・プロセスが同じJavaコア・ライブラリを共有するため、メモリ容量が節約されます。

        以上が、Dalvik仮想マシンの特徴の概要です。実際、Dalvik VMの実装はJava VMと似ています。例えば、Dalvik VMはJDWP(Java Debug Wire Protocol)プロトコルもサポートしているので、Dalvik VMで動作するプロセスをデバッグするためにDDMSを使用することも可能です。Dalvik VMの他の機能や実装原理については、Java VMの実装を参照することが推奨されており、ここでは3冊の参考書が提供されています。

        1. Java仮想マシン仕様(Java SE 7) 

        2. インサイド・ザ・Javaバーチャルマシン 第2版

        3. オラクル JRockit: 決定版ガイド

        また、Dalvik VMの命令セットとdexファイル形式の説明については、以下の公式ドキュメントを参照してください。 http://source.android.com/tech/dalvik/index.html . 仮想マシンの実装原理に興味がある方は、こちらのリンクも参考にしてください。 http://www.weibo.com/1595248757/zvdusrg15 .

        Dalvik仮想マシンの研究の目的は、Java層とC/C++層の間の関数呼び出しを橋渡しし、Linuxカーネル上でAndroidアプリケーションがどのように実行されるかをより理解できるようにすることである。これを達成するために、次の記事では、以下の4つのシナリオに焦点を当てます。

        1. Dalvik仮想マシンの起動処理 を使用する。

        2. Dalvik仮想マシンランタイム を使用します。

        3. JNI関数の登録方法 を使用します。

        4. Javaプロセスやスレッドの生成過程 .

        この4つのシナリオをマスターすれば、これまでの記事と合わせて、Androidのシステムを隅から隅まで網羅できるようになりますので、ご期待ください。

ラオ・ルオの新浪微博。 http://weibo.com/shengyangluo とフォローを歓迎します