1. ローカル変数はどこに保存されるか
まずは基本から。ローカル変数とは、メソッド内で宣言され、そのメソッドの実行中にのみ存在する変数です。寿命は短く劇的です。メソッドが終了すると、すべてのローカル変数は跡形もなく消えます。
Java ではローカル変数はスタックに保存されます。各スレッドには独自のスタックがあります。あるメソッドが別のメソッドを呼び出すと、スタックには新しい「フレーム」(stack frame)がローカル変数と戻りアドレスとともに積まれます。メソッドが終了すると、そのフレームはスタックから取り除かれます。
例: ローカル変数の一生
public class LocalVariableDemo {
public static void main(String[] args) {
int a = 42; // ローカル変数 a は main の中でのみ生存する
printSquare(a);
// ここでは変数 b はもう存在しない!
}
public static void printSquare(int b) {
int square = b * b; // ローカル変数 square
System.out.println("平方: " + square);
// printSquare を抜けると、すべてのローカル変数は消える
}
}
重要なポイント: ローカル変数が 参照(たとえば String、Scanner、配列)である場合、参照自体はスタックにありますが、オブジェクトはヒープにあります。参照が消え、他に参照がなければ、ガーベジコレクタがオブジェクトを削除できます。
図解
Stack (main 用):
| int a = 42 |
| args |
-------------------
Heap:
| [new で作成されたオブジェクト] |
2. Java のメモリリーク: 神話か現実か?
多くの初心者はこう考えます。「Java にはガーベジコレクタがある! だからメモリリークは起きないはずだ!」残念ながら、これは大規模プロジェクトの最初で打ち砕かれる神話です。
ガーベジコレクタは、生きた参照がひとつもないオブジェクトだけを削除します。どこかに参照が(たとえ意外な場所にでも)残っていれば、そのオブジェクトはいつまでもメモリにぶら下がり続けます。あるいは OutOfMemoryError になるまで。
例 1: static コレクションという落とし穴
import java.util.ArrayList;
import java.util.List;
public class MemoryLeakDemo {
// ああ、この static コレクション!
private static final List<String> BIG_LIST = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 1_000_000; i++) {
BIG_LIST.add("文字列番号 " + i);
}
System.out.println("100万個の文字列を追加しました");
// main が終わっても、JVM が生きている限り BIG_LIST はメモリに残る
}
}
何が起きている?
- static 変数 BIG_LIST はクラスが生きている限り(通常は JVM の終了まで)生存します。
- リストに追加されたすべての文字列は、BIG_LIST を通じて常に参照があるため、ガーベジコレクタは削除できません。
- このようなコレクションをうっかり掃除し忘れると、メモリリークになります。
例 2: 解除し忘れたリスナー(listeners)
import java.util.ArrayList;
import java.util.List;
class EventSource {
private final List<Runnable> listeners = new ArrayList<>();
public void addListener(Runnable listener) {
listeners.add(listener);
}
// ... その他のメソッド ...
}
public class ListenerLeakDemo {
public static void main(String[] args) {
EventSource source = new EventSource();
Runnable listener = () -> System.out.println("イベント!");
source.addListener(listener);
// source.removeListener(listener) を呼び忘れると、listener は永遠にメモリに残る!
}
}
問題: リスナーが不要になってもリストから削除しなければ、そのリスナーと、リスナーが参照しているすべてのオブジェクトはメモリに残ります。
例 3: 決してリセットされないキャッシュ
import java.util.HashMap;
import java.util.Map;
public class CacheLeakDemo {
private static final Map<String, byte[]> CACHE = new HashMap<>();
public static void main(String[] args) {
for (int i = 0; i < 1_000_000; i++) {
// 毎回 1 KB の配列を作成
CACHE.put("key" + i, new byte[1024]);
}
System.out.println("キャッシュに100万件を追加しました");
// キャッシュが増大し、メモリが尽きて OutOfMemoryError!
}
}
結論: GC があっても、オブジェクトのライフサイクルを管理しなければ簡単にメモリリークを起こします!
3. 弱参照(WeakReference)とその仲間
ときには、他から参照されなくなったオブジェクトをガーベジコレクタが削除してよいようなキャッシュやコレクションが必要になります。そのために用意されているのが弱参照(WeakReference)です。
通常の(strong)参照
String s = new String("hello"); // strong 参照
オブジェクト s は、少なくとも 1 つの strong 参照がある限りメモリに残ります。
弱参照(WeakReference)
import java.lang.ref.WeakReference;
public class WeakRefDemo {
public static void main(String[] args) {
String strong = new String("こんにちは、世界!");
WeakReference<String> weak = new WeakReference<>(strong);
System.out.println("クリア前: " + weak.get()); // 参照はある
strong = null; // strong 参照を外す
System.gc(); // GC にメモリ解放を依頼(保証はない)
// しばらくすると weak.get() は null になることがある
System.out.println("GC 後: " + weak.get());
}
}
仕組み:
- 少なくとも 1 つの strong 参照がある間、GC はオブジェクトを削除しません。
- 弱参照だけが残った場合、次回のガーベジコレクションでオブジェクトは削除され得ます。
- weak.get() はオブジェクトがまだ生きていればそれを返し、削除されていれば null を返します。
弱参照はどこで使う?
主な用途はキャッシュです。オブジェクトがメモリから消えても問題にならない場合に向いています。たとえば画像をキャッシュするが、キャッシュがメモリを使い切るのは避けたいといったケースです。
例: WeakHashMap
WeakHashMap はキーが弱参照で保持されるコレクションです。キーへの参照が他に無くなれば、対応するエントリはマップから削除されます。
import java.util.Map;
import java.util.WeakHashMap;
public class WeakHashMapDemo {
public static void main(String[] args) {
Map<Object, String> map = new WeakHashMap<>();
Object key = new Object();
map.put(key, "値");
System.out.println("クリア前: " + map);
key = null; // キーへの strong 参照を外す
System.gc(); // GC に作業を依頼
// しばらくするとマップは空になる!
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
System.out.println("GC 後: " + map);
}
}
注意: WeakHashMap が弱参照で持つのはキーのみで、値は通常の strong 参照です。
Soft, Weak, Phantom: 参照ファミリー
Java には 4 種類の参照があります(「強さ」の順に並べると):
| 参照の種類 | GC によっていつ削除されるか | 用途 |
|---|---|---|
|
strong 参照が 1 つもなくなったときのみ | 通常の変数・コレクション |
|
メモリ不足時 | なるべく長く保持したいキャッシュ |
|
strong 参照がなければ次回の GC で | キャッシュ、 WeakHashMap、リスナー |
|
ファイナライズ後(解放の追跡用) | 特殊用途、ヒープ外資源のクリーンアップ |
- SoftReference — メモリが不足したときに削除されます(画像などのキャッシュに最適)。
- WeakReference — 他に参照がなければ最初の GC で削除されます。
- PhantomReference — 最も「幽霊的」な型で、複雑なシナリオに使われます。初心者が使うことはまれです。
4. 実践: メモリリークとその修正
リークの例: static リスト
import java.util.ArrayList;
import java.util.List;
public class LeakExample {
private static final List<byte[]> list = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 100_000; i++) {
list.add(new byte[1024 * 1024]); // 1 MB
if (i % 10 == 0) System.out.println("追加済み " + i + " MB");
}
}
}
何が起きるか?
プログラムは利用可能なメモリを急速に使い切り、OutOfMemoryError でクラッシュします。static リストが作成したすべての配列への参照を保持しているためです。
修正: 弱参照を使う
常にすべてのオブジェクトが利用可能である必要がないなら、弱参照で保持できます。
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
public class LeakFixed {
private static final List<WeakReference<byte[]>> list = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 100_000; i++) {
list.add(new WeakReference<>(new byte[1024 * 1024]));
if (i % 10 == 0) System.out.println("追加済み " + i + " MB");
System.gc(); // GC へのヒント(即時解放は保証されない)
}
}
}
これで、ほかに参照がなければ配列は GC によって削除されます。リストは弱参照だけを保持します。
5. 典型的なメモリリークのシナリオ
- イベントリスナー: リスナーの削除を忘れると、そのオブジェクトは永続化します。
- static コレクション: クリアされないキャッシュやグローバルリストは、メモリリークの原因になります。
- 内部クラスとラムダ: 内部クラスやラムダが外部オブジェクトをキャプチャすると、外部オブジェクトが生きている限り解放されません。
内部クラスの例
public class Outer {
private byte[] bigArray = new byte[1024 * 1024 * 100]; // 100 MB
public Runnable createTask() {
// 匿名の内部クラスは Outer への参照をキャプチャする!
return new Runnable() {
@Override
public void run() {
System.out.println("タスクを実行中");
}
};
}
public static void main(String[] args) {
Outer outer = new Outer();
Runnable task = outer.createTask();
// outer = null にしても、task は依然として bigArray への参照を保持する!
}
}
対策: static な内部クラスを使うか、ロジックを別クラスに切り出して、不要な参照を保持しないようにします。
6. 実践: キャッシュでの弱参照の利用
学習用アプリに、WeakHashMap を使った簡単なキャッシュを追加してみましょう。
import java.util.Map;
import java.util.WeakHashMap;
public class ImageCache {
private final Map<String, byte[]> cache = new WeakHashMap<>();
public void put(String name, byte[] data) {
cache.put(name, data);
}
public byte[] get(String name) {
return cache.get(name);
}
public static void main(String[] args) {
ImageCache cache = new ImageCache();
cache.put("cat", new byte[1024 * 1024]); // 1 MB
System.out.println("猫をキャッシュに追加しました");
// キー "cat" への参照が他に無ければ、オブジェクトは GC により解放される可能性がある
}
}
実際のアプリ(たとえば画像ライブラリ)では、弱参照を使うことで、使用頻度の低いデータを自動的に削除し、メモリのあふれを防ぐのに役立ちます。
7. Strong vs Weak: いつ何を使うべきか?
- Strong 参照 — デフォルト。確実に生存させたいものに使う。
- 弱参照 — キャッシュやリスナーなど、オブジェクトが削除されても致命的でない場合に。
- Soft 参照 — なるべく長く保持したいキャッシュ向け。ただしメモリ不足時には削除され得る。
- Phantom 参照 — 上級者向け(例: ヒープ外資源の後始末)。
8. メモリと参照の取り扱いでの典型的な誤り
誤り No.1: 「GC が全部やってくれる!」 ガーベジコレクタは、生きた strong 参照がひとつもないオブジェクトだけを削除します。どこかに参照を「残して」しまうと(たとえば static コレクションなど)、そのオブジェクトはいつまでも生き続けます。
誤り No.2: 削除し忘れたリスナー。 オブジェクトにリスナーを追加したのに、そのオブジェクトの破棄時に削除しないと、リスナーとそれがキャプチャしているすべてはメモリに残ります。
誤り No.3: 弱参照のないキャッシュ。 自動的に掃除されるべきキャッシュに通常の HashMap を使っていませんか? WeakHashMap や SoftReference の利用を検討しましょう。
誤り No.4: 内部クラスやラムダが外側のオブジェクトをキャプチャする。 内部クラス(やラムダ式)は暗黙に外部オブジェクトへの参照を保持します。外部オブジェクトより長くそれらのインスタンスを保持すると、リークの原因になります。
誤り No.5: GC が即時に動作すると期待する。 System.gc() の呼び出しは、すぐに GC が起きることを保証しません。JVM への「お願い」に過ぎず、命令ではありません。
GO TO FULL VERSION