JVM 実用的な OutOfMemoryError 例外

JVM 実用的な OutOfMemoryError 例外

[[420231]]

Java 仮想マシン仕様によれば、プログラム カウンタに加えて、仮想マシン メモリの他のいくつかのランタイム領域でも OutOfMemoryError (以下、OOM) 例外が発生する可能性があります。 (この記事は主にjdk1.8に基づいています)

Java ヒープオーバーフロー

Java ヒープはオブジェクト インスタンスを格納するために使用されます。オブジェクトを作成し続け、GC ルートからオブジェクトに到達可能なパスを確保して、ガベージ コレクション メカニズムがこれらのオブジェクトをクリアしないようにする限り、オブジェクトの数が増加するにつれて、合計容量が最大ヒープ容量制限に達し、メモリ オーバーフロー例外が発生します。

シミュレーションコード

以下はヒープメモリのオーバーフローをシミュレートする簡単なコードです。

  1. /**
  2. * VM 引数: -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
  3. * @著者 鄭sh
  4. * @日付2021-8-13
  5. */
  6. パブリッククラスHeapOOM {
  7.      
  8. 公共 静的void main(String[] args) {
  9. リスト<byte[]> リスト = 新しい ArrayList<>();
  10. )の間{
  11. list.add (新しいバイト[2048]);
  12. }
  13. }
  14. }

返される結果情報は次のとおりです。

  1. スレッド「main」例外が発生しましたjava.lang.OutOfMemoryError: Java ヒープスペース 
  2. cn.zhengsh.jvm.oom.HeapOOM.main(HeapOOM.java:16)

問題分析

メモリリーク(Memory Leak)なのかメモリオーバーフロー(Memory Overflow)なのかを特定する必要があります。

  • メモリリーク
  • メモリリーク

メモリリーク

JDK に付属する jvisualvm ツールを使用して、ヒープ スナップショット ファイルを読み込み、分析することができます。メモリ リークの場合は、さらにツールを使用して、リークされたオブジェクトから GC ルートまでの参照チェーンを表示し、リークされたオブジェクトがどの参照パスに関連付けられているか、およびどの GC ルートであるかを調べて、ガベージ コレクターによる再利用を防ぐことができます。リークされたオブジェクトの型情報と GC ルートへの参照チェーンに基づいて、通常、これらのオブジェクトが作成される場所をより正確に特定し、メモリ リークの原因となるコードの特定の場所を見つけることができます。

メモリリーク

メモリ リークではない場合、つまりメモリ内のオブジェクトが実際に存続する必要がある場合は、Java 仮想マシンのヒープ パラメータ (-Xmx および -Xms) 設定を確認し、マシンのメモリと比較して、上方調整の余地があるかどうかを確認する必要があります。次に、コードをチェックして、ライフサイクルが長すぎるオブジェクト、保持時間が長すぎるオブジェクト、不合理なストレージ構造設計などがないかどうかを確認し、プログラム実行中のメモリ消費を最小限に抑えます。

仮想マシン スタックとネイティブ メソッド スタック オーバーフロー

HotSpot 仮想マシンは、仮想マシン スタックとローカル メソッド スタックを区別しません。したがって、HotSpot の場合、-Xoss パラメータ (ローカル メソッド スタック サイズの設定) が存在しますが、実際には効果はありません。スタック容量は -Xss パラメータでのみ設定できます。仮想マシン スタックとネイティブ メソッド スタックに関しては、Java 仮想マシン仕様に次の 2 つの例外が記述されています。

  1. スレッドによって要求されたスタックの深さが仮想マシンで許可される最大深さより大きい場合、StackOverflowError 例外がスローされます。
  2. 仮想マシンのスタック メモリが動的拡張を許可している場合、拡張されたスタック容量が十分なメモリに適用できないときに OutOfMemoryError 例外がスローされます。

Java 仮想マシン仕様では、Java 仮想マシン実装がスタックの動的な拡張をサポートするかどうかを選択することが明示的に許可されていますが、HotSpot 仮想マシンは拡張をサポートしないことを選択しています。そのため、スレッド作成時やメモリ申請時にメモリ不足が発生して OutOfMemoryError 例外が発生しない限り、拡張によりスレッド実行時にメモリオーバーフローが発生することはありません。スタック容量が新しいスタック フレームに対応できないため、StackOverflowError 例外が発生するだけです。

仮想マシンのスタックメモリオーバーフロー

スタックオーバーフローエラー

サンプルコード:

  1. /**
  2. * VM引数: -Xss128k
  3. *
  4. * @著者 鄭sh
  5. * @日付2021-08-13
  6. */
  7. パブリッククラスJavaVMStackSOF {
  8. プライベートintスタック長さ = 1;
  9.  
  10. パブリックボイドスタックリーク() {
  11. スタック長++;
  12. スタックリーク();
  13. }
  14.  
  15. 公共 静的void main(String[] args) throws Throwable {
  16. JavaVMStackSOF oom = 新しい JavaVMStackSOF();
  17. 試す {
  18. oom.stackLeak();
  19. } キャッチ (Throwable e) {
  20. システム。 out .println( "スタックの長さ:" + oom.stackLength);
  21. eを投げる;
  22. }
  23. }
  24. }

例外情報を返します

  1. スレッド「main」例外が発生しましたjava.lang.StackOverflowError
  2. スタック長:992
  3. cn.zhengsh.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
  4. cn.zhengsh.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:14)
  5. //.... 以下省略

メモリ不足エラー

  1. パッケージ cn.zhengsh.jvm.oom;
  2.  
  3. /**
  4. * @著者 鄭sh
  5. * @日付2021-08-13
  6. */
  7. パブリッククラスJavaVMStackSOF2 {
  8. プライベートスタティック  intスタック長さ = 0;
  9.  
  10. 公共 静的voidテスト(){
  11. 長い unused1、 unused2、 unused3、 unused4、 unused5、 unused6、 unused7、 unused8、 unused9、 unused10、 unused11、
  12. 未使用12、未使用13、未使用14、未使用15、未使用16、未使用17、未使用18、未使用19、未使用20、未使用21、
  13. 未使用22、未使用23、未使用24、未使用25、未使用26、未使用27、未使用28、未使用29、未使用30、未使用31、
  14. 未使用32、未使用33、未使用34、未使用35、未使用36、未使用37、未使用38、未使用39、未使用40、未使用41、
  15. 未使用42、未使用43、未使用44、未使用45、未使用46、未使用47、未使用48、未使用49、未使用50、未使用51、
  16. 未使用52、未使用53、未使用54、未使用55、未使用56、未使用57、未使用58、未使用59、未使用60、未使用61、
  17. 未使用62、未使用63、未使用64、未使用65、未使用66、未使用67、未使用68、未使用69、未使用70、未使用71、
  18. 未使用72、未使用73、未使用74、未使用75、未使用76、未使用77、未使用78、未使用79、未使用80、未使用81、
  19. 未使用82、未使用83、未使用84、未使用85、未使用86、未使用87、未使用88、未使用89、未使用90、未使用91、
  20. 未使用92、未使用93、未使用94、未使用95、未使用96、未使用97、未使用98、未使用99、未使用100;
  21. スタック長++;
  22. テスト();
  23. 未使用1 = 未使用2 = 未使用3 = 未使用4 = 未使用5 = 未使用6 = 未使用7 = 未使用8 = 未使用9 = 未使用10 = 未使用11 =
  24. 未使用12 = 未使用13 = 未使用14 = 未使用15 = 未使用16 = 未使用17 = 未使用18 = 未使用19 = 未使用20 =
  25. 未使用21 = 未使用22 = 未使用23 = 未使用24 = 未使用25 = 未使用26 = 未使用27 = 未使用28 = 未使用29 =
  26. 未使用30 = 未使用31 = 未使用32 = 未使用33 = 未使用34 = 未使用35 = 未使用36 = 未使用37 = 未使用38 =
  27. 未使用39 = 未使用40 = 未使用41 = 未使用42 = 未使用43 =
  28. 未使用44 = 未使用45 = 未使用46 = 未使用47 = 未使用48 = 未使用49 = 未使用50 = 未使用51 =
  29. 未使用52 = 未使用53 = 未使用54 = 未使用55 = 未使用56 = 未使用57 = 未使用58 = 未使用59 =
  30. 未使用60 = 未使用61 = 未使用62 = 未使用63 = 未使用64 = 未使用65 = 未使用66 =
  31. 未使用67 = 未使用68 = 未使用69 = 未使用70 = 未使用71 = 未使用72 = 未使用73 =
  32. 未使用74 = 未使用75 = 未使用76 = 未使用77 = 未使用78 = 未使用79 = 未使用80 =
  33. 未使用81 = 未使用82 = 未使用83 = 未使用84 = 未使用85 = 未使用86 =
  34. 未使用87 = 未使用88 = 未使用89 = 未使用90 = 未使用91 = 未使用92 =
  35. 未使用93 = 未使用94 = 未使用95 =
  36. 未使用96 = 未使用97 = 未使用98 = 未使用99 = 未使用100 = 0;
  37. }
  38.  
  39. 公共 静的void main(String[] args) {
  40. 試す {
  41. テスト();
  42. } キャッチ (エラー e) {
  43. システム。 out .println( "スタックの長さ:" + stackLength);
  44. eを投げる;
  45. }
  46. }
  47. }

出力:

  1. スタック長:6986
  2. スレッド「main」例外が発生しましたjava.lang.StackOverflowError
  3. cn.zhengsh.jvm.oom.JavaVMStackSOF2.test(JavaVMStackSOF2.java:22)
  4. cn.zhengsh.jvm.oom.JavaVMStackSOF2.test(JavaVMStackSOF2.java:22)

要約する

スタック フレームが大きすぎるか、仮想マシンのスタック容量が小さすぎるため、新しいスタック フレーム メモリを割り当てることができない場合、HotSpot 仮想マシンは StackOverflowError 例外をスローします。ただし、スタック容量の動的な拡張が可能な仮想マシン上で同じコードを使用すると、状況は異なります。

スレッドを作成するとメモリオーバーフローが発生する

注意: 次の実験を行うと、オペレーティング システムがフリーズする可能性があります。仮想マシンで実行することをお勧めします。

  1. /**
  2. * VM引数: -Xss512k
  3. *
  4. * @著者 鄭sh
  5. * @日付2021-08-13
  6. */
  7. パブリッククラスJavaVMStackOOM {
  8.      
  9. プライベートvoid dontStop() {
  10. )の間{
  11. }
  12. }
  13.  
  14. パブリックvoid スタックリークByThread() {
  15. )の間{
  16. スレッド thread = new Thread(new Runnable() {
  17. @オーバーライド
  18. パブリックボイド実行(){
  19. 停止しないでください();
  20. }
  21. });
  22. スレッドを開始します。
  23. }
  24. }
  25.  
  26. 公共 静的void main(String[] args) throws Throwable {
  27. JavaVMStackOOM oom = 新しい JavaVMStackOOM();
  28. oom.stackLeakByThread();
  29. }
  30. }

メソッド領域とランタイム定数プールのオーバーフロー

ランタイム定数プールはメソッド領域の一部であるため、これら 2 つの領域のオーバーフロー テストを一緒に実行できます。 HotSpot は JDK 7 から徐々に「永続世代をなくす」計画を開始し、JDK 8 ではメタスペースを完全に使用して永続世代を置き換えました。

メソッド領域のメモリオーバーフロー

メソッド領域の主な役割は、クラス名、アクセス修飾子、定数プール、フィールドの説明、メソッドの説明などの型関連の情報を保存することです。この領域をテストするための基本的な考え方は、実行時に大量のクラスを生成し、メソッド領域がオーバーフローするまで埋めることです。 Java SE API(GeneratedConstructorAccessorやリフレクション時の動的プロキシなど)を直接使用することでクラスを動的に生成することもできますが、今回の実験ではCGLibの助けを借りてバイトコードランタイムを直接操作することで、大量の動的クラスを生成しました。

  1. /**
  2. * VM引数: -XX:MetaspaceSize=21m -XX:MaxMetaspaceSize=21m
  3. *
  4. * @著者 鄭sh
  5. * @日付2021-08-13
  6. */
  7. パブリッククラスJavaMethodAreaOOM {
  8. 公共 静的void main(String[] args) {
  9. )の間{
  10. エンハンサー enhancer = new Enhancer();
  11. エンハンサー.setSuperclass(OOMObject.class);
  12. エンハンサー.setUseCache( false );
  13. エンハンサー.setCallback(新しいMethodInterceptor() {
  14. @オーバーライド
  15. パブリックオブジェクトインターセプト(オブジェクトobj、メソッドメソッド、オブジェクト[]引数、メソッドプロキシプロキシ)はThrowableをスローします{
  16. proxy.invokeSuper(obj, args)を返します
  17. }
  18. });
  19. エンハンサーを作成します();
  20. }
  21. }
  22.  
  23. 静的クラスOOMObject {
  24. }
  25. }

出力コード

  1. 原因: java.lang.OutOfMemoryError: Metaspace
  2. 原因: java.lang.OutOfMemoryError: Metaspace

定数プールの例

String::intern() は、文字列定数プールにこの String オブジェクトと等しい文字列がすでに含まれている場合に、プール内の文字列を表す String オブジェクトへの参照を返すネイティブ メソッドです。それ以外の場合、この String オブジェクトに含まれる文字列が定数プールに追加され、この String オブジェクトへの参照が返されます。 JDK 6 以前の HotSpot 仮想マシンでは、定数プールは永続世代に割り当てられます。 -XX:PermSize と -XX:MaxPermSize を通じて永続世代のサイズを制限できます。これにより、定数プールの容量が間接的に制限されます。

  1. /**
  2. * @著者 鄭sh
  3. * @日付2021-08-13
  4. */
  5. パブリッククラスRuntimeConstantPoolOOM2 {
  6. 公共 静的void main(String[] args) {
  7. 文字列 str1 = new StringBuilder( "Computer" ).append( "Software" ).toString();
  8. システム。出力.println(str1.intern() == str1);
  9. 文字列 str2 = new StringBuilder( "ja" ).append( "va" ).toString();
  10. システム。出力.println(str2.intern() == str2);
  11. }
  12. }

このコードを JDK 6 で実行すると、2 つの false 値が生成されますが、JDK 7 で実行すると、1 つの true 値と 1 つの false 値が生成されます。違いが生じる理由は、JDK 6 では、 intern() メソッドが最初に検出された文字列インスタンスを永続世代の文字列定数プールにコピーして保存し、永続世代の文字列インスタンスへの参照を返すためです。 StringBuilder によって作成された文字列オブジェクト インスタンスは Java ヒープ上にあるため、同じ参照になることはできず、結果は false を返します。 JDK 7 (および JRockit などの他の仮想マシン) の intern() メソッド実装では、文字列インスタンスを永続世代にコピーする必要がなくなりました。文字列定数プールは Java ヒープに移動されているため、定数プール内の最初のインスタンス参照のみを記録する必要があります。したがって、intern() によって返される参照は、StringBuilder によって作成された文字列インスタンスと同じです。 str2 の比較は false を返します。これは、String-Builder.toString() の実行前に文字列「java」がすでに出現しており、その参照がすでに文字列定数プール内にあるため、intern() メソッドで要求される「最初の遭遇」の原則を満たしていないためです。文字列「コンピュータ ソフトウェア」は初めて出現するため、結果は true を返します。

このマシンの直接メモリオーバーフロー

直接メモリの容量は、-XX:MaxDirectMemorySize パラメータで指定できます。デフォルトでは、Java ヒープの最大値 (-Xmx で指定) と一致します。このコードは、DirectByteBuffer クラスをバイパスし、メモリ割り当てのリフレクションを介して Unsafe インスタンスを直接取得します (Unsafe クラスの getUnsafe() メソッドは、ブートストラップ クラス ローダーのみがインスタンスを返すように指定しており、これは、仮想マシンの標準クラス ライブラリ内のクラスのみが Unsafe 関数を使用できるようにするという設計者の希望を反映しています。JDK 10 では、Unsafe の一部の関数が VarHandle を介して外部使用に開放されました)。 DirectByteBuffer はメモリを割り当てるときにメモリ オーバーフロー例外をスローしますが、例外をスローするときにオペレーティング システムにメモリ割り当てを実際に要求するわけではありません。代わりに、計算によってメモリを割り当てることができないことがわかった場合は、コード内でオーバーフロー例外を手動でスローします。メモリ割り当てを要求する実際のメソッドは Unsafe::allocateMemory() です。

  1. /**
  2. * VM引数: -Xmx20M -XX:MaxDirectMemorySize=10M
  3. *
  4. * @著者 鄭sh
  5. * @日付2021-08-13
  6. */
  7. パブリッククラス DirectMemoryOOM {
  8. プライベート静的最終int _1MB = 1024 * 1024;
  9.  
  10. 公共 静的void main(String[] args)は例外をスローします{
  11. フィールド unsafeField = Unsafe.class.getDeclaredFields()[0];
  12. unsafeField.setAccessible( true );
  13. 安全でない unsafe = (Unsafe)unsafeField.get( null );
  14. )の間{
  15. 安全ではありません。メモリを割り当てます(_1MB);
  16. }
  17. }
  18. }

出力:

  1. スレッド「main」例外が発生しましたjava.lang.OutOfMemoryError
  2. スレッド「main」例外が発生しましたjava.lang.OutOfMemoryError
  3.  
  4. java.base/jdk.internal.misc.Unsafe.allocateMemory(Unsafe.java:616)
  5. jdk.unsupported/sun.misc.Unsafe.allocateMemory(Unsafe.java:462)
  6. cn.zhengsh.jvm.oom.DirectMemoryOOM.main(DirectMemoryOOM.java:21)

参考文献

「JVM仮想マシンの徹底理解 - 第3版」周志明

https://docs.oracle.com/javase/specs/jls/se8/html/index.html

<<:  Docker イメージの削減: 1.43G から 22.4MB へ

>>:  Kafkaの独創的な高性能設計の1つ

推薦する

justhost - バレンタインデープロモーション / 月額 2.25 ドルの無制限ホスティング / 素晴らしい回線

justhost、バレンタインデーの特別オファー:月額料金はたったの 2.25 米ドル、justho...

簡単なレビュー:BandwagonHost Japan VPS、ソフトバンク回線使用、2.5〜10Gbpsの帯域幅、BandwagonHostがいかに優れているかを説明します

BandwagonHost VPS はいかがでしょうか?価格が高すぎるのですが、レンガ積みの作業はど...

概要: 2012 年 5 月 4 日の Google PR 値の更新

tui56フォーラムの王宝塵さんのように、GoogleのPR値の更新を待っているウェブマスターは間違...

ウェブサイトの重量が 6.28K から 4 レコードに

2012 年 6 月 28 日は、多くのウェブマスターや SEO 実践者に深い印象を残した日でした。...

SEOには秘密はないが、コツはある

多くの人は、SEO はキーワード ランキングと同じだと考えています。実際、キーワード ランキングは ...

タオバオの顧客になる前に考えるべきことと準備

今日のオンライン金儲け業界では、タオバオアフィリエイトプロジェクトの競争が非常に激しいです。どの段階...

シェア: 新しいウェブサイトを立ち上げてから35日以内にBaiduホームページにランクインした実践的な経験

私はしばらく企業ウェブサイトの仕事をしてきましたが、SEOの専門家に比べると、私はまだ新人です。私た...

AWS IoT デバイス管理の概要

AWS IoT Device Management を使用すると、大規模な IoT デバイスを簡単か...

次世代インターネット構築に関する意見が発表され、ファーウェイ、ZTEなどの企業が最初に恩恵を受けた

3月29日、国家発展改革委員会は「第12次5カ年計画期間における次世代インターネットの開発と建設に関...

高性能で軽量な分散メモリキューシステム - beanstalk

Beanstalk は、高性能、軽量、分散型のインメモリ メッセージ キュー システムです。当初の設...

企業の本当の嘘を最適化する

検索エンジンマーケティング業界には、ある程度、嘘や誤解を招く情報が溢れています。これらの誤解を招く情...

QingCloud ハイブリッドクラウド

[51CTO.comよりオリジナル記事] 近年、消費者層の構造や消費パターンが変化し、顧客ロイヤルテ...

WeChat 5.0後の影響と機会の分析

WeChat 5.0のリリースは大きな騒動を引き起こし、多くの人々に恐怖感を与えました。最も心配して...

信頼性の高いホスティングサービス5USD/月/768MB RAM/100GB HDD/1TBトラフィック

reliablehostingservices.net は 2010 年に設立され、ドメイン名は 2...