OOM が発生した場合でも、Java はリクエストの処理を続行できますか?

面接で「Java プログラムで OOM が発生した場合、プロセスはリクエストの処理を続行できますか?」といった質問をされた場合、混乱するでしょうか? これはネチズンのチェ・ルーさんがシェアした話だが、卒業後の最初の数年間は、彼はとても有意義な日々を送っていたため、マスターできない面接の質問はこの世に存在しないと常に感じていたという。その後、Byte の面接で、面接プロセスを完全に覆しました。面接の経験をいくつか提供したいと思います。

Javaの利点は何ですか?

インタビュアーが現れるとすぐに、「メモリ管理に関して Java にはどのような利点があると思いますか?」という話題に直接切り込みました。

私:簡単だよ。手動でメモリを解放する必要がある C 言語と比較して、Java はガベージ コレクション メカニズムを通じて自動メモリ管理を実装します。このメカニズムにより、使用されなくなったメモリ リソースを自動的に特定してクリーンアップできるため、面倒な手動の解放プロセスが不要になり、開発者の作業が大幅に簡素化されます。これにより、開発者はメモリ管理の問題をあまり気にすることなく、ビジネス ロジックの構築に集中できるようになり、開発者の作業負荷が大幅に簡素化されます。

OOMとは何ですか

インタビュアー: メモリ不足 (OOM) の状況についてご存知ですか?

私: 私の経験上、オンラインではメモリ オーバーフローの状況によく遭遇します。特に Java で書かれたアプリケーションでは、OOM は通常、メモリ オーバーフロー例外を指します。つまり、アプリケーションが新しいオブジェクトにメモリ領域を割り当てる必要がある場合、利用可能なメモリが需要を満たすのに十分ではなく、その結果 OOM 例外がスローされます。 。

どのような状況で OOM が発生しますか?

インタビュアー: いいですね、オンライン事故コードのすべてがあなたによって書かれたわけではありませんよね? では、どのような状況でメモリ オーバーフローが発生するかについて話していただけますか?

私: もちろん、よくある状況はヒープ メモリのオーバーフローです。オブジェクトを作成すると、ほとんどの場合、オブジェクトは JVM のヒープ メモリを占有します。ヒープ メモリがオブジェクトの割り当て要件を満たせなくなると、OOM 例外がスローされます。

通常、エラー メッセージは次のとおりです: java.lang.OutOfMemoryError: Java heap space

ヒープ メモリ オーバーフローの具体的なシナリオ

インタビュアー: これは抽象的すぎますが、もっと具体的に教えていただけますか?

私: そうですね、メモリ オーバーフローを引き起こす一般的な状況がいくつかあります。

オブジェクトのライフ サイクルが長すぎる: オブジェクトのライフ サイクルが長すぎて、オブジェクトが大量のメモリを占有する場合、新しいオブジェクトの継続的な作成中にヒープ メモリが使い果たされ、メモリ オーバーフローが発生します。この状況は通常、コレクションがキャッシュとして使用されているが、キャッシュ削除メカニズムが無視されている場合に発生します。

無限再帰: 再帰呼び出しの終了条件または再帰の深さが不足していると、スペースが枯渇し、オーバーフロー エラーが発生します。この問題は多くの場合、テスト環境で発見され、運用環境では顕在化されません。

ビッグ データの収集: 大量のデータを処理する場合、大きすぎるファイルの読み込みや大きすぎるクエリ結果セットなど、メモリが適切に管理されていない場合、メモリ オーバーフローが発生します。

不適切な JVM 設定: JVM メモリ パラメータの設定が不当である場合 (たとえば、ヒープ メモリ設定が小さすぎてアプリケーションのメモリ要件を満たせない場合)、メモリ オーバーフローも発生します。

次の例は、メモリ オーバーフローを引き起こす無限ループです。

List list = new ArrayList();
while (true) {
    list.add(1);
}

メモリリークとは何ですか

インタビュアー: 私たちのプログラムでメモリ リークが発生する可能性があることをご存知ですか? 理解していますか?

私: はい、メモリ オーバーフローの状況とは異なり、メモリ リークと呼ばれる別の特別なシナリオがあります (本質的にはまだメモリ オーバーフローであり、単に間違ったメモリ オーバーフローです)。これは、プログラムが解放されず、使用されなくなるという事実を指します。実行中のプロセスがメモリを消費し、メモリ使用量が増加し続け、最終的にはシステム リソースが使い果たされることになります。この状況はメモリ リークと呼ばれます。

今回は事前に質問に答えましたが、メモリ リークにつながる一般的な状況は次のとおりです。

オブジェクトへの参照が正しく解放されません。オブジェクトの使用後に参照を null に設定するのを忘れたり、データ構造から削除したりすると、オブジェクトはガベージ コレクションされず、メモリ リークが発生します。ThreadLocal など。

存続期間の長いオブジェクトは存続期間の短いオブジェクトへの参照を保持します。存続期間の長いオブジェクトが存続期間の短いオブジェクトへの参照を保持している場合、存続期間の短いオブジェクトが使用されなくなった場合でも、存続期間の長いオブジェクトへの参照は依然として存在します。 . 、寿命の短いオブジェクトはガベージ コレクションできないため、メモリ リークが発生します。

サードパーティ ライブラリの過剰な使用: 一部のサードパーティ ライブラリにはメモリ リークが発生したり、リソースが正しく解放されない場合があります。これらのライブラリが不適切に使用されたり、適切に管理されなかったりすると、メモリ オーバーフローが発生する可能性があります。

コレクション クラスの不適切な使用: コレクション クラスを使用する場合、要素が正しくクリーンアップされないと、コレクションが不要になったときにコレクション内のオブジェクトが解放されず、メモリ リークが発生します。

リソースが正しく解放されない: プログラムがファイル、データベース接続、ネットワーク接続などのリソースを使用し、これらのリソースが不要になったときに正しく解放されない場合、リソース リークが発生し、最終的にはメモリ リークが発生します。

次の例では、存続期間の長いオブジェクトが存続期間の短いオブジェクトへの参照を保持しているため、メモリ リークが発生します。

List list2 = new ArrayList();

@GetMapping("/headOOM2")
public String headOOM2() throws InterruptedException {
    while (true) {
        list2.add(1);
    }
}

他に何か状況はありますか?

インタビュアー: ヒープ メモリのオーバーフローについて話していますが、他に何か状況はありますか?

再帰呼び出しによりスタック オーバーフローが発生する

再帰呼び出しのレベルが深すぎて、スタック領域にこれ以上のメソッド呼び出し情報を収容できない場合、StackOverflowError 例外が発生します。これは OOM 例外でもあります。たとえば、次の例の無限再帰呼び出しはスタック オーバーフローを引き起こします。

public class RecursiveExample {
    public static void recursiveFunction() {
        recursiveFunction();
    }

    public static void main(String[] args) {
        recursiveFunction();
    }
} 

メタスペースが枯渇した

メタスペースは、Java 8 以降のバージョンでクラスのメタデータを格納するために使用される領域です。これは、以前のバージョンの永続世代 (PermGen) を置き換えます。メタスペースは主にクラスの構造情報、メソッド情報、静的変数、コンパイルされたコードなどを格納するために使用されます。

プログラムが多数のクラスをロードして定義したり、クラスを動的に生成したり、リフレクションを使用してクラスを頻繁に操作したりする場合など、メタスペースが枯渇する可能性があります。メタスペースの枯渇につながる一般的な状況は次のとおりです。

過剰なクラスのロード: アプリケーションが多数のクラスを動的にロードするか、動的に生成されたクラスを使用する場合、メタスペースの使用量が増加します。これらのクラスを適時にアンロードできない場合、メタスペースが枯渇する可能性があります。

文字列定数が多すぎる: Java の文字列定数はメタスペースに格納されます。アプリケーションで多数の文字列定数、特に長い文字列を使用すると、メタスペースが枯渇する可能性があります。

リフレクションの頻繁な使用: リフレクション操作には大量のメタデータ情報が必要であり、より多くのメタスペースを占有します。アプリケーションがクラス操作を実行するためにリフレクションを頻繁に使用する場合、メタスペースが枯渇する可能性があります。

多数の動的プロキシ: 動的プロキシは、リフレクションを使用してプロキシ オブジェクトを作成する手法です。アプリケーションが多数の動的プロキシを使用する場合、多数のプロキシ クラスが生成され、多くのメタ領域を占有します。

メタスペースのサイズが正しく制限されていません。デフォルトでは、メタスペースのサイズは無制限で、必要に応じて動的に拡張されます。メタスペースのサイズ制限が正しく設定されていない場合、または制限が小さすぎる場合は、メタスペースが枯渇する可能性があります。

次の例は、過剰なクラスのロードによって引き起こされるメモリ リークです。

public class OOMExample {
    public static void main(String[] args) {
        while (true) {
            ClassLoader classLoader = new CustomClassLoader();
            classLoader.loadClass("com.example.LargeClass");
        }
    }
}

究極の質問

インタビュアーは満足そうにうなずき、Java スレッドがリクエストを処理するときに何が起こるかについて私がよく知っていることを示しました。次に、「Java スレッドがリクエストを処理しているときに、OOM 例外が発生します。プロセス全体はリクエストの処理を続行できますか?」という質問を提起しました。

私が答えようとしたとき、面接官は、この質問はより複雑な内容を含んでおり、単純な「はい」か「いいえ」で答える質問ではないことを思い出させてくれました。彼は、まず自分の考えを整理することを提案しました。

インタビュアーの目から、この質問には深い意味があることが分かりました。少し考えた後、私は「OOM がプロセス全体のクラッシュを引き起こすとは思わない」と答えました。

次にインタビュアーは、「どうやって理解しましたか? OOM はメモリ不足の現れではありませんか? メモリが不足しているため、プロセスはリクエストの処理を続行できますか?」と尋ねました。

私は、「OOM にもかかわらず、ガベージ コレクション メカニズムを通じて一部のメモリを解放することは可能です。」と説明しました。

インタビュアーは「ガベージコレクションを行ってもメモリが足りないことが分かり、OOM例外がスローされるのではありませんか?ガベージコレクションが継続できなくなりメモリ不足になったということではありませんか?」と反論しました。 OOM 例外を引き起こすのはどれですか? プロセス全体 メモリ不足、非効率的なガベージ コレクション、そして最終的には OOM です。」

このようなパンチの組み合わせを前に、私は少し心の準備ができていなかったので、心の中で静かに文句を言うことしかできませんでした。残念ながら面接は不合格に終わりました。面接官は私が退職するとき、「私もこんなことは起こってほしくない。私のプログラミング経験の不足のせいにするしかない。」とほのめかしているようでした。

実戦

家に帰ったら、すぐに OOM をテストするためにコーディングの練習をしました。

環境は OpenJdk 11 -Xms100m -Xmx100m -XX:+PrintGCDetails です。

ヒープメモリオーバーフロー

まずメソッドを作成し、それを呼び出し、毎秒ループでコンソール情報を出力します。その主な機能は、リクエストを処理する他のスレッドをシミュレートすることです。

@GetMapping("/writeInfo")
public String writeInfo() throws InterruptedException {
    while (true) {
        Thread.sleep(1000);
        System.out.println("正在输出信息");
    }
}

次に、オブジェクトを List に入れるための無限ループ メソッドを作成します。その主な機能は、OOM を引き起こすスレッドをシミュレートすることです。

@GetMapping("/headOOM")
public String headOOM() throws InterruptedException {
    List list = new ArrayList();
    while (true) {
        list.add(1);
    }
}

最終的な結果として、headOOMOOM 例外がスローされますが、コンソールは出力を続けます。【スクリーンショットは大きすぎるので載せません】

これが答えですか?実際にはいいえ、最初のステップでは、ログはコンソールに出力されるだけで、明確なオブジェクトは作成されません。これを少し変更して行を追加し、各印刷前に 1,000 万個のオブジェクトを作成します。

public String writeInfo() throws InterruptedException {
    while (true) {
        Thread.sleep(1000);
        Byte[] bytes = new Byte[1024 * 1024 * 10];
        System.out.println("正在输出信息");
    }
}

結果は引き続き出力されます。これを見て、確かに答えは「演奏を続けられる」という人もいるかもしれないが、Too Young Too Simpleとしか言いようがない。見下ろす

ヒープメモリリーク

古いルールは依然として上記と同じです。

public String writeInfo() throws InterruptedException {
    while (true) {
        Thread.sleep(1000);
        Byte[] bytes = new Byte[1024 * 1024 * 10];
        System.out.println("正在输出信息");
    }
}

メモリ リークを引き起こすメソッドを作成します。list2 のスコープはクラス オブジェクト レベルであるため、メモリ リークが発生します。

List list2 = new ArrayList();
@GetMapping("/headOOM2")
public String headOOM2() throws InterruptedException {
    while (true) {
        list2.add(1);
    }
}

その後、実行が続行され、その結果、このメソッドに対応するスレッドが最初に OOM をスローしますheadOOM2

その後、 WriteInfoこのメソッドに対応するスレッドが OOM をスローするため、プロセス全体が基本的にリクエストを処理できなくなっていると思います。

この推測を確認するには、 writeInfoこのメソッドを再度呼び出して、OOM 例外を直接スローします。これは私たちの推測が正しいことを示しています。

このとき、10Mを1Mに変更すると、writeInfo この方法を再度実行できるので、信じられない場合は試してみてください。

これはメモリ リークがあることを示しています。他のスレッドが実行を継続できるかどうかは、これらのスレッドの実行ロジックが大量のメモリを占有するかどうかによって決まります。

メモリ リークが発生しない場合、オブジェクトを頻繁に作成すると OOM が発生するのはなぜですか? GC はオブジェクトをリサイクルしないのですか?

  1. ヒープ メモリ制限: Java プログラムのヒープ メモリにはサイズ制限があります。オブジェクトが頻繁に作成され、時間内にリサイクルできない場合、ヒープ領域が枯渇する可能性があります。ガベージ コレクターは、使用されなくなったオブジェクトをリサイクルしようとしますが、作成速度がリサイクル速度を超えると、ヒープ メモリ不足により OOM が発生します。
  2. ガベージ コレクションのオーバーヘッド: ガベージ コレクターは使用されなくなったオブジェクトを再利用しますが、ガベージ コレクション自体は時間とコンピューティング リソースを消費します。一時オブジェクトを頻繁に作成すると、リサイクル時間が長くなり、アプリケーションの効率が低下します。
  3. メモリの断片化: オブジェクトの作成と破棄を頻繁に行うと、メモリ空間の断片化が発生する可能性があります。空きメモリの合計が十分な場合でも、新しいオブジェクトに割り当てられる連続した大きなメモリ ブロックが十分にない場合は、OOM が発生します。

GC ログを観察すると、OOM が発生してもヒープ サイズがしきい値に達しておらず、他の要因が OOM 例外の原因であることがわかります。

要約する

まず、OOM とは何か、およびメモリ オーバーフローやメモリ リークなどのその発生シナリオについて説明しました。次に、Java スレッドがリクエストを処理しているときに OOM 例外がスローされた場合、プロセス全体が引き続きリクエストを処理できるかどうかという質問を提起しました。リクエスト。

その後、メモリ オーバーフローとメモリ リークの 2 つの状況をシミュレートするコード実験を実施し、次の結論に達しました。

  1. メモリ オーバーフローの場合、GC 速度がメモリ割り当て速度に追いつけず、OOM が発生してスレッドが強制終了されても、通常はプロセス全体がリクエストの処理を続行できます。
  2. メモリ リークが発生した場合、リサイクルできないメモリによって OOM が発生し、リサイクルできないオブジェクトが継続的に生成されるのを防ぐためにスレッドが強制終了される可能性があります。この時点で、メモリを大量に消費していないスレッドは実行を継続できますが、メモリを大量に消費しているスレッドは実行できなくなる可能性があります。極端な場合には、プロセスがクラッシュする可能性があります。

上記の結論は、プロセスに対する OOM の影響を反映しており、プロセスがさまざまな状況下でもリクエストを処理できるかどうかを示しています。

おすすめ

転載: blog.csdn.net/mixika99/article/details/132295217
おすすめ