コンテンツにスキップ

Java OOM 異常のオブザーバビリティベストプラクティス


よくある OOM 異常のシナリオ

  1. ヒープオーバーフロー - java.lang.OutOfMemoryError: Java heap space。
  2. スタックオーバーフロー - java.lang.OutOfMemoryError。
  3. スタックオーバーフロー - java.lang.StackOverflowError。
  4. メタスペースオーバーフロー - java.lang.OutOfMemoryError: Metaspace。
  5. 直接メモリオーバーフロー - java.lang.OutOfMemoryError: Direct buffer memory。
  6. GC 超過 - java.lang.OutOfMemoryError: GC overhead limit exceeded。

ガベージコレクタ

ガベージコレクタはメモリ回収の実行者です。ベンダーやバージョンが異なる仮想マシンに含まれるガベージコレクタは大きく異なる可能性があり、一般的にさまざまなパラメータが提供されており、ユーザーはアプリケーションの特性や要件に応じて各メモリ世代で使用するコレクタを組み合わせることができます。——『深入理解JAVA虚拟机』

ガベージコレクタ(ガベージコレクションとも呼ばれる)について、『深入理解JAVA虚拟机』第3版の目次では、ほとんどのガベージコレクタがリストアップされています。下図の通りです。 java_oom_1.png

ローカル JVM のガベージコレクタを確認

コマンド java -XX:+PrintFlagsFinal -version |FINDSTR /i ":" を使用して、ローカルのガベージコレクタが Parallel であることを確認します。

C:\Users\lenovo>java -XX:+PrintFlagsFinal -version |FINDSTR /i ":"
     intx CICompilerCount                          := 4                                   {product}
    uintx InitialHeapSize                          := 266338304                           {product}
    uintx MaxHeapSize                              := 4257218560                          {product}
    uintx MaxNewSize                               := 1418723328                          {product}
    uintx MinHeapDeltaBytes                        := 524288                              {product}
    uintx NewSize                                  := 88604672                            {product}
    uintx OldSize                                  := 177733632                           {product}
     bool PrintFlagsFinal                          := true                                {product}
     bool UseCompressedClassPointers               := true                                {lp64_product}
     bool UseCompressedOops                        := true                                {lp64_product}
     bool UseLargePagesIndividualAllocation        := false                               {pd product}
     bool UseParallelGC                            := true                                {product}
java version "1.8.0_101"
Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, mixed mode)

K8s 環境での JVM ガベージコレクタを確認

K8s 環境では、通常 openjdk:8-jdk-alpine または openjdk:8u292 をベースイメージとして使用します。サービス起動後、ガベージコレクタが有効になっていないことが確認できます。

root@ruoyi-system-c9c54dbd5-ltcvf:/data/app# 
root@ruoyi-system-c9c54dbd5-ltcvf:/data/app# java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=8388608 -XX:MaxHeapSize=134217728 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops 
openjdk version "1.8.0_292"
OpenJDK Runtime Environment (build 1.8.0_292-b10)
OpenJDK 64-Bit Server VM (build 25.292-b10, mixed mode)
root@ruoyi-system-c9c54dbd5-ltcvf:/data/app# 

前提条件

1. JDK バージョン 1.8(JDK8 とも呼ばれる)

各 JDK のガベージコレクションの仕組みは異なり、メモリ構造も大きく変化しています。特にバージョン 1.6、1.7、1.8 では顕著な違いがあります。現在、多くの企業が JDK 1.8 を使用しており、本ベストプラクティスも JDK 1.8 をベースとしています。他のバージョンの JDK を使用している場合でも、考え方は参考にできます。

2. JVM のオブザーバビリティを有効化

まず JVM のオブザーバビリティ を有効にしてください。Guance のビューから、初期ヒープメモリが 80 M であることが確認でき、起動時に指定したパラメータと一致しています。

image.png

3. ログのオブザーバビリティを有効化

参考: **Kubernetes クラスタでのログ収集のいくつかの方法。今回は主に socket 方式を採用していますが、他の方法も使用可能です。**

ヒープオーバーフロー - java.lang.OutOfMemoryError: Java heap space

ヒープオーバーフロー異常は、よく見られるものです。ヒープ内のオブジェクトが回収できなくなり、ヒープメモリが増加し続けて最大値に達すると、データが満杯になって発生します。オーバーフローのコードサンプルを直接示します。起動時の最大ヒープメモリを -Xmx80m に設定すると、すぐにエラーが発生します。

1. 起動パラメータ

-Xmx80m -javaagent:C:/"Program Files"/datakit/data/dd-java-agent.jar -Ddd.service.name=system -Ddd.agent.port=9529

2. リクエスト

ブラウザで http://localhost:9201/exec/heapOOM にアクセスします。異常出力が表示されるまでしばらく待つ必要があります。異常出力が確認できたら、Guance に移動して対応するログを確認します。

3. Guance でログを確認

image.png

スタックオーバーフロー - java.lang.OutOfMemoryError

スローされる例外は以下の通りです。実際にスレッドを作成する必要がある場合は、フレームスタックのサイズを -Xss512k に調整する必要があります。デフォルトのフレームスタックサイズは 1M で、小さく設定するとより多くのスレッドを作成できます。フレームスタックが不足している場合は、どこで多くのスレッドが作成されているかを特定する必要があります。本番プログラムでは、jstack コマンドを使用して現在のスレッドの状態をファイルにエクスポートし、そのファイルを fastthread.io サイトにアップロードして分析します。コードが本当にこれだけのスレッドを必要とする場合、「JVM 総メモリ - ヒープ = n * Java 仮想マシンスタック」に従って、ヒープメモリまたは Xss を減らして、割り当て可能なスレッド数を増やすことができます。

1. 起動パラメータ

-Xmx80m -javaagent:C:/"Program Files"/datakit/data/dd-java-agent.jar -Ddd.service.name=system -Ddd.agent.port=9529

2. リクエスト

ブラウザでアクセス: http://localhost:9201/exec/stackOOM

3. Guance でログを確認

スレッドが瞬時に作成されると、JVM 標準ツールはスレッド関連の監視メトリクスを報告しなくなりますが、Guance は引き続き最新の JVM 監視メトリクスを報告します。

image.png

しばらくすると、JVM 標準ツールに異常が発生します。

image.png

その後、システムが疑似フリーズ状態になります。

image.png

スタックオーバーフロー - java.lang.StackOverflowError

主に再帰呼び出しや無限ループで発生します。スタックフレームが大きすぎるか、仮想マシンスタックの容量が小さすぎるために、新しいスタックフレームにメモリを割り当てられない場合、HotSpot 仮想マシンは StackOverflowError 例外をスローします。プログラムが再帰するたびに、データ結果(内部のポインタなど)がスタックにプッシュされます。そのため、より多くの再帰呼び出しに対応するには、フレームスタックを大きくする必要があります。

1. 起動パラメータ

-Xmx80m -javaagent:C:/"Program Files"/datakit/data/dd-java-agent.jar -Ddd.service.name=system -Ddd.agent.port=9529

2. リクエスト

ブラウザで http://localhost:9201/exec/stackOFE にアクセスします。

3. Guance でログを確認

image.png

メタスペースオーバーフロー - java.lang.OutOfMemoryError: Metaspace

JDK 8 以降、PermGen は完全に廃止され、メタスペースがその代替として登場しました。メタデータ領域はメソッド領域とも呼ばれます。デフォルト設定では、仮想マシンにメソッド領域(メタデータ領域)のオーバーフロー例外を発生させることは困難です。クラスに関する情報、定数プール、メソッド記述子、フィールド記述子などが格納され、実行時に大量のクラスが生成されるとこの領域がオーバーフローします。起動時に -XX:MetaspaceSize-XX:MaxMetaspaceSize を小さく設定しすぎると、起動時にエラーが発生します。

1. 起動パラメータ

-Xmx80m -XX:MetaspaceSize=30M -XX:MaxMetaspaceSize=90M -javaagent:C:/"Program Files"/datakit/data/dd-java-agent.jar -Ddd.service.name=system -Ddd.agent.port=9529

2. リクエスト

ブラウザで http://localhost:9201/exec/metaspaceOOM にアクセスします。 3. ログを確認 メタデータオーバーフロー後は、ログの書き込みなどの操作は行われなくなります。

直接メモリオーバーフロー - java.lang.OutOfMemoryError: Direct buffer memory

直接メモリオーバーフローは、ヒープメモリに加えて直接メモリ(オフヒープメモリ)を使用する場合に発生します。NIO はパフォーマンス向上のために Java ヒープとネイティブヒープの切り替えを避けるため、直接メモリを使用します。デフォルトでは、直接メモリのサイズはヒープメモリのサイズと同じです。オフヒープメモリは JVM の制限を受けませんが、マシン全体のメモリサイズに制限されます。以下のコードでは、最大ヒープメモリを 80m、直接メモリを 70m に設定し、毎回 1M をリストに追加します。この場合、70 回出力された後(Spring Boot アプリケーションでは 70 回未満)、次に割り当てるときに nested exception is java.lang.OutOfMemoryError: Direct buffer memory が発生します。

1. 起動パラメータ

-Xmx80m -javaagent:C:/"Program Files"/datakit/data/dd-java-agent.jar -Ddd.service.name=system -Ddd.agent.port=9529

2. リクエスト

ブラウザで http://localhost:9201/exec/directBufferOOM にアクセスします。

3. Guance でログを確認

image.png

GC 超過 - java.lang.OutOfMemoryError: GC overhead limit exceeded

前述の3つはいずれも GC 超過を引き起こす可能性があります。JDK 1.6 以降で追加されたエラータイプで、ヒープメモリが小さすぎるとこのエラーが報告されます。GC の 98% で 2% 未満しか回収できない場合にこのエラーが発生します。つまり、最小最大メモリに問題がある場合に発生します。

Guance

どのような異常であっても、Guance の JVM モニタリングビュー で手がかりを見つけることができ、ログの状況と組み合わせて JVM パラメータをチューニングします。GC の回数が多すぎるまたは少なすぎる、GC 時間が長すぎる、スレッドが突然増加する、ヒープメモリが突然増加するなど、いずれも注意が必要です。

image.png

Guance OOM ログアラート

上記のいくつかの OOM 異常シナリオは、異常の発生方法と Guance 上での動作を示したものです。実際の本番環境では、OOM 異常はビジネスロジックに影響を与え、より深刻な場合はシステムの中断を引き起こす可能性があります。Guance のアラート機能を利用して、関連担当者に迅速に通知し、介入することができます。

StackOverflowError 異常検知の設定

image.png

OutOfMemoryError 異常検知の設定

image.png

アラート通知の設定

モニター一覧 - グループ化。アラート通知ボタンをクリック。

image.png

通知先を設定します。Guance は複数の通知先をサポートしており、現在はメール通知を採用しています。

image.png

異常がトリガーされると、以下のようなメール通知を受信します。

image.png

デモコード

このプログラムコードは、RuoYi マイクロサービスフレームワーク上でデモンストレーションされています。

package com.ruoyi.system.controller;

import com.ruoyi.common.core.domain.system.SysDept;
import com.ruoyi.common.core.web.domain.AjaxResult;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author liurui
 * @date 2022/4/11 9:28
 */
@RequestMapping("/exec")
@RestController
public class ExceptionController {

    @GetMapping("/heapOOM")
    public AjaxResult heapOOM() {
        List<SysDept> list = new ArrayList<>();
        while (true) {
            try {
                TimeUnit.MILLISECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.add(new SysDept());
        }
    }

    @GetMapping("/stackOOM")
    public AjaxResult stackOOM() {
        while (true) {
            Thread thread = new Thread(() -> {
                while (true) {
                    try {
                        TimeUnit.HOURS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            });
            thread.start();
        }
    }

    @GetMapping("/directBufferOOM")
    public AjaxResult directBufferOOM() {
        final int _1M = 1024 * 1024 * 1;
        List<ByteBuffer> buffers = new ArrayList<>();
        int count = 1;
        while (true) {
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M);
            buffers.add(byteBuffer);
            System.out.println(count++);
        }
    }

    @GetMapping("/stackOFE")
    public AjaxResult StackOFE() {
        stackOverFlowErrorMethod();
        return AjaxResult.success();
    }

    public static void stackOverFlowErrorMethod() {
        stackOverFlowErrorMethod();
    }

    @GetMapping("/metaspaceOOM")
    public AjaxResult metaspaceOOM() {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(SysDept.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object obj, Method method,
                                        Object[] args, MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });
            enhancer.create();
        }
    }
}

フィードバック

このページは役に立ちましたか?