2010年6月17日木曜日

今のJVMに欠けている物

原文: チャールズ=オリバー=ナター

今日ツイッターで、「JVM及びJDKが、あらゆるプログラミングにおいて真にイケてるプラットフォームになる為には未だ幾つかの欠陥が有る」と呟きました。沢山の人から「もっと詳しく」とせっつかれたので、ここに短く書き起こしておきます。勿論、これで全部という訳ではないのでしょうが、今日思いついたのはこれだけです。

ゼロから起動する際のパフォーマンス

現存するJVMの起動はかなり速いですが、Java 7でのHotSpot(訳注:Sun及びオラクルのJVM)にはこれをより良くする為の改良が盛り込まれています。普通、こういった改良は、バイトコードを予め検証したり(或いは検証の為のヒントを与えたり)、クラスデータを幾つかのプロセスで共有したり、在り来たりではありますがプログラムのロード時間やリンク時間を短縮する工夫を凝らす事で成し遂げられます。ところが、多くのアプリケーションにしてみれば、このような改良は、起動時間を長引かせる最大の原因(ゼロから起動する際のパフォーマンス)には余り効果がありません。JVMはJITコンパイルした結果をセーブしないので、起動する際には常にゼロからのスタートを余儀なくされます。その時、コンパイルするに至る迄はバイトコードをインタープリタで走らせるのです。インタープリタの無いJVMでさえ、一番初めに掛かる、それほど最適化されていないアセンブリコードへのコンパイルにはかなりの時間を要します(コマンドラインでJRockit(オラクル及びBEAのJVM)やJ9(IBMのJVM)をいじってみて下さい)。

改善の為の提案は今までにもなされていますが、どれも一筋縄では行きません。

  • コンパイルする過程の分割
    初めのコンパイルは最速で、最適化を行わないコンパイラを使い、後から適当なプロファイリングを行いながら最適化を行う。HotSpotのJava 7リリースではこのようなコンパイラが装備されるかも知れませんが、この開発を妨げるような問題が幾つか有りました。
  • コンパイルした結果や最適化した結果を何らかの形で保存する
    理論的には可能ですが、これを深く追求すればするほど保存するのは難しくなっていきます。メモリ内で行われる最適化やコンパイルの結果は、普通メモリのレイアウトに依存しています。これをディスクに保存すると、ロードする際、以前と違う物(メモリアドレスやクラスアイデンティティ等)を消してしまう必要が有ります。.NETでは、凡、静的コンパイルに限られてはいるものの、これが可能です。これが妥協点という事でしょうか。
  • JVMを常に走らせておいて、それに新しい仕事を与える。
    JRubyではnailgunというライブラリを用いてこれを実現していますが、問題点が幾つか有ります。第一に、JVMの状態(システムプロパティやメモリ消費量)に不都合が生じる場合が有ります。第二に、死なないスレッドが発生した場合にそれを終了させる事が出来ないので、長い間走っていると、このような「死に損ない」のスレッドが溜まって行きます。そして第三に、コンソールで走っている訳では有りませんから、普段コンソールで当たり前にやっているような事が出来ません。
  • これが、JRubyが直面する解決出来ない最大の問題であり、弁明を必要とする最大の問題です。JRubyは速く、時としてとても速く、日に日に速くなっていますが、初めの五秒はそんなに速くないので、余り良いとは言えない第一印象を与えることになるのです。

    コンソールとターミナルのサポートを強化

    標準でJVMに装備されているIOストリームが色々な面で役に立たないと嘆いているブログは枚挙に暇が有りません。例えば、selectを使えないというのがありますが、JRubyにはこれが原因で直せないバグが幾つか有ります。また、サブプロセスに渡す事が出来ない、というのも有りますが、これはIOライブラリの欠陥よりはプロセス起動の為のAPIに拠る所が多いと言えるでしょう。ターミナル関係の機能はJava APIには悉く欠落しているので、ラインエディットの機能を備える為だけにjlineみたいなライブラリを取り入れざるを得ない事になるのです。もしJDKがターミナルとプロセスを上手く扱えるAPIを標準で備えると、こうした余計な手間暇を掛ける必要が無くなってしまいます。

    未だ望みは有ります。Java 7に取り入れられる予定のNIO2ではプロセス起動の為のAPI(標準IOストリームを継承する事も可能)や、select可能な様々なチャンネル等の多くの機能が盛り込まれています。これで充分だといいですね。

    壊れたAPIを直す

    JDBCは欠陥品です。何故かと言うと、ドライバーをハードリファレンスを含んだマップに登録しなければならず、登録を行ったクラスローダーから登録削除をしなければメモリリークしてしまうからです。と言う事は、ウェブアプリやEEアプリケーションからJDBCをロードすると、ドライバーがアプリケーションへのリファレンスを持ち、且つ上に述べたマップがドライバーへのリファレンスを持つので、アプリケーション全体がメモリに残る事になります。これがアプリケーションサーバがアプリケーションをundeployした後でもメモリリークを起こす最大の理由です。JRubyの立場から言うと、これも直す事が出来ないバグです。

    オブジェクトのシリアル化も壊れています。何故かと言うと、クラスローダーを獲得し、オブジェクトのフィールドをリフレクションによりアクセスし(リフレクションを使ってフィールドにアクセスするならば、可能ならばいっそのことカプセル化等は無視してしまえばいい)、そしてオブジェクトのインスタンスを適切に初期化する事なく生成する為に有りとあらゆるトリックを使っているからです。これを使う為には、引数を何も取らないコンストラクタを用意したり、コンストラクタ以外でも初期化が可能なようにfinal修飾子を外したりしなければならず、万が一にもデフォルトで用意されているシリアライザを使おうものなら、その遅さは天誅が下るほどです。

    リフレクションが遅すぎるのに、これをどうする事も出来ないというのもあります。リフレクションを通したメソッドを呼び出しを何層にも渡って行う羽目になるだけではなく、引数のリストと数値関係をボックス化しなければならず、加えて例外を扱う為にこれ等全てをラッパーに包む必要が有る、と言った具合です。invokedynamicに伴って、メソッドへのアクセスが高速な直のポインターであるメソッドハンドルが装備されます。これは当の昔に加えられて然るべきフィーチャでしたが、幸いな事にJava 7でこれが現実のものとなります。それ迄は、JRuby等のプロジェクトはリフレクションに掛かるコストを負担するか、メソッドハンドルを手作業で生成するしかありません。実際、JRubyでは両方やっています。

    正規表現もいただけません。簡単な選択を用いるだけで、大きなインプットに出くわした時Javaスタックが崩壊してしまうほどです。現在の、Sunによって産み出された正規表現は、選択等に再帰を用いているのでインプットが大きいと簡単に壊れてしまいます。JRubyではこの酷い問題に対処する為、我々自身の手で行ったもの二度を含めて、何と四度も正規表現エンジンの実装を行いました。ユーザの為には私たちは血の滲むような努力をしています。

    他にも例はまだまだあります。それはJava 1.0から手直しの入っていない遺物あったり、(老いたAPIは死なず。ただ軽視されゆくのみ…あるいは完全無視されるのみ。)実際には複数のJVMプロセスがアプリケーションを一つずつ又は多くても二三個走らせているのが効率的であるのに、巨大な一枚岩の様なサーバが何十ものアプリケーションを取り扱うのが良いという姿勢の遺物もあります。加えて一枚岩な環境では、アンデプロイの際にメモリリークを起こしたり、プロセスを隔離していれば起こらない、基本的なリソースの奪い合いが起きるなどの問題を抱えています。このようにまずいAPIを本腰を入れて手直しをするのは価値ある事だと思います。

    ネイティブライブラリとPOSIX機能の強化

    Java 6の時点ですらシンボリックリンクを扱う事が出来ませんし、ファイルのアクセス許可設定も限られたものです。プロセスを新たに起動させるAPIはとても酷い。全てのチャンネルでselect出来ない。出来るのはソケットだけ。ネイティブライブラリを使う為には、JNAやJFFI等の、ネイティブライブラリを動的にロード及びバインドする素晴らしいライブラリが有るにも拘わらず、JNIのコードを書かなければなりません。

    POSIXに似たフィーチャが欠けているのは今では言語道断です。JDKに見られるシステムレベルのAPIの殆どは、Windows 95辺りの最低限のレベルに合わせたものです。最近のオペレーティングシステムではPOSIXのAPIの殆どを実装しているというのに。NIO2により、かなり改善されますが、POSIXの一部が対応されないのはほぼ確実です。対応されなかったPOSIX APIにJava APIを完全に合わせるには人手が足りないとか、システムに依存し過ぎのAPIである、等の理由が考えられます。

    ネイティブライブラリをロードする問題についても、これが当の昔にJDKの一部になっていて当然であると言えます。「純粋なJavaを!」と声高に叫ぶ人は多いと思います。私も叫ぶでしょう。しかし、Javaのライブラリには機能が備わっていないとか、外部プロセスを介していたのでは容量向上に期待出来ない場合はあります。このような場合、ネイティブライブラリを使わざるを得ません。ところが、これをJavaプラットフォームでやろうとすると、入門者には中々手も足も出るような代物では有りません。ネイティブコードをいつ呼び出すか、自分たちで選べるようにするためにも、JNAやJFFI、又はこれに似た物をJDKに入れて欲しいものです。

    決め台詞

    ここまでに挙げた様々な問題点の多くは、JRubyでは多大な代償を払って、解決或いは何らかの方法で対処してきました。他のどのプロジェクトよりもJVMやJDKの欠点に対処してきたと言っても過言ではないでしょう。起動時間の短縮の為にあらゆる手段を尽くしました。Cのコードを一行も書く事なくネイティブライブラリにバインド出来るように、華麗かつ堅実なライブラリを同梱しています。コンパイルの際、また実行時に多くのコードを生成して、リフレクションをそれほど使わずに済むような工夫もしています。十数個のOSでPOSIXにネイティブに対応しています。Marcin Mielzynski氏が鬼車をJavaに移植しました。外部プロセスの呼び出しがRubyユーザの思惑通りになるように考えられるトリックは全て使いました。云々。

    でも、このやり方では長くやって行く事は出来ません。今まで上手くやってきたとは言え、JVMやJDKにいつまでも継接ぎを当てて行く訳には行きません。願わくば、この記事が世界中のJVMとJDKの開発者への警鐘となりますように。Javaプラットフォームが、サーバのみ、巨大アプリのみ、継続アプリのみ、ターミナル無しの世界になってしまわないようにするために、これらの問題点を解決して下さい。いつでも協力します。:-)

    0 件のコメント:

    コメントを投稿