シリーズ第2弾はプログラムとメモリの関係です。
最近の言語ではあまりメモリだのポインタだのを意識しなくてもかけてしまうので、最近プログラムを始めた人はあまりメモリを意識しないでプログラムを書いている人も多いと思います。
簡単な画面や内部処理を実装するうえではそれでもいいかもしれませんが、少し複雑な処理の設計・実装や処理速度を意識したプログラムとなるとメモリの使い方が重要な要素となってきます。
プログラムを始めるときには、そのロジックによってコンピュータ内部で何が起こるのかを意識して勉強するようにしましょう。
前回の記事で書いたように、現在のコンピュータは(ほとんど)CPU、メモリ、ストレージの組合せでできています。
(それ以外にもマザーボードやグラボやネットワークカードなどもありますが、基本てな構成という意味で)
プログラムを書く上で一番気を付けなければいけないのがメモリの使い方です。
というのも、プログラムでは変数を使用しますが、変数に値を入れるということはメモリに書き込むことを意味しているからです。
変数を読んだり書いたりするということはメモリから読み込んだり書き込んだりすることを意味します。
読み書きの回数が多くなったり、大きなデータを書き込んだりするとプログラム全体の処理が遅くなったり、メモリーの枯渇などの障害の原因になったりします。
自分のプログラムによって、メモリ内で何が起きているかを意識しながらプログラムを書くようにしましょう。
(注意)
私もメモリの中身を可視化して見たわけではありませんし、言語ごとに動作も違います。
なので、ここで説明するのは「こんな感じのことが起きている」という説明になります。
ただ、この考え方でプログラムを20年以上書いていてたくさんの課題や難題を解決してきましたので大きな間違いはないと思います。
メモリの確保
以下のようなプログラムを書いたとします。
int i1 = 100;
JAVA的な書き方です。
最近の言語では型宣言がいらないものが多いですが、内部で自動的に型を判定してメモリに確保しています。
このプログラムが実行されるとメモリには以下のように記録されます。
かなり、抽象的な書き方ですがこんなイメージです。
本当は、型や長さを示すビットと値のビットが2進数で記録されます。
また、長さで指定された領域も確保されます。
今回は”100″ですが、intの最大値65535(言語によって違うけど)まで入力できる領域が確保されます。
文字列の場合はどうなるでしょう。
String s1 = "abc"; String s2 = "abcdefghij";
文字列には最大の長さがありませんので、データの長さに合わせてメモリを確保する必要があります。
そのため、変数を示すメモリには値が格納されたメモリのアドレスを格納し、データが保存された領域を参照するようにします。
もし、一つの領域で領域が足りない場合は別の領域も利用するため2つの領域を変数として使用する断片化が発生します。
このようにすることでメモリを全て使い切るまでデータを格納することができるようになります。
以上のように、型によってメモリの格納方法が異なってきます。
intのように変数を表すメモリ領域に直接値を登録する型をプリミティブ型、
Stringのように格納した領域を表す(ポインタ)アドレスを格納する型をオブジェクト型といいます。
NULLとは
ではNULLとはどういう状態でしょう。
空文字とNULLはどのように違うのでしょうか?
以下のようなプログラムを実行したらメモリの中はどうなるのでしょうか?
String s3;
String S4 = "";
s3は変数のみを宣言していますが、インスタンスは設定されていません。
それに対してs4は空文字のインスタンスが設定されています。
つまりメモリ上は以下のようになります。
String s5 = "abc";
s5 = null;
このようにインスタンスを設定していた変数にnullを設定するとメモリ内は以下のような状態となります。
クラスの使用
クラスの場合はどうでしょう。
クラスは内部に変数やメソッドを内包することができます。
クラスは宣言しただけではメモリにロードされません。
インスタンス化して初めてメモリが確保されます。
class C {
int i1;
String s1;
}
C c1 = new C();
c1.i1 = 100;
c1.s1 = "abc";
厳密にはちょっと違いますが、イメージ的にはこのような配置になると考えてください。
赤い枠の部分がインスタンスになります。
String型と同じようにクラスのインスタンスを作成すると変数を表す領域にはクラスが格納されるアドレスが登録されます。
呼び出したり書き込んだりする場合はこのアドレスの位置を参照して行います。
ここで、以下のようにするとどうなるでしょう。
C c2 = c1;
c2.i1 = 200
このように、代入することで同じアドレスを参照するようになります。
つまり同じインスタンスを参照するため、内部の変数の値を変更すると代入元のインスタンスも変更されてしまいます。
このように、オブジェクト型の場合、プリミティブ型の場合ではメモリ確保の仕方が異なりますので、プログラムを書く時には注意が必要です。
また、こういった機能をうまく利用することで効率のいいプログラムを書くことも可能です。
メモリの開放
では、確保したメモリはいつ解放されるのでしょうか?
最近の言語ではガベージコレクション(GC)が主流となっています。
ガベージコレクションとは、いらなくなったメモリを勝手に開放する自動お掃除ロボットのようなものです。
でも、何でもかんでも勝手に掃除されてしまったら「まだ使ってるのに捨てちゃダメじゃん」ってことになりますよね。
なので、使っている/使って内を以下のように判断しています。
処理ステップのスコープ外のメモリ
JAVAの場合、基本的に中カッコ”{}”で囲まれて部分がスコープとなります。
例えば以下のようなプログラムを実行したとします。
Class StrAddClass {
private String resultString;
public static void main(String[] arg){
StrAddClass addCls = new StrAddClass();
addClass.setResultString("aaa");
addClass.addString("bbb");
addClass.addString("ccc");
System.out.println("処理結果: " + addClass.getResultString());
}
public String getResultString(){
return this.resultString;
}
public void setResultString(String value){
this.resultString = value;
}
public void addString(String value){
String separator = ":";
this.resultString += separator + value;
}
}
このプログラムを実行するとmainメソッドが呼び出され、”aaa”、”bbb”、”ccc”という文字列がセパレータ”:”で区切られた文字列を作成して標準出力に出力します。
8行目でaddStringメソッドが実行され、22行目でseparatorが宣言されてインスタンス”:”が設定されています。
23行目でプライベート変数、引数、separatorが結合されて戻り値で返します。
9行目でも同様の処理を行い、10行目で標準出力されています。
先ほどの22行目で作成されたseparatorのインスタンスにアクセスできる範囲(スコープ)はどこからどこまででしょうか?
スコープは先ほど説明した通り中カッコで囲まれた間なので、22行目~23行目ということになります。
処理が9行目に移った時点で、このスコープから外れますので、separatorのインスタンスはスコープ外となり、ガベージコレクション対象となるというわけです。
9行目を実行すると同じ処理が実行され、22行目でseparatorのインスタンスが生成されますが、8行目で実行されたときのインスタンスとは別のものになりますので、ガベージコレクションが実行されたとしても問題ないということになります。
アドレスを参照している変数がすべて解放されているメモリ
先ほどのプログラムを以下のように修正しました。
Class StrAddClass {
private String resultString;
public static void main(String[] arg){
String result = createString();
System.out.println("処理結果: " + addClass.getResultString());
}
private static createString(){
StrAddClass addCls = new StrAddClass();
addClass.setResultString("aaa");
addClass.addString("bbb");
addClass.addString("ccc");
}
public String getResultString(){
return this.resultString;
}
public void setResultString(String value){
this.resultString = value;
}
public void addString(String value){
String separator = ":";
this.resultString += separator + value;
}
}
先ほどのmainメソッドからcreateStringメソッドを呼び出し、その中でStrAddClassクラスのインスタンスを生成しています。
この場合、StrAddClassインスタンスのスコープは11行目~14行目ということになり、7行目に処理が映った時点でStrAddClass内で作成したインスタンス(26行目のseparatorや3行目のresultStringなど)はすべてスコープ外となり、ガベージコレクションの対象となります。
大体、この二つです。(プリミティブ型の場合、即座に解放される言語もありますが)
スコープとは、プログラムを実行している行を含むメソッドのことです。
変数はクラスやパブリックとしても宣言できますよね。
クラス変数の場合はクラスのインスタンスが解放されたとき、パブリック変数の場合は明示的に参照しないようにプログラムしないと解放されません。
メモリの枯渇
プログラムの利用できるメモリ領域は基本的に限られています。
領域サイズは実行時に指定することができますが、そのサイズを超えると”Out of memory error”などのシステムエラーとなりシステムダウンとなります。
(最近のWebAppサーバーは1アプリでメモリが枯渇しても落ちませんが、数年前は1アプリがメモリ枯渇を起こすと全システムが停止していました)
データベースからデータを無制限に読み込むとメモリは枯渇してしまいます。
そのため、今回説明したメモリの確保や解放の仕組みをしっかりと理解して、上手にメモリを開放しながらシステムを動かすプログラムを書くことを心がけてください。
最近はJAVAでもJAVAVMメモリのみでなく、ネイティブメモリも利用できるようになりましたがメモリ開放ができない場合が発生しており、逆にメモリリークが起こるようになってしまいました。
言語ごとの特性をよく理解して、プログラムを書くようにしましょう。