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では採用されていない