ITシステムの基礎知識を解説(IT素人さん向け)リンクリストに戻る
以前、スタックについて非情技(非ITエンジニア)向けに解説記事を書いた。
スタック(LIFO)という概念
この記事の中で「ヒープについては後日解説する」と書いていたのだが、すっかり忘れていたので、遅ればせながらヒープ領域の解説をしたいと思う。
すっかり忘れて、申し訳ありませんでした。
今回も非情技(非ITエンジニア)向けに解説します。
メモリのレイアウト
以前のスタックの解説でも説明したが、メモリのレイアウトについてもう一度解説しておく。
昔は「メモリモデル」という言葉を使用していたがマルチスレッドの発達で、メモリモデルという言葉は別の意味で使用されるようになり、この言葉は使えなくなった。
とりあえず、メモリのレイアウトという言葉で説明する。
アプリとも呼ばれるプログラムは、通常はHDDやSSD上に「実行ファイル」として保存されている。
Windows なら拡張子「.exe」のファイルがそれだ。
実行ファイルには機械語の命令の束が記述されている。
この実行ファイルを起動すると、OSはプロセスインスタンスというメモリ領域を作成する。
プログラムはOSによって与えられたプロセスインスタンスの中のメモリ領域にしかアクセスできない。
他のプログラムに与えられたプロセスインスタンスやOSが使用しているメモリ領域にはアクセスできないようにメモリ領域が保護されている。
また、プロセスインスタンスは仮想アドレスにより0から始まる連続したメモリ領域を使用できるようになっている。
実際に使用している物理メモリ領域は0から始まる連続したメモリ領域ではない。
プログラムがメモリ領域を使いやすいようにOSがメモリ空間を管理している。
プロセスインスタンスは仮想的なメモリ空間なのだ。
プロセスについては以下の記事で少し解説している。
マルチプロセス(マルチタスク)とマルチスレッドの違いを解説(IT素人さん向け)
プロセスインスタンスは大きく分けて三つの記憶領域に分けて作成される。
その三つの領域は「ヒープ領域」「スタック領域」「コード領域」である。
正確にはもう少し細かく分かれるが、ざっくりとこの三つに分かれると認識していて問題ない。
プログラマーでもこの認識で通用する。(組み込みエンジニアは除く)
OSは実行ファイルを起動したとき、三つの領域でプロセスインスタンスを作成し、「コード領域」に実行ファイルから「命令の束」を読み取りコピーする。
そして、作業用記憶領域として「ヒープ領域」「スタック領域」を確保して、「コード領域」と三つの領域を一纏まりに管理する。
このプロセスインスタンスはプロセス(タスク)ごとに領域を確保する。
同じ実行ファイルを二重起動した場合は、プロセスインスタンスを二つ作成する。
(細かい話をすると二重起動した場合はコード領域は一つの領域を複数プロセスで共有したりするのだが、長くなるので省略する)
スタックの使われ方は「スタック(LIFO)という概念」の中で説明したので、そちらを見て欲しい。
この記事では「ヒープ領域」についてIT素人さん向けに解説する。
「ヒープ領域」とは
以下はプロセスインスタンスの三つの領域を図で表した物だ。
プロセスインスタンスは図で示したようにアドレス0番から始まる連続したメモリ領域である。
しかし、このアドレスはOSによって提供される仮想アドレスであり、物理メモリは0番から始まる連続したメモリ領域ではない。
使用している物理メモリはバラバラのメモリ領域をOSが集めてきて、プロセスに与えた領域である。
プロセスがアクセスする仮想アドレスをOSが物理アドレスに変換して、物理アドレスを使用する。
プロセスは物理アドレスの状態を把握する必要はない。
図で示したように通常はヒープ領域とスタック領域は上下に隣接した配置になる。
コード領域はOSにもよるが、上か下に配置される。
このメモリレイアウトはOSや、Javaや.NETなどVMの仕様によって変わるが、基本的な考え方は皆同じと考えて欲しい。
コード領域の初期化
実行ファイルを起動すると実行ファイルの中の「機械語の命令の束」を、コード領域に読み込む。
そしてコード領域の先頭の仮想アドレスをプログラムカウンタに代入する。
プログラムを実行する場合は、このプログラムカウンタの示す仮想アドレスの命令を読み込みCPUで実行して、プログラムカウンタに一命令長数を加算する。
RISCなら一命令長数は一定なので、例えば命令を一つ実行するたびに4バイト加算して、プログラムカウンタが常に次の命令の仮想アドレスを示すようにする。
スタックとヒープの使い方
スタックは主に「ローカル変数」の領域として使用される。
ローカル変数の配列で連続領域を確保する時もスタックが使用される。
ローカル変数の構造体も同様である。
また、関数やメソッドを呼び出す時に、呼び出した命令の仮想アドレスを保存する事にも使用される。
後で元の仮想アドレスに戻ってくる為だ。
ヒープは動的にメモリ領域を確保する為に使用される。
C言語なら malloc 関数でメモリ領域を確保するとヒープが使用される。
他の言語なら new 演算子でクラスのインスタンスを確保するとヒープが使用される。
先頭から、空いている領域を探し、必要なサイズのメモリ領域を確保する。
使い終わったら開放して、また別の機会に使用される。
スタックとヒープの配置
スタックについては以前の「スタック(LIFO)という概念」という記事で説明しように、スタック領域の下から使う。
つまり仮想アドレスの大きい方から、小さい方に向かって領域を確保する。
逆にヒープ領域は上から使う。
つまり仮想アドレスの小さい方から、大きい方に向かって領域を確保する。
図を見ると分かると思うが、ヒープとスタックは互いに上と下の逆方向からメモリ領域を使用する。
これはメモリ資源が稀少だった時の知恵で、ヒープの使用量が大きくなり過ぎても、スタックの使用量が少なければ、メモリを確保する事ができる。
逆でも同様である。
空いているメモリ資源を有効に使用する工夫なのだ。
ヒープのフラグメンテーション
ヒープ領域は領域を確保して、使い終わったら領域を開放する事を繰り返す。
これを繰り返していくと、確保した領域と領域の間に、中途半端な長さの未使用領域が増えていく。
これが増えてくると、断片的な未使用領域の合計は十分に余裕があるのに、新たに必要なメモリ領域を確保できないという現象が起きるようになる。
例えば、ヒープ領域のサイズが合計100MBあるとして、10MBの確保メモリ領域が5箇所づつ、間に10MBの未使用領域を開けて確保されているとする。
この場合、未使用領域の合計は50MBある。
しかし、連続した未使用メモリ領域は最大で10MBしかない。
この状態で20MBのヒープ領域を確保しようとしてもメモリ不足でエラーになってしまう。
このように確保しているメモリ領域が分散して、未使用領域も断片化して、大きな連続したメモリ領域が確保できなくなる現象をフラグメンテーションと呼ぶ。
C言語や、C++などのネイティブ言語で開発した場合は、このフラグメンテーションを手動で管理する。
ネイティブ言語では、ヒープ領域の管理がかなり困難になる。
だから、ネイティブ言語では極力ヒープ領域を使わずにスタックを使用したり、ヒープ領域全体をブロック状に管理してどの領域を何に使用するか、手動で管理したりする。
スタックならフラグメンテーションは起きない。
ガベージコレクションによるメモリの自動管理
Java や .NET などの VM では、メモリの空き領域の管理をガベージコレクションという機構を使って自動管理する。
ガベージコレクションの備わったコンピュータ言語ではメモリの解放命令が存在しない。
プログラムがクラスインスタンスなどで確保したメモリ領域はガベージコレクションによって自動で開放する。
確保メモリがヒープ領域いっぱいになるとガベージコレクションが稼働して、現在使用されていない確保領域を開放して未使用領域にする。
また、フラグメンテーションにより断片化した確保領域を集約して、連続した未使用領域を作り出す。
これをプログラム実行中に、確保メモリがヒープ領域いっぱいになる度に、実行する事で、メモリの効率的活用を行う機構だ。
ガベージコレクションは VM でしか使用できない。
昔のプログラマーにとってヒープの管理というのは、結構な難問だった。
確保したメモリを解放し忘れて、時間が経つほど空きメモリが現象して、やがてメモリ不足でダウンする「メモリーリーク」に悩まされてきた。
ガベージコレクションの登場はこの難問からの解放を意味する画期的な発明だった。
また、同時に管理の難しいヒープ領域の有効活用にも貢献した。
正直なところ、私はもうネイティブコードで面倒なヒープ領域の管理とメモリーリークの対処をする気になれない。
開発言語を選定する場合はガベージコレクションの使用できるものを選択する事をお勧めする。
効率が全く違う。
業務担当者など非ITエンジニアにもこの点は理解して欲しい。
これでプロセスインスタンスのメモリレイアウトについては、一通り説明したはずだ。
ブログの読まれ方を見ると、意外にこのようなITシステムの低水準の仕組みについての解説記事が読まれるので、時々このような記事も書いて行きたいと思う。
ユーザーが何を望んでいるのかは、今のところハッキリとは分かっていない。
色々と書いて試してみようと思っている。
ITシステムの基礎知識を解説(IT素人さん向け)リンクリストに戻る