実行スピードに挑戦する
Javaアーキテクチャの変遷をたどる
Simple、Object-Oriented、Distribute |
Javaはポインタを廃したことで、メモリ管理を考えなくてよくなった。構文仕様も非常に明快である。とはいえC++のオブジェクト指向は受け継いでいる。URLを指定してリモートのオブジェクトにアクセスできる。これらのことが、分散オブジェクト環境にぴったりとはまった。
ホワイトペーパーには書かれていないが、ネットワーク上の別のマシンにあるオブジェクトを呼び出す分散オブジェクト環境では、ポインタが頭の痛い問題となる。ここで、Javaしか知らない(ポインタを知らない)という幸運な読者のために、ポインタの説明をしておこう。ポインタとは、変数に値が代入されるという概念に、コンピュータのメモリとアドレスの関係を実装したようなものである。まず、変数にメモリのアドレス値を代入しておく。そして、そのアドレスに格納しているメモリ内容を参照することができるようにする。この機構がポインタである。この機構によって、ポインタ自体の型はポインタ型と分かるが、ポインタに格納されているアドレスが指すメモリ上にはどんな型の値が格納されているか分からない。すなわち、データを格納したときの型を覚えていないとポインタは扱えないわけだ。さらに、たちの悪いことに、アドレスが指すメモリに対する型指定はダイナミックに可能だ。そのため、間違った型指定がまかり通ってしまう。そして、このことが、分散オブジェクトでは困った問題をもたらすのだ。例えば、あるプログラムが、ほかのマシン上にあるオブジェクトを呼び出そうとする。そのオブジェクトにポインタがあった場合、呼び出し元のプログラムは、相手方のマシンのオブジェクトがいじれるすべてのメモリをいちいちコピーしてこないと、うまく動かない。場合によっては、領域ごとどっさり転送、なんて状況になってしまう可能性もある。
OMGのCORBA Webサイト |
昔、CORBAという分散オブジェクトブローカの仕様がOMGという組織の腕利きのメンバーによって、いろんな言語で仕様化されていた(いまでもあるが……)。その中のC++のインプリメンテーションでは、ネットワークに分散されたこれらのオブジェクトを参照する際のメモリコピーの責任がプログラマに重くのしかかってくるのである。プログラマが何げなくポインタを使用したとき、それが、ネットワーク上の大量のメモリ転送を引き起こしてしまうこともある。さらに、そのオブジェクトのライフサイクルに対しても、メモリの確保、解放がリモートということを意識してプログラミングしなければ、メモリフォルトでアプリケーションが終了したりもする。もちろんサーバ側もその危険にさらされている。
ポインタの概念がないJavaでは、このようなことが一切起こらない。コンパイラがポインタを許さないのだから。CORBA仕様でもJavaのインプリメンテーションがあり、当然こちらの方がすっきりしたインプリメンテーションになっている。Javaには、RMIというネットワークリモートオブジェクトの呼び出しに関する仕様があり、その中でその恩恵をあますことなく利用できた。いま、花盛りのJ2EE仕様、SOAPなどにおいてもまた、しかりである。これは取りも直さず、Simple、Object-Oriented、Distributedという思想が盛り込まれていたからにほかならない。
Robust、Secure、Architecture Neutral、Portable |
堅牢、保護、機種に非依存、ポータブルという概念は、さまざまな機種、OS、言語環境が、おでんのように混在するインターネットで必要不可欠な要素である。メモリ管理をプログラマに強制しないことが堅牢性をもたらしている。インスタンスがないオブジェクトを参照すればnull exceptionが発生するが、try-catch句で囲んでおけばプログラムで処理可能である。メモリを直接アクセスできないので、悪用してPCのブートエリアに余計なコードを埋め込もうとしても出来ない。
ホワイトペーパーでは、Secureな実装ポリシーとしてvirus-free、tamper-freeを提唱している。実際の世の中にあるブラウザでは、いろいろ穴が見つかってはつぶされている今日であるが、理想としてはvirus-free、tamper-freeである。
インターネットの向こうで実行されているマシンに依存するデータ、実行形式の違いを意識させないのは、分散オブジェクトにとって必須条件である。この思想が、Architecture Neutral、Portableである。例えばプログラマが、このオブジェクトはこのメーカーのマシンで動くので整数は16bitになる。だから、こちらのマシンから送る変数の上限値を16bitに落としておかないと……、といったことをあらゆるマシンが接続されたインターネット上で考えるのは不可能に近い。
Interpreted |
いよいよ実装の話になる。Interpretedとは、翻訳して実行するという意味だ。ポインタの概念をやめて、メモリ管理の責任を言語側で吸収するには、メモリ管理を実行環境でサポートする必要がある。加えて、プラットフォームへの依存性を排除して、どのマシンでも共通に動作し、かつ、データもプラットフォームに依存しない統一された表現を維持するには、Smalltalkのようなインタプリタ方式のほうが都合が良い。
インタプリタの対義語にコンパイラがある。コンパイラはあらかじめ自然言語に近いソースコードをCPUが即時実行できる機械語に翻訳し、それを後から一気に実行するものである。それに対しインタプリタは、ソースコードを読みながら、逐次実行する。インタプリタの老舗といえばBill Gates氏の作ったBASICだと思う。JavaはBASICと違って、ソースコードを一度バイトコードという中間コードに翻訳してから、それをインタプリタが実行するようになっている。
UCSD Pascalの創造主Niklaus Wirth博士のページ |
ところで、Javaのような中間コード方式のインタプリタは、Javaが最初ではない。Smalltalkかというとそうでもない。世界中の研究者の論文を読みあさってみなければ本当に何が最初かは、正しい結論は出ない。しかし、商用、ポピュラーという意味で老舗といえばUCSD Pascalだと思う。1970年代初めのマイコン有史前の時代は、メモリは非常に高価でCPUはやたら遅い、という環境だった。その中で、構造化プログラミングと豊富な型宣言を有したUCSD Pascalの中間コードコンパイラ方式は、開発者Niklaus Wirth博士の“言語仕様と最適化は独立した問題である”という議論の証明のために作られたともいわれている。しかし、実際にインプリメンテーションの面でかなり有利だった。
中間コードコンパイラとは、自然言語に近いプログラミング言語を一度言語仕様に関係しない仮想マシン用のコードに変換するものである。中間コードインタープリタは、このコードを現実のマシン上で動くプログラムに解釈しながら実行する。ソースコードを直接コード中に持たないことから、メモリ効率が非常によく、小さなメモリでも実行できる。さらに、この中間コードを翻訳して実行するプログラムさえ作ってしまえば、どのマシンでも実行できる。この中間コードは機械語に近いものであれば、翻訳して実行するプログラムも短く、短いことで性能もそこそこ出る。
実際には、自然言語に近いプログラム記述=Java言語を*.javaファイルに作成、javacコマンドでコンパイルしてバイトコードの*.classファイルに変換する。java.exeを実行することで、このバイトコードを翻訳しながら実行する、という流れになる。これは、UCSD Pascalと全く同じである。さらに、Javaの場合、バイトコードの効率化を考えて、ZIPで圧縮されていたりもする。CPUとメモリの関係に比べてインターネットの通信速度や、ディスクのI/Oはけた違いに遅い。CPUを使ってZIPで圧縮して転送量を減らすか、圧縮をやめてCPUの負荷を下げるかの選択で前者を選ぶのは、当然といえよう。
バイトコードを翻訳する実行形式(/opt/java/bin/javaとかjava.exe)のことを、Java Virtual Machine(Java仮想マシン、以下JVM)という。いろんな機種のいろんなOSでJVMを作ってしまえば、どのマシンでも、全く同じバイトコードを実行できる。バイトコードの扱うデータは、どのマシンでも同じものとして取り扱える。バイトコードがある程度機械語に近ければ実行も軽い。翻訳して実行するから、メモリの不正使用があれば、翻訳した時点でエラー処理ができる。もう、プロセスがいきなり落ちることはないのだ。こうして、すべてのインターネットでの要件がJVMによって一気に解決できたかのようである。
High Performance |
機種に依存しない仮想マシンはいいこと尽くしではない。やはり、コンパイルによってターゲットとなる機種のCPUが直接実行できる機械語に比べればパフォーマンスは悪い。なぜなら、パーシング(読み込んで解釈する)の分だけオーバーヘッドとなり、実行速度が落ちるからだ。加えて、Javaの場合、実行時に定義された変数のメモリ領域も後始末をしないとならない。どういうことかというと、ローカル変数に数値を代入するには、その変数用のメモリを確保して、そこに代入したい値を格納する。そして、変数はいつかは使用しなくなるので後始末しなければならないが、いちいちその場で始末していては性能向上が望めないわけだ。そこでJavaでは、領域がなくなったときにまとめて後始末する。これをガベージコレクションという。
当初のHot Javaブラウザでは、アプレットを動かしていたが、この程度であれば上記の方式でも十分に用が足りた。実際、Hot JavaブラウザのDukeのアニメはよくできていて、初めて見たときはすごいと思ったものだ。ガベージコレクションが動作しているときはちょっとつまずいたように止まるが、そんなのは愛敬の範囲だ。インタプリタが小さければ、実行するときの機械語は、メモリに張り付いたまま動作するし、バイトコードは効率がよく、コードを読み込む量が少なくて済むのだ。しかし、今日においては、そのようなオーバーヘッドは命取りだ。秒間何千リクエストという量をそつなくこなすには無理がある。
JITの登場 |
前節で紹介したようなパフォーマンスの問題を解決するために登場したのがJIT(Just In Time Compiler:実行時コンパイル)だ。これは、利用されるクラスをその都度、中間コードから機械語にコンパイルして実行するという発想である。当然のことながら、実行時にコンパイラが裏で機械語に翻訳するので、最初の1回目の実行は時間がかかる。オブジェクト指向言語では、オブジェクトを使用する最初に初期化するメソッドを定義できる。この定義はコンストラクタといって、オブジェクトを準備する部分である。実行時コンパイルは初回、そのオブジェクトが作られるときに動作するので、コンストラクタが走るときは遅くなってしまう。いろいろなオブジェクトを臨機応変に使用する場合はコンパイルが重くなってしまうという欠点があるが、それでも、通常のインターネットのアプリケーションは繰り返しが多いので、インタプリタに比べれば十分に速い。多分、繰り返しが全くない直線道路のようなプログラムは(作るのも難しいが)、インタプリタの方が速いだろう。
機械語にコンパイルされると移植性がなくなるかというとそうではない。コンパイルされた機械語は、メモリに置かれてファイルに落とされないので、その場限りで捨てられてしまう。つまり、java.exeの起動が終わればなくなってしまう。ほかのUNIXマシンで起動したときは、新たにそのマシンの機械語が使われるので、移植に問題は起きない。もちろん扱うデータの形式は保たれるので心配はない。
JITは当然、コンパイルされた機械語の性能うんぬんよりも、コンパイルに要する時間が命であるために、機械語の効率、性能は余りよくないインプリメンテーションが多い。もちろん、性能はインプリ任せなので、個々の例に対して、一概に論ぜられない。
ガベージコレクタの改善 |
Javaでは、確保したメモリの開放はJVMが自動的に行う。この機構をガベージコレクタと呼ぶ。JVMで使用するメモリは実行時に指定する(この領域をヒープという)。初期のJavaにおいては、ヒープの使用方法は一通りだった。空きがなくなったらまとめて開放して、使用していないところを使用できるようにするというものだ。小さなメモリで動作させれば、まとめて開放しても領域が少ない分、短期間で終わる。大きく取れば、ヒープがなくなるまで快適に動き続け、ちょっと止まってまた動くというような動きになる。動画でいえば、ちょこまか動いていて、ちょっとリズムがおかしくなるのが前者。すかすかリズミカルに動いていたと思うとパタっと止まって、また、動くというのが後者になる。すなわち、このガベージコレクタの動作が、実行速度のボトルネックとなる。
Webアプリケーションプログラマでなくても、ローカル変数は割とすぐなくなるが、かなり長い間使用されるオブジェクトも必要になるケースに出くわすことがあるだろう。この両者を一緒のメモリ管理領域においてよいわけがない。
そこで、NewとOldというヒープの中身を2つに分けた構造が登場した。短期的に参照されてすぐ必要なくなる変数は、Newのヒープに置かれて、頻繁に開放される。一方ある程度長い間メモリに居座る変数は、Oldのヒープに置かれて、そこがいっぱいになったときに開放されるという具合である。このOldがいっぱいになったときに起きるガベージコレクションがFull GCと呼ばれるものである。
HotSpotの登場 |
ヒープの改善は、Webアプリケーションサーバの実装に望まれていた。同時に、Webアプリケーションサーバでは、より多くのクラスを抱えるようになって、プログラムも巨大になった。このように巨大なアプリケーションでは、JITのように新しいクラスがロードされ、実行するたびに全部コンパイルするのでは時間がかかり、これがばかにならなくなってしまう。JITがいかに高速にコンパイルするといってもオーバーヘッドが大きい。そこで、インタープリットしている間にプロファイルを取って、特にたくさん使う部分だけコンパイルしてあとはほっとけば、もっと速くなるのではないか? という発想がHotSpotである。この技術はヒューレット・パッカード(以下HP)がJavaSoftに提案し、現在実装されている。なんと、JavaSoftはオープンだったことであろうか!
Java HotSpot Technology |
多くのアプリケーションにおいて、処理全体の20%の部分が、実行時間の80%を占めるというようなことは多々ある。CISCマシンとRISCマシンのどちらが優秀かの議論があったときに、HPではいち早くRISCを商用化した。これは、CISCマシンでは、実行時間のほとんどは一部の命令セットを行うために費やされていて、そのほかの複雑な命令セットは残りのわずかな時間にしか使われていないという統計情報から、実行時間のほとんどを費やしている命令セットを高速化し、代わりに、複雑な命令セットの実装をやめてしまうという発想である。HotSpotもこれと同じような発想なのだ。
以前、CとJavaはどっちが速いのか? という質問をよくされたが、速いのはJavaか、最適化パラメータを指定してよくコンパイルされたCであると答えたことがある。ポインタをうまく使用したプログラムにはかなわないが、素人が単に最適化オプションを指定してコンパイルしたCの計算プログラムとJavaを比べたら、Javaの方が速いという例が実際にあった。何回かインタプリタで実行し、プロファイルを取得し、これを基にコンパイルするので、Cでいえば、profデータを基にコンパイルする“最適化コンパイル”と同じような効果をもたらす。プロファイルを取ると、どうして速いのかといえば、例えばあるif文が100回繰り返すうち、trueになるのがたったの1回で残りの99回はfalseであったとしよう。falseのときにjumpするようにコンパイルしてしまったら、99回もjumpが起こってしまう。コンパイラが機械語に翻訳するときにjump命令を発動する確率が少ないことが分かっているなら、trueになるコードをjumpする方に、ロジックをひっくり返して配列する。CPUは100回のうち、99回はjump命令を実行しないので、命令をほとんど連続読み込みでき、先読みしていればほとんどヒットする状況にできる。ヒットしないのはjump命令を実行したときだけだ。
これは、前方予測といわれる技法でもある程度カバーできるが、コンパイラでやった方が効く。最近のIntelのItaniumに使われているテクノロジのEPIC(Explicitly Parallel Instruction Computing:明示的並列命令コンピューティング)では、コンパイル時に最適化し、6本もある内部レジスタを無駄なく使用し、高性能を発揮する。HotSpotとこの技術は非常に相性がいい。HPでは、Itanium搭載のHP-UXマシンに対応したHotSpot実装のJVMをすでにリリースし、高い性能を実現している。
Cでは、最初のコンパイル時にプロファイルデータを取るように指定し、一度テストRunするとこれらの情報を得ることができる。その結果を最適化コンパイルに反映する。高度なコンパイルテクニックである。でも、Javaなら適当に流しておくと自動的にやってくれるのだ。計算に供するシナリオが変わったら、C++ならprofの取り直し、コンパイルのやり直しだが、Javaなら単に実行すれば、そのときに応じたコードが吐き出される。
HotSpotとヒープの改善が実装されたのはJDK(Java Development Kit)1.3.xからである。そうして、ECサイトにJavaが性能のネックとなることなく利用されるに至ったわけだ。HPのItanium対応JDKの最新版は、1.4.2である(2003年10月25日現在)。
Multithreaded(マルチスレッド) |
Hot Javaのような動画を扱うブラウザの仕様において、マルチスレッドは必須の要件である。1つの動画が動いている間にほかのボタンの操作ができないとなると、全く使い物にならないからだ。もちろん、今日においては、Webアプリケーションサーバにおいて、複数のリクエストをマルチスレッドで並行してこなすためにも必要不可欠な機構である。当初のJavaにおいては、マルチスレッドはすべてJVMで実装されていた。インタプリタがタイムシェアリングして、仮想的に複数のスレッドを動作させる機構になっていた。このようなインタプリタで実行されるスレッドをgreen threadと呼ぶ。おそらくGreen Projectの名残である。JIT、HotSpotの登場によって、実行時に機械語にコンパイルされるので、仮想マシン上のスレッドも、OSが直接管理するスレッドになった。これをnative threadと呼ぶ。green threadに比べればオーバーヘッドが格段に少ない。
Dynamic |
C++には、ネームスペースという概念がある。Javaにはない。似たようなものがあるとすれば、Class Pathである。Class Pathは、環境変数CLASS_PATHにセットしてJavaを呼び出すか、パラメータで指定する。なぜ、OSのファイル構造を指定するのかといえば、実行時にクラスを呼び込む方式だからだ。Javaは、プログラムを*.javaで記述して、javacコマンドでコンパイルし、*.classという名前のクラスファイルを作成して、実行すると説明したが、この、クラスファイルが実行時に読み込まれるので、読み込み元のディレクトリを指定しておかなければならないのだ。
おかしな話で、UNIXには、gnu zipが標準的にある。だいたいどのUNIX OSでも利用可能だ。しかし、ZIPはない物もある。WindowsのWinZipで作成したZIPファイルをUNIX上に持ってきて展開したいけど、できない! と泣きつかれたことがある。そんなときは、JDKに付属するjarコマンドというJavaのプログラムが使えるのだ。Windowsのエンジニアならおなじみである。逆に、UNIXのtarコマンドで作成したファイルをWindowsで展開したいときは、jarコマンドが使えるのだ。もっとも、Windowsならtarコマンドのフリーソフトはたくさんあるが。
jarコマンドをいきなり登場させて申し訳ないが、Class Path上に多数のclassファイルが置かれるのも管理的に厄介である。Javaでは、クラスファイル用に、WinZipと同じような機能を持ったjarコマンドが提供されていて、クラスファイルをアーカイブ化(ディレクトリごと固める)することができるようになっている。このファイルは、*.jarという識別子が付けられ、jarファイルと呼ばれる。Java版のtarだからjarというのだろう。
動的にロードされることで、メモリの節約に大きく貢献していた。作ったプログラムが全部実行されるわけではないからだ。巨大なjarを作成してライブラリとしてさまざまなクラスをまとめて扱ったとしても、使わないものはメモリにロードされないので実行時のメモリは気にしないで、クラスライブラリを作成できる。Swingなどもそうである。全部読み込んだら大変なことになる。
ところで、この実行時にクラスを読み込む形式は、今日のWebアプリケーションサーバテクノロジの発想を促す大きな柱にもなっている。実行時の動的クラスの呼び込みがなければ、JSP(JavaServer Pages)なんか簡単に発想できない仕様である。JSPでは、HTMLライクな*.jspファイルの中にJava言語を埋め込んでおく。通常の実装ではJSPが参照されたときに、*.javaが新規に生成され、該当個所がjavacでコンパイルされて*.classが作成される。この*.classはServletとして動作する。つまり、HTMLもどきなJSPがアクセスしたときに初めて、Servlet化して、実行時にそのクラスがロードされ、実行されるのだ。
しかし、JSPも高度に肥大化している今日では、起動時にできる限り事前にコンパイルしておく、といった機能をアプリケーションサーバでは標準的に装備しているのである。JSPを*.javaにした時点で2Gbytesを超えてしまい、エラーが出てしまう、とかいうケースもある。また、ほかのケースでは、事前にコンパイルしておくことを前提としたような設計をしたりもするので笑える(笑えないか……)。
一度稼動させたら止められないWebアプリケーションサーバの場合、バグによるソースコードの修正を反映させるデプロイメントがスムーズに行われる必要がある。このようなときに、クラスの動的なロードをポリシーにしているJavaでの発想は、Webアプリケーションサーバにも引き継がれている。BEA WebLogic Application Serverの場合、現存のクライアントのコネクションは古いバージョンで対応し、新しいクライアントのコネクションから、新しくデプロイしたクラスに任せるといったような機能(スマートデプロイメントという)を備えていたりする。
◆
コンピュータそのものの歴史が、たったの半世紀強で、すでに8年の歴史を持っているJavaはもう目新しい言語では決してない。マイコン少年の時代から、Jobs氏や、Gosling氏と同じ世代を過ごしたエンジニアとして、Javaの仕様はすごいと感じる。それが伝わったら幸いだ。Javaのことを広い読者層にあまりよく説明できなかったが、前述のoriginal
Java Whitepaperや、Java誕生のページをまだ見ていない貴方!
ぜひ、見にいってみて下さ
い。
2/2 |
INDEX |
||||||||||
実行スピードに挑戦するJavaアーキテクチャの変遷をたどる | ||||||||||
Page1 Javaの歴史 |
||||||||||
Page2
|
筆者プロフィール | ||
塩野誠(しおの まこと) 横河ヒューレット・パッカード(株)(現日本ヒューレット・パッカード(株))に入社。計測事業MKTGにて半導体主力製品の企画を担当後、コンピューター事業に移動。SEとして半導体生産システムのHPソリューション、通信事業において研究分野のソリューションに従事し、ネットワーク管理ソリューションHP OpenView、HPのCORBA製品を主軸に、数々のプロジェクトに参画する。 その後、ISV製品事業でDB製品、Java製品の技術担当、EJBコンソーシアムに参加。現在は、BEAコンピテンスセンターでBEA製品の技術担当、HPサーバ上でのBEA製品の機能検証、性能試験、製品技術サポートに従事。また、日本ヒューレット・パッカードが提供する技術サイト「HP-UX Developer Edge」の執筆を担当している。
|
Java Solution全記事一覧 |
- 実運用の障害対応時間比較に見る、ログ管理基盤の効果 (2017/5/9)
ログ基盤の構築方法や利用方法、実際の案件で使ったときの事例などを紹介する連載。今回は、実案件を事例とし、ログ管理基盤の有用性を、障害対応時間比較も交えて紹介 - Chatwork、LINE、Netflixが進めるリアクティブシステムとは何か (2017/4/27)
「リアクティブ」に関連する幾つかの用語について解説し、リアクティブシステムを実現するためのライブラリを紹介します - Fluentd+Elasticsearch+Kibanaで作るログ基盤の概要と構築方法 (2017/4/6)
ログ基盤を実現するFluentd+Elasticsearch+Kibanaについて、構築方法や利用方法、実際の案件で使ったときの事例などを紹介する連載。初回は、ログ基盤の構築、利用方法について - プログラミングとビルド、Androidアプリ開発、Javaの基礎知識 (2017/4/3)
初心者が、Java言語を使ったAndroidのスマホアプリ開発を通じてプログラミングとは何かを学ぶ連載。初回は、プログラミングとビルド、Androidアプリ開発、Javaに関する基礎知識を解説する。
|
|