基盤ソフトウェア 5/22

Javaの仮想機械の話をずっとしてきました

先週は何の話をしていたかというと
Escape Analysis
メモリ管理

  • -
メモリは巨大な区画 どの辺が使用中か記憶しておく オブジェクトがいらなくなって使わないということになったら そのメモリ(区画)は使用中から未使用になって また使えるようになる スタックフレーム もう少し高速なやりかたはないかということで 生み出されたテクニック あるメソッドを呼んだとき メソッドを呼んだ間しか使わないことが判っている変数は スタック領域に割り当てられる メソッドが呼ばれている間だけ確保される メソッドが終わったらまた他の用途のために使用される メソッド呼び出しは必ず、 最初に呼び出したメソッドが最後に終わる main -> run -> readLine readLineが終わらないとrunは終わらない メソッド呼び出しが深くなればなるほど、 使用領域が増えていく メソッドが終わるたびに浅くなっていく このように簡単な動き方をするのでメモリ管理も簡単 Cだとスタティックな大域変数など様々な種類の変数があるけれど こういうスタック領域にしまえるデータはautoな局所変数だけ 普通のヒープにとるオブジェクトだと newの度、mallocの度に確保する スタック領域の場合はどかっと確保する このサイズはコンパイルのときに決める ではどうしてmallocなどはコンパイルするときに決められないのか C言語の場合はmallocは引数でサイズを決められるし ループの中にnewやmallocがあった場合だめ JavaはStringを大量に使っていて プログラムを解析してStringが局所的にしか使われないのであれば オブジェクトをスタックフレームに割り付けて高速化 これがEscape Analysis じゃあスタックフレームに置けるものは? for文の中に変数宣言があって、 kという変数が宣言されていて、あるメモリ領域を割り当てても、 ループ2回目でも同じ場所をkのために使用できる 解析をして、1つ割り当てれば問題ないということがわかれば それはスタックフレームに置くようにする C言語の場合、メモリをどこに置くかかなり細かくコントロールできる 例えばC言語で書かれたfooという関数 int foo(){ int a[5]; -> Stack a[0] = 1; : int b; -> ポインタをStack } a[5]という配列がでんっと割り当てられる retunr aとやると非常に危険 Javaの場合 int foo(){ int a = {'1', '2'}; -> Heap } Javaでa[]と定義すると、aという変数(リファレンス)はStack 配列をさしているリファレンスと配列オブジェクトの両方が作られる オブジェクトはHeap領域に作られる C言語でプログラムをするのはこのあたりを十分に理解しておかなければならない 非常に危なっかしい オブジェクト指向わからないけどCは書けるけどCのメモリモデルをろくにしらない人は 本当の意味でCのプログラマではない Javaだと安全なパターンしか使えない ただし安全装置をはずしても問題ないとコンパイラが判断した場合は、 スタックフレームに置く
  • -
Method dispatch オブジェクト指向の基本中の基本 オブジェクト指向メソッドを呼ぶとどのメソッドが呼ばれるかは 実行してみないとわからない メソッドを呼んだらそのオブジェクトに定義されたメソッドを探して なければスーパークラスを探したりする いちいち探さないといけないので時間がかかる Cの関数とJavaメソッド呼び出しは文法上は似ているけれど 実際に行われていることはかなり違う Cの関数呼び出しは早い スタックかヒープかの話と同様に、 メソッドディスパッチは便利だけど早い オブジェクト指向には Actual typeとApparent typeという二つの型がある Actual type ↓ Person p = new Student(); ↑ Apparent type Actual typeは実際のクラス Apparent typeそれを指す変数のクラス リファレンスの型 Actual typeにしかないメソッドを呼び出したい場合は キャストを行わなければならない SmalltalkとかRubyみたいに動的に型が決まる言語は この2つの区別はない Actual typeしかない しかし存在しないメソッドを呼び出したら実行時に例外が発生する Javaであれば普通はそのようなことは起こらない ただし2つの型を考えなければならない 一長一短がある メソッド呼び出しのスピードアップをするには この話が非常に重要になる C++の場合 Apparant typeはコンパイル時にわかっている この型のクラスかそのサブクラスのメソッドが呼ばれる Vitual function table(メソッド表)を作る 同じメソッドは同じ行にくるようにメソッド表を作成 person->getAge()だったらperson->vtbl[2]()のようにやって 実行時に表の検索が不要になる Cの関数呼び出しよりはちょっと遅くなるけど結構早い ただし注意深くやらないといけない スタティックに型が決まる言語の場合このテクニックが使えるが、 ダイナミックに型が決まる言語の場合は表がでかくなりすぎて使えない 潜在的には全部のメソッドが呼ばれる可能性がある しかしそれだけではない プログラムに5個くらいしかクラスが出てこないことが判っていても使えない 何故ならテーブルの位置が揃えられない 頑張っても、表に空きがたくさん出来てしまう Sparse array メモリが無駄 もっと大きな問題は分割コンパイルが出来なくなる プログラムの中にある全部のメソッドがわかってなければならないので 使う可能性のあるクラスは一緒にコンパイルしなければならない これは現実的ではない C++の多重継承 この場合はメソッド表を作るのがちょっとややこしくなる メソッドが表の何番目か決められなくなる 多重継承というのは実装面からもあまりよくない Javaでは採用されていない