はじめに
Javaで開発・運用していると、「java.lang.OutOfMemoryError」というログがに出会うことがあると思います。OOM(OutOfMemoryError) は「メモリが足りない」だけで片付けると再発します。
この記事では OutOfMemoryErrorの種類・原因・対策まで、ログとして記載していきたいと思います。
他にも、体系的にJavaを学びたい方には以下の教材がおすすめです:
👉スッキリわかるJava入門
👉スッキリわかるJava入門 実践編
OutOfMemoryErrorとは
OutOfMemoryError は 例外(Exception)ではなく エラー(Error) です。つまり、基本的には「アプリが継続できない状態」になります。catchして握りつぶすのではなく、根本原因を特定して潰すのが正攻法です。
OutOfMemoryErrorの種類
| メッセージ | ざっくり何が足りないか | よくある原因 |
|---|---|---|
| Java heap space | Javaヒープ | データの溜め込み/メモリリーク/無制限キャッシュ |
| GC overhead limit exceeded | GCが限界 | ほぼメモリリーク or データの溜め込みを継続 |
| Metaspace | クラス情報領域 | 動的クラス生成/クラスローダリーク |
| unable to create new native thread | OSスレッド | スレッド作りすぎ/プール設定ミス |
| Direct buffer memory | Directメモリ | バッファ解放遅れ |
| Requested array size exceeds VM limit | 巨大配列 | 一括読み込み/巨大JSON/CSV |
実務で多い原因
ava heap space:コレクションの溜め込み
java.lang.OutOfMemoryError: Java heap space
ヒープ(Javaが使うメモリ)にオブジェクトが増え続け、入りきらなくなっています。
一番多いのは List/Mapにため込む設計をしていることです。
public List<User> loadAllUsers() {
List<User> users = new ArrayList<>();
for (User u : userRepository.findAll()) { // 全件
users.add(u); // 全部メモリに保持
}
return users;
}
改善例:ページングして溜めない
OOM(OutOfMemoryError)の8割は「溜めない設計」に直すとことで改善します。
public void processUsers() {
int limit = 1000;
int offset = 0;
while (true) {
List<User> users = userRepository.findByPaging(limit, offset);
if (users.isEmpty()) break;
for (User u : users) {
handle(u); // ここで処理して捨てる
}
offset += limit;
}
}
無制限キャッシュ
キャッシュは「高速化」のために保存しますが、上限がないと永遠に増えます。
結果、ヒープを食い尽くしてOOMになります。
private static final Map<String, User> CACHE = new HashMap<>();
public User getUser(String id) {
return CACHE.computeIfAbsent(id, this::loadFromDb);
}
改善例:上限付き
キャッシュは 上限(件数/容量)+期限(TTL) をセットで考えることで改善します。
private final Map<String, User> cache =
new LinkedHashMap<>(1024, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, User> eldest) {
return size() > 1000; // 上限
}
};
メモリリーク:参照が切れず本来消えるはずのものが残る
java.lang.OutOfMemoryError: GC overhead limit exceeded
JavaはGC(ガベージコレクション)がありますが、参照が残っている限り解放されません。参照が残り継ぐけることによりメモリが増え続けてしまうことで発生します。
private static final ThreadLocal<List<String>> LOCAL =
ThreadLocal.withInitial(ArrayList::new);
public void doWork() {
LOCAL.get().add("temp"); // スレッドに紐づく
// removeしないとスレッドプールで残り続ける可能性
}
改善例:finallyでremove(鉄則)
参照は使用しなくなったら開放する設計を行うことで、改善されます。
public void doWork() {
try {
LOCAL.get().add("temp");
} finally {
LOCAL.remove();
}
}
巨大データ一括処理:ファイル/JSON/画像を丸ごと持つ
java.lang.OutOfMemoryError:Requested array size exceeds VM limit
巨大なデータを byte[] や String に一括で持つと、あっという間にヒープを食い切ります。
byte[] data = java.nio.file.Files.readAllBytes(java.nio.file.Paths.get("hoge.csv"));
改善例:ストリーミング処理
ストリーミングで処理したり分割して処理することで、改善を図ります。
try (java.io.BufferedReader reader =
java.nio.file.Files.newBufferedReader(java.nio.file.Paths.get("huge.csv"))) {
String line;
while ((line = reader.readLine()) != null) {
processLine(line); // 1行ずつ処理して捨てる
}
}
unable to create new native thread:スレッドを作りすぎ
java.lang.OutOfMemoryError: unable to create new native thread
スレッドはOSリソースを使います。無限に作るとJVMではなく OS側で作れなくなります。
for (int i = 0; i < 100000; i++) {
new Thread(() -> doWork()).start();
}
改善例:スレッドプール(上限を決める)
java.util.concurrent.ExecutorService executor =
java.util.concurrent.Executors.newFixedThreadPool(20);
for (int i = 0; i < 100000; i++) {
executor.submit(this::doWork);
}
executor.shutdown();
まとめ
OutOfMemoryErrorは単なる「メモリ不足」ではありません。多くの場合は 設計・実装・運用の積み重ねの結果 として発生します。現場で再発させないために、データ量の管理出会ったり、参照の解放、スレッドの上限等、設計や改修段階でそれらを考慮することを心掛けて開発を行っていきましょう。
ドキュメント
【公式ドキュメント】
Java SE Specifications (oracle.com)
最後に
Javaの環境構築は、この記事を参照してみてください。
【開発環境構築】VS CodeでJavaを使用するための環境構築を実施する – SEもりのLog (selifemorizo.com)
以上、ログになります。
これからも継続していきましょう!!


コメント