みなさんは Segmentation Fault に遭遇したことがありますか?
私はあります。ターミナルにSegmentation Fault (core dumped)
とだけ表示され、まともなログもないので何からどうしたらいいかわからず非常に苦労しました。「せぐふぉ」と略すと可愛い気がしますが、その実かなりめんどくさいやつです。
最近 Java の例外について勉強したとき、「これは何で Segmentation Fault にならないんだろう?」となったポイントがありました。ここでは Segmentation Fault の復習も兼ねて、Java が Segmentation Fault を起こさない仕組みについてまとめてみます。
Segmentation Fault とは?(一般)
ひとことで言うと、「プログラムが不正なメモリアクセスを行ったときに発生するエラー」になります。
C や C++ではポインタを使用してメモリアクセスを直接管理できます。これは柔軟性に優れる一方で、不適切なポインタ操作を行うと不正なメモリアクセスに繋がります。
一方、Java や Python のような言語では、ガベージコレクションなどの機能によりメモリ管理が自動化されているため、メモリ管理をプログラマが行う必要は通常ありません。またエラーハンドリングもしっかりしているため、Segmentation Fault が発生することは基本ないでしょう。しかしながら、このような言語でも C 系の外部ライブラリを使用している場合には Segmentation Fault が発生する可能性があります。
さて、「不正なメモリアクセス」とは具体的になんでしょうか?
Segmentation Fault が発生するケースとして、主に次のような場合が考えられます。
Segmentation Fault が起きるケース
-
不正なポインタアクセス
無効なメモリ領域を指すポインタへのアクセスをしようとすると発生します。
e.g. 初期化されていないポインタ、NULL ポインタの参照 -
バッファオーバーフロー
配列外のメモリへの書き込みや読み込みをやろうとすると発生します。 -
スタックオーバーフロー
再帰呼び出しが深すぎてスタック領域を超えてしまうような場合に発生します。 -
読み取り専用メモリへの書き込み
読み取り専用としてマークされたメモリ領域に対して書き込み操作を行おうとすると発生します。
これらのケースを把握しておくことで、Segmentation Fault の原因特定がやりやすくなるのではないでしょうか。
本題: Java で Segmentation Fault が起きない仕組み
前述したように、Java や Python のような言語には Segmentation Fault を発生させないような仕組みが備わっています。(無理やり意図的に発生させることもできるみたいです。12)
ここでは、前節で上げた 4 つのケースについて、「Segmentation Fault を発生させない仕組み」がどのようなものかを紹介したいと思います。お題は Java です。
1. 不正なポインタアクセス
これに関しては、Java ではポインタ操作を行わないためそもそも発生しません。しかし、参照がnull
の状態でメソッドを呼び出してしまう、ということは起こり得ると思います。これはある意味で不適切なメモリアクセスと言えるでしょう。そしてそんなときに発生する例外がNullPointerException
でした。
public class NullPointerExample {
public static void main(String[] args) {
Object obj = null;
obj.toString(); // NullPointerException
}
}
2. バッファオーバーフロー
Java では、バッファサイズを超えるアクセスを行った際のエラーハンドリングとしてArrayIndexOutOfBoundsException
が存在します。これにより、バッファオーバーフローが起こりそうになっても Segmentation Fault は発生しません。
public class ArrayIndexExample {
public static void main(String[] args) {
int[] numbers = {0, 1, 2, 3, 4};
for (int i = 0; i <= numbers.length; i++) {
System.out.println(numbers[i]); // ArrayIndexOutOfBoundsException (i = 5)
}
}
}
3. スタックオーバーフロー
例えば下記のような、再帰的な呼び出しを無限に行うコードを実行してみましょう。
public class StackOverflowExample {
public static void main(String[] args) {
recurse();
}
public static void recurse() {
recurse(); // StackOverflowError
}
}
この場合はStackOverflowError
が発生します。
で、ここで「これってメモリリークのことか?」と思ったのですが、メモリリークとスタックオーバーフローは違います。
スタック領域とはメモリの中でもローカル変数やメソッドの情報を格納する領域のことです。3再帰的なメソッド呼び出しを無限に行ってしまうと、どこかの時点でコールスタックがメモリの限界を超えてしまうことになります。このようなプログラムを実行するとStackOverflowError
が発生し、即座にクラッシュします。
メモリリークとは、ガベージコレクションが不要なオブジェクトを解放できずに、ヒープ領域が消耗される現象です。メモリリークによりヒープ領域の確保が不可能になると、OutOfMemoryError
が発生します。
import java.util.ArrayList;
import java.util.List;
public class OutOfMemoryExample {
static List<Object> objects = new ArrayList<>();
public static void main(String[] args) {
while (true) {
objects.add(new Object());
}
}
}
上記のコードを実行すると、OutOfMemoryError
が発生します。実際にやってみるとわかりますが、クラッシュするまでに多少の時間がかかります。これは動的なオブジェクト生成を大量に行った結果としてヒープ領域が不足し、エラーが起こるという経過を辿るためです。
整理すると、OutOfMemoryError
はヒープ領域に関するエラーであり、大量のオブジェクト生成やメモリリークが原因となります。StackOverflowError
はコールスタックが対象であり、不適切な再帰やネストの使用が原因となります。
4. 読み取り専用メモリへの書き込み
Java では実際のメモリへの直接アクセスではなく、オブジェクトや参照を通してメモリ管理を行います。そのため C や C++で起こるような、「読み取り専用メモリへの書き込み」による直接的なメモリエラーは通常発生しません。
余談: もし Segmentation Fault が存在しなかったら?
ここまで、Segmentation Fault が不正なメモリアクセスによるエラーであること、Java では Segmentation Fault が発生しないような仕組みが備わっていることを見てきました。
Segmentation Fault は厄介なエラーですが、もしこのエラーが発生せずに「不正なメモリアクセス」を行うプログラムが実行できてしまったとしたら、何が起こるでしょうか?
不正な書き込みが許可されてしまうため、プログラムは別の変数やメモリ領域を破壊しながら実行が続くことになります。場合によってはソースコード自体が変更されてしまうかもしれません。また、セキュリティの脆弱性という点でも問題があり、過去にはバッファオーバーフローを利用した攻撃が行われた事例もあります。4
Segmentation Fault には不正なメモリアクセスを検出し、プログラムを強制終了させるための保護機構という重要な役割もあるということですね。
ご指摘があればコメントお願いします!