本について
Amazon: https://www.amazon.co.jp/dp/4822284654
題材として選択したのは、「オブジェクト指向でなぜつくるのか-第2版-」という本です。13章で構成されており、今回は第5章について、まとめました。
ゴール
オブジェクト指向プログラミングのメモリの使い方について説明できる
一般的なメモリの使い方
オブジェクト指向プログラミングの動作環境の特徴は、メモリの使い方にあります。ただ、従来のプログラミングの動作環境と共通する点も多いので、まずは一般的なメモリの使い方について説明がされています。
プログラムのメモリ領域は基本的に、 静的領域、 ヒープ領域、 スタック領域 の3つに分けられます。
静的領域
プログラム開始時に確保され、以降プログラムが終了するまで配置が固定される領域を指します。
ここには、静的な変数であるグローバル変数と、プログラムを実行可能な形式に変換したコード情報が格納されます。
ヒープ領域
プログラム実行時に動的に確保するためのメモリ領域を指します。
アプリケーションから要求されたら、必要なサイズのヒープ領域を割り当て、不要になれば解放します。複数のスレッドから同時に割り当てを要求されても整合性を保てるように、 OS や仮想マシンが管理機能を提供しています。
スタック領域
サブルーチン呼び出しの制御のために使われるメモリ領域を指し、 サブルーチンの引数 や ローカル変数 、 戻り先 の情報が格納されます。
呼び出したサブルーチンの処理が終わる前に、サブルーチンが次のサブルーチンを呼び出す入れ子構造になるので、 LIFO 方式によりメモリを効率的に使用できるようになっている。
オブジェクト指向プログラミングのメモリの使い方
※ オブジェクト指向プログラミングの中でも特に普及している Java を前提に説明されています。
クラスのコード情報がクラスにつき1つだけロードされる
オブジェクト指向プログラミングでは、クラスから生成されたインスタンスが動作することで、プログラムが動きます。ただインスタンスを生成するためには、事前にクラスのコード情報がメモリにロードされている必要があります。
クラスのコード情報がロードされるタイミングは大きく2つに分かれます。ひとつは 事前にすべてのクラス情報をメモリにロードする方式 で、もうひとつは 必要な時点でメモリに逐次ロードする方式 です。
Java は後者で、新しいクラスを使うコードを実行するたびに、ファイルから対応するクラスのコード情報をメモリにロードします。
Java ではクラスのコード情報を逐次ロードする方式を採用していて、実行時にメモリの配置が変わることがあるため、「静的領域」ではなく「メソッドエリア」と呼びます。
インスタンス生成のたびにヒープ領域が使用される
インスタンスを生成すると、そのクラスのインスタンスを格納するのに必要な大きさのメモリがヒープ領域に割り当てられます。またインスタンスを指定してメソッドを呼び出せるように、インスタンスとメソッドエリアにあるクラス情報を対応づけます。
メソッドが書かれたコード情報はクラスごとに1箇所だけに存在し、インスタンスからはその場所を指し示すように管理しています。
従来のプログラミング言語で書いたプログラムは、ヒープ領域を積極的に使うことはありませんでした。なぜなら、不要になったメモリを解放し忘れてメモリリークを引き起こしやすいからです。
オブジェクト指向プログラミングでは、インスタンスを生成するたびにメモリがヒープ領域に割り当てられます。ヒープ領域は有限ですので、インスタンスをむやみに増やし続けると CPU に大きな負荷がかかってしまうということに注意しましょう。
変数にはインスタンスの「ポインタ」が格納される
インスタンスを格納する変数には、インスタンスそのものではなく、インスタンスの ポインタ が格納されます。ポインタ をひと言でいえば、「メモリ領域の場所を示す情報」です。
この方法を使えば、インスタンスの大きさに関係なく、常に同じ形式でインスタンスを管理することができます。
ポリモーフィズムの仕組み
ポリモーフィズムの具体的な実現方法がいくつか考えられますが、ここではメソッドテーブルによる実現方法を前提に説明されています。
各クラスにメソッドテーブルが用意され、そこには各クラスで定義されたメソッドがメモリに展開されている場所、すなわちメソッドのポインタが格納されています。
ポリモーフィズムの関係にあるクラスでは、メソッドテーブルが統一されているため、メソッドの中身が違っていても呼び出し方法を統一することができます。
継承の仕組み
継承されたメソッドのコード情報は、サブクラスではメモリに展開しません。サブクラスのインスタンスが、スーパークラスのメソッドを呼び出せるのは、サブクラスのメソッドテーブルにスーパークラスのメソッドのポインタが定義されているからです。
また、スーパークラスから継承されたインスタンス変数は、ヒープ領域にある全てのサブクラスのインスタンスにコピーされます。
まとめ
- クラスのコード情報がクラスにつき1つだけ制定領域(メソッドエリア)にロードされる
- インスタンス生成のたびにヒープ領域が使用される
- 変数にはインスタンス自体が格納されるのではなく、「ポインタ」が格納される
- メソッドテーブルを複数のクラスで統一することでポリモーフィズムを実現できる
- メソッドテーブルを使えば、スーパークラスで定義されたメソッドをメモリに展開せずとも、サブクラスのインスタンスから呼び出せる
所感
普段の開発ではメモリのことなど気にしたことがなかったので、理解することが本当に難しかったです。
ただメモリの使われ方を知ると、変数のコピーに気をつけないといけないことや、インスタンスを作りすぎると CPU に負担をかけてしまうことなど、今までとは違った視点でプログラミングについて考えることができました。
また呼び出し側のロジックを共通化するという点で Ruby のダックタイピングはポリモーフィズムなのかと思っていましたが、メモリの使われ方を見ると、似て非なるものなのかなとも思いました。