【Java】OutOfMemoryErrorの原因と対策をする方法

Java

はじめに

Javaで開発・運用していると、「java.lang.OutOfMemoryError」というログがに出会うことがあると思います。OOM(OutOfMemoryError) は「メモリが足りない」だけで片付けると再発します。
この記事では OutOfMemoryErrorの種類・原因・対策まで、ログとして記載していきたいと思います。

他にも、体系的にJavaを学びたい方には以下の教材がおすすめです:

👉スッキリわかるJava入門 
👉スッキリわかるJava入門 実践編

 

OutOfMemoryErrorとは

OutOfMemoryError は 例外(Exception)ではなく エラー(Error) です。つまり、基本的には「アプリが継続できない状態」になります。catchして握りつぶすのではなく、根本原因を特定して潰すのが正攻法です。

 

OutOfMemoryErrorの種類

メッセージざっくり何が足りないかよくある原因
Java heap spaceJavaヒープデータの溜め込み/メモリリーク/無制限キャッシュ
GC overhead limit exceeded GCが限界ほぼメモリリーク or データの溜め込みを継続
Metaspaceクラス情報領域動的クラス生成/クラスローダリーク
unable to create new native threadOSスレッドスレッド作りすぎ/プール設定ミス
Direct buffer memoryDirectメモリバッファ解放遅れ
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)

以上、ログになります。
これからも継続していきましょう!!

Javaサーバーサイド関連未分類
シェアする
おすすめIT本
良いコード/悪いコードで学ぶ設計入門

「ITエンジニア本大賞2023」技術書部門で大賞を受賞した本です。
・コードの可読性
・普段意識したほうが良いこと
・リファクタリング考え方
等、普段のコードを設計する際に意識することが書かれています。
コードのあるべき姿に迷ったら一度読んでみると良い本です。

仕組みと使い方がわかる Docker&Kubernetesのきほんのきほん

Dockerって何?となったときに私が最初に読んだ本です。
Dockerがどんな仕組みで動いているのか、コマンドでは何を命令しているのかを理解できるように、イラストを多用して説明しています。

1冊ですべて身につくJavaScript入門講座

「ITエンジニア本大賞2024」技術書部門で大賞を受賞した本です。
私が次に読もうと思っている本なのでおすすめとして挙げておきたいと思います。

コメント

タイトルとURLをコピーしました