オブジェクト指向プログラムが動く仕組み
Java 等、ほとんどのオブジェクト指向プログラミング言語は、実際に計算機上
でどのような機構によって動作しているかをプログラマは知っている必要はな
いように設計されています。しかし、それら言語は特にメモリの使い方に特徴
がありますので、内部機構を全く知らずにプログラミングしていると、メモリ
を浪費してマシンパワーを圧迫したり、バグが出た時に対応ができないことが
あり得ます。
ということで、プログラムの動作機構について最低限のことは理解してお
く必要があると思いますので、ここで簡単にみていきます。
中間コード方式としての Java実行環境
既にみたように、プログラミング言語の実行には、事前に機械語におとして一
括実行するコンパイラ方式と、1ステップずつ逐次実行するインタープリタ方
式がもともとあり、その両者のメリットのいいとこどり=処理速度を保ちつつ、
機械への依存を出来るだけへらすため、中間コード方式というものが
考えられ、Java はこの方式をとっています。ハードウェアの進歩と多種多様な
マシンが共存するインターネット環境という背景から、現在ではすっかり定着
しています。
ちなみに、文法が規定されている「言語」と、その「実行環境」は、本質的に
は独立です。Java や VB でかかれたプログラムを純粋な機械語にコンパイルす
るシステム(ソフトウェア)を開発すれば、もちろん動きます。とはいえ、VB
は MS が、Java はSun Microsystems (Oracle に吸収合併)が提供する環境が
実質的標準 (de facto standard) 環境ですので、中間コード方式という説明を
するわけです。
プロセスとスレッド
CPUが1つしかなくても複数のプログラム=プロセスが時分割されて擬似的に同
時並行的に動かすことができる、というのがマルチプ
ロセス環境で、現在のシステムではあたりまえになっています。さらにも
う少し細かくみてみると、1つのプロセスの中でもいくつかの役割分担(入出
力、計算処理、…)をしていて、それらが比較的独立している場合には、プロ
セス内をさらに細かい単位でわけて、CPUを効率的に使うことができます。
たとえば入出力は非常に時間がかかり、さらに待ちの多い
ルーチンとなるため、別扱いで普段は寝ていてもらう、
のようなことをするわけです。
この細かい単位が「スレッド」で Java はマルチスレッド環境をサポートして
います。マルチプロセスとの違いですが、一般的にスレッドは(CPUは別々に使
いますが)メモリの一部(後述)を同じプロセス内で共有できることが多いで
す。このため、スレッド間の情報交換は非常に簡単ですが、その反面相互依存
性があるので、1つのスレッドの不具合は別のスレッドにも影響します。ある
システムをマルチスレッドにするかマルチプロセスで構成するかは、メモリ資
源、入出力の負荷等いろいろな観点から考えるべきでしょう。
スタティック・ヒープ・スタック
まずオブジェクト指向かどうかはさておき、
そもそも、現在(OS上で動く)プログラムの
動作環境として、メモリを使い分けている、ということを
知っておかなくてはいけません。それがすなわち
- スタティック(静的)領域
- ヒープ領域
- スタック領域
です。
スタティック領域
スタティック領域とは、プログラムの開始時 (Windows だと
Explorer でダブルクリックした瞬間?) に確保され、プログラムが終了するま
で固定されて置かれる領域です。「スタティック(静的)」と呼ばれ
るくらいで、この領域に格納される情報の配置は実行中変化しない、というこ
とで、グローバル変数と、プログラムの(機械語レベルの)実行
形式が置かれています。
ヒープ領域
ヒープはプログラムの実行時に動的に確保するためにOSが用意して
おいてくれる領域です。これは、プログラムの実行中にメモリが必要になった
アプリケーションが必要なサイズを要求することで割り当てられ、逆に不要に
なれば元に戻す、という使い方をします。実際には整合性の制御など、複雑な
機能をOSが行っています。C では malloc()/free() 関数(システムコール)
で確保/解放をおこないますし、Java の new でオブジェクトを作ると
きにもここが使われます(後述)
スタック領域
スタック領域は、ローカル変数や、関数(メソッド)呼び出し
の引数・戻り値 用の領域等を確保するために使われます。前述のス
レッドに関連して、ヒープはスレッド間で共有されますが、スタックはスレッ
ドごとに個別にもちます。スタックというのは積み重ねる、ということで、い
わゆる「後入れ先出し」(Last In, First Out, LIFO) の構造になっています。
これは、関数呼び出しが入れ子になって続く時に、とにかくいま動いている関
数についての領域を一番上においておき、関数から戻るとその領域を解放して、
その呼び出し元の関数について動く、というわけです。
たとえば、コードが動く様子を考えると、以下のようになります、
main() {
funcA();
funcB();
}
funcA() {
funcC();
}
以上を整理すると、以下のような表にまとめられるでしょう
| スタティック | ヒープ | スタック |
使われ方 |
アプリケーション開始時に確保 | 開始時に一定領域がOSで確保され、
必要になればアプリケーションに割り当て | 後入れ先出し |
格納される情報 |
グローバル変数、実行コード | 任意 | ローカル変数、引数
&戻り値用領域 |
確保される単位 |
プロセス(アプリケーション)で1つ | システム(またはプロセス)
で1つ | スレッド毎に1つ |
オブジェクト指向のメモリの使い方
以上が理解できたところで、ではオブジェクト指向言語の動作環境はどうなっ
ているか、ということを Java を代表として考えます。
他の言語 (C++, .NET, ruby, ...) で微妙に異なるところはありますが、まず
は当面の対象としている Java を掘り下げておくことで、他の言語を知るべき
時がきたときにも理解が容易になると思います。
クラス情報は1つだけメモリにおかれる
Java が動くときにはクラスからインスタンスが作られて動くわけですが、実際
にプログラムが動くときにはどうなっているかというと、インスタンスを作る
前に、対応するクラスの情報がメモリにある必要があるわけです。こ
こでクラスの情報とは、Java だと static で宣言されているクラス変
数と、メソッドのコードです。インスタンス変数はインスタン
ス毎に別々にもつということは、ここには関係ないわけです。
このクラス情報のメモリにおくのに、事前に(プログラム起動時に)すべてメ
モリにおくというやり方もあります (C++ はこの方式)が、Java では必要になっ
た時点でメモリに逐次ロードする、というやりかたを採用しています。これは、
メモリ効率を考えると同時に、ネットワーク分散されてる場合などの動的結合
も可能としています。ただ、新しいクラスのロードが頻繁におこると、オーバー
ヘッド=処理時間のロスがでて、処理速度は犠牲になります。
ちなみにこのクラス情報は、上の説明でいうスタティック領域におかれますが、
Java では必要なクラスが動的にロードされるのでスタティック=「静的」では
ない、ということで「メソッドエリア」と呼びます。
インスタンス生成はヒープに
ではインスタンスはどうなっているかというと、インスタンスを生成すると、
そのインスタンス変数に必要な領域と、メソッドエリアへの対応付けのための
情報の領域がヒープに確保されます。
構造化プログラム(以前)では、ヒープの割り当て/解放処理は重い(管
理に時間がかかる)のにくわえて、使い終わったあとの解放のところでバ
グが出やすく、特に解放しわすれるとプログラムのメモリ使用がどんどん増え
てシステムを圧迫する、いわゆるメモリリークを起こしてしまう可能
性があるため、あまり積極的に使うものではありませんでしが。しかし、Java
に代表される最近のオブジェクト指向プログラムではインスタンスは原則すべ
てヒープに配置されます。言い方をかえると、遠慮なくどんどんヒープを使っ
ている、ということで、これは、後述の GC のおかげてプログラマが解放から
開放されるので安心、というわけです。
とはいえ、メモリと同時に CPU も消費するので、大量のインスタンス生成など
は厳しいかもしれないですので、もしパフォーマンスがでない場合は注意が必
要です。
変数にはインスタンスのポインタが格納
では、そのインスタンスをどう操作しているか、ですが、たとえば
public void execute(String fileName) {
// ...
File f = new File(fileName);
// ...
}
のようにインスタンスを生成して f で受けているとき、この f にはいってい
るものはそのインスタンスそのものではなく、そのポインタ(=
メモリのアドレスのようなもの)であることを理解する必要があります。
このようにすることで、インスタンスの大きさに関係なく同じ
形式でインスタンスを利用できます。
ちなみに C++ では、インスタンスそのものを変数にいれることも可能ですが、
Java ではそれを禁止してヒープで管理することで、後述の GC でのメモリ管理
の自動化も可能としているわけです。
インスタンスを示す変数の代入はポインタのコピー
上に関連して、インスタンス変数の値を別の変数に代入する、
ということは、ポインタをコピーしているわけで、
インスタンスの実体をコピーしているわけではない
ということは注意が必要です。つまり、
public void execute(String fileName) {
// ...
File f1 = new File(fileName);
File f2 = f1;
// ...
}
としたときには、f1 も f2 も同じファイルを操作しようとしている、というこ
とです。もちろんインスタンス変数も同じものを使うので、一方 (f1) で変化
させた情報はもう一方 (f2) の操作に影響をあたえてしまいます。それが嫌な
ら;
public void execute(String fileName) {
// ...
File f1 = new File(fileName);
File f2 = new File(fileName);
// ...
}
とインスタンスを別々に生成しないといけません。
孤立したインスタンスは GC が処理
前述したようにGC (garbage collection) はゴミ集め=不要になっ
たヒープ上のメモリを回収する、ということで、プログラマにとって
は便利ではありますが、その分システム (Java VM) には複雑で重い処理で、完
璧なものはなかなか実現できない=メモリリークは100%おきないとは言え
ないです。ここでは、考え方として、どのように GC が行われるか=不要なメ
モリを見つけて処理しているかを簡単にみていきます。
GC をするのももちろんプログラムなわけですが、それは専用のものが、Java
の場合 VM (実行環境)の機能として持っています。プログラムは定期的にヒー
プの状態を調べ空きメモリが少なくなるとGC を動かします。Java の場合、不
要なメモリとは、もはや不要となったインスタンスの領域なわけです。
では「不要」をどう判断するか、ですが、「孤立したインスタン
ス」を探す、ということになります。
Java プログラムはクラスからインスタンスが作られ、そのインスタンスに対し
てメソッドを動かして処理をすすめていくわけです。そのインスタンスは別の
インスタンスを使うこともできるわけで、このようにして連鎖的にたくさんの
インスタンスが作られ、参照情報のネットワークのようなものが作られるわけ
です。インスタンスは実際にはポインタが変数に代入され操作しているわけで
すので、上のネットワークはヒープだけではなくスタック
や(スタティックに対応する)メソッドエリアにも関連していきます。
スタックはローカル変数・メソッドの引数などの領域で、その変数にインスタ
ンスを指定することは可能であるということですから、スタックにはヒープに
存在するインスタンスのポインタが格納されます。メソッドエリアにある
static な変数にももちろんポインタを代入できます。スタックとメソッドエリ
アは、その時点でのプログラムの処理で使われる可能性があるので、ここから
間接的にでも参照されているインスタンスは「不要ではない」=
GC の対象にはならない、というわけです。逆に、これらから辿れな
いものが GC の対象になるわけです。
プログラムの動作に応じてスタックは生成したり消滅したりしているので、
過去のある時点でメソッドを動かして、スタックにのっかっている変数にイン
スタンスを生成させてうけて処理をしていたとします。しかし、そのメソッド
が終了するとそのスタック領域は解放されるわけで、その時点でインスタンス
を指し示すものがなくなってしまう、というようなことが頻繁に起こっていま
す。
図示すると、以下のようになります。
この図で矢印は「参照している」を意味しています。ここでは、A は
もちろん生きている、すなわち、これからもプログラムで利用される可能性が
ある、ということです。B も、生きている A から参照されているので生きてい
ます。D は何からも参照されていないので、今後使われる可能性はない、すな
わち不要となっています。C は分かりにくいかもしれませんが、不要な D から
参照されていても利用される可能性はありませんし、生きている B を参照はし
ていますが、B から参照されているけではないので、やはり利用される可能性
はありません。すなわち、A,B は必要ですが C,D は不要= GC
の対象となり、回収されてその分のメモリ領域が再利用される、というわ
けです。
プログラマーの立場からすると、スタックやメソッドエリアから参照するイン
スタンスは気をつけて、不要なものは極力参照しない、ということなのですが、
実際にはなかなか気付きにくいものなので、大量にリンクされているとかでな
い限りはシステムにお任せでもいいでしょう。ただ、メモリ不足でプログラム
が落ちたりすることがあれば、この点に注意してバグをさがすのを最初にする
といいかもしれません。
あと、多態性は違うクラスが同じ表現になったり、継承される情報の種類によっ
てメモリ配置は異なったりするのですが、詳細は略します。興味のある人は自
分で調べてみてください。
講義用スタイル
印刷用スタイル