完全な JVM オフヒープ メモリ リークのトラブルシューティング記録

完全な JVM オフヒープ メモリ リークのトラブルシューティング記録

  [[339593]]

序文

この記事では、「JVM メモリ割り当ての原則の分析」や「一般的な JVM 問題のトラブルシューティング方法とツールの共有」など、オンライン JVM オフヒープ メモリ リークの問題をトラブルシューティングするプロセスとアイデアを記録します。皆様のお役に立てれば幸いです。

トラブルシューティングのプロセス全体を通して、私は多くの回り道をしましたが、今後の参考のために、学んだ教訓として、この記事に私の考えやアイデアをすべて書き留めておきます。記事の最後では、メモリ リークの迅速なトラブルシューティングのためのいくつかの原則もまとめました。

「この記事の主な内容:」

  • 障害の説明とトラブルシューティングのプロセス
  • 障害原因と解決分析
  • JVM ヒープメモリとオフヒープメモリの割り当て原則
  • 一般的なプロセス メモリ リークのトラブルシューティング手順とツールの紹介と使用

障害の説明

8 月 12 日の昼休み中に、当社のビジネス サービスは、サービス プロセスがコンテナーの物理メモリ (16G) の 80% 以上を占有しており、まだ増加しているというアラートを受け取りました。

監視システムはチャートを呼び出して次の情報を表示します。

たとえば、Java プロセスでメモリ リークが発生し、ヒープ メモリの制限が 4G であるとします。これは 4G を超えており、メモリがいっぱいになりつつあります。これは JVM オフヒープ メモリ リークであると考えられます。

その時のサービス プロセスの起動構成を確認します。

  1. -Xms4g -Xmx4g -Xmn2g -Xss1024K -XX:PermSize=256m -XX:MaxPermSize=512m -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=80

その日は新しいコードはリリースされていませんでしたが、「その日の朝、メッセージ キューを使用して履歴データの修復スクリプトをプッシュしていました。このタスクは、サービスの多数のインターフェイスを呼び出します」ため、当初はこのインターフェイスに関連しているのではないかと疑いました。

次の図は、その日の通話インターフェイスへの訪問回数の変化を示しています。

事故発生時の通話量は、通常の状況(1 分あたり 200 回以上)と比較して大幅に増加(1 分あたり 5000 回以上)していることがわかります。

「スクリプトによるメッセージの送信を一時的に停止したところ、API 呼び出しの数は 1 分あたり 200 回以上に減少しました。コンテナ メモリが急激に増加することはなくなり、すべて正常に戻ったようです。」

次に、このインターフェースにメモリ リークがあるかどうかを確認します。

トラブルシューティングのプロセス

まず、次のトラブルシューティングのアイデアを説明しやすくするために、Java プロセスのメモリ割り当てを確認しましょう。

「オンラインで使用している JDK1.8 バージョンを例に挙げます。」 JVM のメモリ割り当てについてはネット上にまとめが多数あるので、二次創作は行いません。

JVM メモリ領域は、ヒープ領域と非ヒープ領域の 2 つの部分に分かれています。

  • ヒープ領域: これは私たちがよく知っている新しい世代と古い世代です。
  • 非ヒープ領域: 図に示すように、非ヒープ領域にはメタデータ領域と直接メモリが含まれます。

「永続世代 (JDK8 ではネイティブ世代) には JVM ランタイムによって使用されるクラスが格納され、永続世代のオブジェクトはフル GC 中にガベージ コレクションされることに注意してください。」

JVM メモリの割り当てを確認した後、障害に戻りましょう。

ヒープメモリ分析

当初はヒープメモリとは関係ないことが基本的に確認されていましたが、漏洩したメモリがヒープメモリの上限である 4G を超えていたため、念のため手がかりを求めてヒープメモリを調べました。

新世代と旧世代のメモリ使用量曲線とリサイクル時間を観察しました。いつも通り、大きな問題はありませんでした。次に、事故現場のコンテナに JVM ヒープ メモリ ログをダンプしました。

ヒープメモリダンプ

ヒープメモリスナップショットダンプコマンド:

  1. jmap -dump:live、フォーマット=b、ファイル=xxxx.hprof pid

ナレーション: jmap -histo:live pid を使用して、ヒープ メモリ内のライブ オブジェクトを直接表示することもできます。

エクスポート後、ダンプ ファイルをローカル コンピューターにダウンロードし、Eclipse の MAT (メモリ アナライザー) または JDK に付属の JVisualVM を使用してログ ファイルを開きます。

以下に示すように、MAT を使用してファイルを開きます。

「ヒープメモリ内には、メッセージキューからメッセージを受け取っている nioChannel や nio.HeapByteBuffer など、nio 関連の大きなオブジェクトがいくつかあることがわかりますが、数が少ないので判断材料にはなりません。とりあえず観察してみましょう。」

次に、インターフェース コードの閲覧を始めました。インターフェイス内の主なロジックは、グループの WCS クライアントを呼び出し、データベース テーブル内のデータを検索して WCS に書き込むことです。他に追加のロジックはありません。

特別なロジックが見つからないため、WCS クライアントのカプセル化にメモリ リークがあるのではないかと考え始めました。この疑いの理由は、WCS クライアントが最下層の SCF クライアントによってカプセル化されているためです。 RPC フレームワークであるため、その基盤となる通信伝送プロトコルは直接メモリを要求する場合があります。

「私のコードが WCS クライアントのバグを引き起こし、直接メモリ呼び出しを継続的に要求して、最終的にメモリがいっぱいになった可能性はありますか?」

私は WCS の担当者に連絡し、遭遇した問題を説明しました。彼らは、問題を再現できるかどうかを確認するために、ローカルで書き込み操作のストレス テストを実行すると返答しました。

彼らからのフィードバックを待つには時間がかかるため、まずは自分たちで理由を解明するつもりです。

「私は直接メモリに疑いを集中しました。インターフェイス呼び出しの数が多すぎることと、クライアントによる nio の不適切な使用が、ByteBuffer を使用した直接メモリ要求の過度な増加につながっているのではないかと疑いました。」

「ナレーション: 最終結果は、この先入観が調査プロセスの回り道につながったことを証明しています。問題のトラブルシューティングのプロセスでは、合理的な推測を使用して調査の範囲を絞り込むことは問題ありませんが、最初にすべての可能性をリストアップすることが最善です。可能性を深く検討しても役に立たないことがわかったら、戻って他の可能性を慎重に検討する必要があります。」

サンドボックス環境の再現

当時の障害シナリオを復元するため、サンドボックス環境でストレステストマシンを申請し、オンライン環境との整合性を確保しました。

「まず、メモリ オーバーフロー (多数のインターフェイス呼び出し) をシミュレートしてみましょう。」

スクリプトにデータのプッシュとインターフェースの呼び出しを継続させ、メモリ使用量を監視し続けます。

通話が開始されると、メモリが継続的に増加し始め、制限がないように見えました (制限により Full GC はトリガーされませんでした)。

「次に、通常の通話音量(通常音量通話インターフェース)をシミュレートしてみましょう。」

インターフェースの通常の呼び出し量(比較的小さく、10 分ごとにバッチ呼び出しが行われます)をストレス テスト マシンに転送し、旧世代のメモリと物理メモリの次の傾向を取得しました。

「問題は、なぜメモリは増え続け、メモリがいっぱいになるのかということです。」

当時、JVM プロセスが直接メモリ サイズ (-XX:MaxDirectMemorySize) を制限していなかったため、オフヒープ メモリが増加し続け、FullGC 操作がトリガーされなかったと推測されました。

上の図から、2 つの結論を導き出すことができます。

  • メモリリークインターフェースの呼び出し回数が多い場合、ヒープ内の古い世代などが FullGC の条件を満たさない場合、FullGC が実行されず、メモリ使用量が増加し続けます。
  • 呼び出し量が少ない通常の状況では、メモリ リークはゆっくりと発生し、FullGC によってリークされた部分が常にリサイクルされます。そのため、問題は発生せず、システムは長期間にわたって正常に動作することができます。

「前述のとおり、プロセスの起動パラメータには直接メモリの制限がないため、-XX:MaxDirectMemorySize 構成を追加し、サンドボックス環境で再度テストしました。」

プロセスが占有する物理メモリが増加し続け、設定した制限を超えてしまったため、構成が機能していないようでした。

これには驚きました。 JVM のメモリ制限に問題がありますか?

「この時点で、トラブルシューティング プロセス中の直接的なメモリ リークに焦点を当てることは永遠になくなったことがわかります。」

「ナレーション: JVM のメモリ制御を信頼する必要があります。パラメータが無効であることがわかった場合は、自分で理由を調べて、パラメータを誤って使用していないかどうかを確認する必要があります。」

直接記憶分析

直接記憶に何があるのか​​をさらに調査するために、私は直接記憶の研究を始めました。ダイレクト メモリではヒープ メモリのように占有されているすべてのオブジェクトを簡単に表示できないため、ダイレクト メモリを確認するにはいくつかのコマンドが必要です。ダイレクトメモリでどのような問題が発生したかを確認するために、いくつかの方法を使用しました。

プロセスメモリ情報 pmap を表示する

pmap - プロセスのメモリマップを報告する

pmap コマンドは、プロセスのメモリ マッピング関係を報告するために使用され、Linux のデバッグや操作および保守に適したツールです。

  1. pmap -x pid ソートが必要な場合 |ソート -n -k3**

実行すると、次の出力が得られました。要約すると、次のようになります。

  1. ..
  2. 00007fa2d4000000 8660 8660 8660 rw --- [ 匿名 ]  
  3. 00007fa65f12a000 8664 8664 8664 rw --- [ 匿名 ]  
  4. 00007fa610000000 9840 9832 9832 rw --- [ 匿名 ]  
  5. 00007fa5f75ff000 10244 10244 10244 rw --- [ 匿名 ]  
  6. 00007fa6005fe000 59400 10276 10276 rw --- [ 匿名 ]  
  7. 00007fa3f8000000 10468 10468 10468 rw --- [ 匿名 ]  
  8. 00007fa60c000000 10480 10480 10480 rw --- [ 匿名 ]  
  9. 00007fa614000000 10724 10696 10696 rw --- [ 匿名 ]  
  10. 00007fa6e1c59000 13048 11228 0 rx -- libjvm.so  
  11. 00007fa604000000 12140 12016 12016 rw --- [ 匿名 ]  
  12. 00007fa654000000 13316 13096 13096 rw --- [ 匿名 ]  
  13. 00007fa618000000 16888 16748 16748 rw --- [ 匿名 ]  
  14. 00007fa624000000 37504 18756 18756 rw --- [ 匿名 ]  
  15. 00007fa62c000000 53220 22368 22368 rw --- [ 匿名 ]  
  16. 00007fa630000000 25128 23648 23648 rw --- [ 匿名 ]  
  17. 00007fa63c000000 28044 24300 24300 rw --- [ 匿名 ]  
  18. 00007fa61c000000 42376 27348 27348 rw --- [アノン]  
  19. 00007fa628000000 29692 27388 27388 rw --- [ 匿名 ]  
  20. 00007fa640000000 28016 28016 28016 rw --- [ 匿名 ]  
  21. 00007fa620000000 28228 28216 28216 rw --- [ 匿名 ]  
  22. 00007fa634000000 36096 30024 30024 rw --- [ 匿名 ]  
  23. 00007fa638000000 65516 40128 40128 rw --- [ 匿名 ]  
  24. 00007fa478000000 46280 46240 46240 rw --- [ 匿名 ]  
  25. 0000000000f7e000 47980 47856 47856 rw --- [ 匿名 ]  
  26. 00007fa67ccf0000 52288 51264 51264 rw --- [ 匿名 ]  
  27. 00007fa6dc000000 65512 63264 63264 rw --- [ 匿名 ]  
  28. 00007fa6cd000000 71296 68916 68916 rwx -- [ 匿名 ]  
  29. 00000006c0000000 4359360 2735484 2735484 rw --- [ 匿名 ]  

一番下の行は 4G を占めるヒープ メモリのマッピングであり、他の行には小さなメモリ占有物が多数あることがわかります。しかし、この情報だけでは問題はわかりません。

ネイティブメモリトラッキング

ネイティブ メモリ トラッキング (NMT) は、VM 内のメモリ使用量を分析する Hotspot VM の機能です。 NMT データにアクセスするには、jcmd ツール (JDK に付属) を使用できます。

まず VM 起動パラメータを通じて NMT を有効にする必要がありますが、NMT を有効にするとパフォーマンスが 5% ~ 10% 低下することに注意してください。

  1. -XX:NativeMemoryTracking=[オフ|概要 |詳細]
  2. #オフ: デフォルトではオフ
  3. # summary: 各カテゴリのメモリ使用量のみをカウントします。
  4. # 詳細:個々の呼び出しサイトによるメモリ使用量を収集します。

次に、プロセスを実行し、次のコマンドを使用して直接メモリを表示します。

  1. jcmd <pid> VM.native_memory [概要 |詳細 |ベースライン |要約.diff |詳細.diff |シャットダウン] [スケール= KB |翻訳[GB]
  2.   
  3. # summary: カテゴリ別のメモリ使用量。
  4. # detail: 概要情報に加えて仮想メモリ使用量を含む詳細なメモリ使用量。
  5. # ベースライン: 後で簡単に比較できるようにメモリ使用量のスナップショットを作成します
  6. # summary.diff: 前回のベースラインの要約と比較する
  7. # detail.diff: 詳細を前のベースラインと比較する
  8. # シャットダウン: NMTをシャットダウンする

使用するのは:

  1. jcmd pid VM.native_memory 詳細スケール=MB > temp .txt

結果は図のようになります。

上の写真の情報では問題が明確に示されていません。少なくとも当時の私には、これらの情報からはまだ問題がわかりませんでした。

捜査は行き詰まったようだ。

山と川は密集していて、出口がないように見える

調査が行き詰まったとき、WCS と SCF から「両者ともカプセル化にメモリ リークがないことを確認しました」という回答を受け取りました。 WCS は直接メモリを使用しません。また、SCF は基礎となる RPC プロトコルですが、このような明らかなメモリ バグは残りません。そうでなければ、オンラインで多くのフィードバックがあるはずです。

JVM メモリ情報を表示する jmap

この時点では問題が見つからなかったため、新しいサンドボックス コンテナーを再度開き、サービス プロセスを実行してから、jmap コマンドを実行して JVM メモリの「実際の構成」を確認しました。

  1. jmap -ヒープ pid

結果は次のとおりです。

  1. プロセス ID 1474接続しています。お待ちください...
  2. デバッガーが正常に接続されました。
  3. サーバーコンパイラが検出されました。
  4. JVMバージョンは25.66-b17です
  5.  
  6. 新世代並列スレッドを使用します。
  7. スレッドローカルなオブジェクト割り当てを使用します。
  8. 並行マークスイープGC
  9.  
  10. ヒープ構成:
  11. 最小ヒープ空き率 = 40
  12. 最大ヒープ空き率 = 70
  13. 最大ヒープサイズ = 4294967296 (4096.0MB)
  14. 新しいサイズ = 2147483648 (2048.0MB)
  15. 最大新規サイズ = 2147483648 (2048.0MB)
  16. 古いサイズ = 2147483648 (2048.0MB)
  17. 新しい比率 = 2
  18. 生存率 = 8
  19. メタスペースサイズ = 21807104 (20.796875MB)
  20. 圧縮クラススペースサイズ = 1073741824 (1024.0MB)
  21. 最大メタスペースサイズ = 17592186044415 MB
  22. G1ヒープ領域サイズ = 0 (0.0MB)
  23.  
  24. ヒープ使用量:
  25. 新世代(エデン + 1 サバイバースペース):
  26. 容量 = 1932787712 (1843.25MB)
  27. 使用済み = 1698208480 (1619.5378112792969MB)
  28. 空き= 234579232 (223.71218872070312MB)
  29. 87.86316621615607% 使用済み
  30. エデンスペース:
  31. 容量 = 1718091776 (1638.5MB)
  32. 使用済み = 1690833680 (1612.504653930664MB)
  33. 空き= 27258096 (25.995346069335938MB)
  34. 98.41346682518548% 使用済み
  35. から 空間
  36. 容量 = 214695936 (204.75MB)
  37. 使用済み = 7374800 (7.0331573486328125MB)
  38. 空き= 207321136 (197.7168426513672MB)
  39. 3.4349974840697497% 使用済み
  40.  空間
  41. 容量 = 214695936 (204.75MB)
  42. 使用済み = 0 (0.0MB)
  43. 無料= 214695936 (204.75MB)
  44. 0.0% 使用済み
  45. 同時マークスイープ生成:
  46. 容量 = 2147483648 (2048.0MB)
  47. 使用済み = 322602776 (307.6579818725586MB)
  48. 空き= 1824880872 (1740.3420181274414MB)
  49. 15.022362396121025% 使用済み
  50.  
  51. 29425 個のインターンされた文字列が 3202824 バイトを占める

出力情報から、古い世代と新しい世代の両方がまったく正常であり、メタスペースが占める容量は 20M のみであり、直接メモリも 2G であることがわかります...

はぁ? MaxMetaspaceSize = 17592186044415 MB なのはなぜですか? 「制限はないようです。」

起動パラメータを詳しく見てみましょう。

  1. -Xms4g -Xmx4g -Xmn2g -Xss1024K -XX:PermSize=256m -XX:MaxPermSize=512m -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=80

構成は -XX:PermSize=256m -XX:MaxPermSize=512m で、これは永続世代のメモリ空間です。 「1.8 以降、Hotspot 仮想マシンは永続世代を削除し、代わりにメタスペースを使用します。」 JDK1.8をオンラインで使用しているため、「メタスペースの最大容量に制限は一切ありません。」 -XX:PermSize=256m -XX:MaxPermSize=512m は 1.8 では古いパラメータです。

次の図は、1.7 から 1.8 への永続世代の変化を示しています。

「Metaspace でメモリリークが発生している可能性がありますか?」

ローカルでテストすることを選択したので、パラメータの変更が容易になり、JVisualVM ツールを使用してメモリの変更を視覚的に確認できるようになりました。

JVisualVMを使用して実行中のプロセスを観察する

まず、メタスペースを制限し、パラメータ -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=128m を使用して、問題のあるインターフェースをローカル ループで呼び出します。

図に示すように:

「メタスペースが使い果たされると、システムがフル GC をトリガーし、メタスペース メモリが再利用され、多くのクラスがアンロードされることがわかります。」

次に、メタスペースの制限を削除します。つまり、以前に問題を引き起こしたパラメータを使用します。

  1. -Xms4g -Xmx4g -Xmn2g -Xss1024K -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=80 -XX:MaxDirectMemorySize=2g -XX:+UnlockDiagnosticVMOptions

図に示すように:

「メタスペースが増加し、呼び出し回数が増えるにつれてロードされるクラスの数も増加しており、正の相関傾向を示していることがわかります。」

未来への新たな希望

問題は突然明らかになりました。「インターフェースを呼び出すたびに、クラスが絶えず作成され、メタスペースのメモリを占有している可能性が非常に高いです。」

JVM クラスの読み込みを監視 - 詳細

プログラムをデバッグするときに、プログラムによってロードされたクラス、メモリのリサイクル状態、呼び出されたローカル インターフェイスなどを表示する必要が生じることがあります。このような場合に -verbose コマンドが必要になります。 myeclipse では、右クリックして設定できます (以下を参照)。または、コマンド ラインで java -verbose と入力して表示することもできます。

  1. -verbose:class クラスの読み込みステータスを表示する
  2. -verbose:gc 仮想マシンのメモリ回復を表示します
  3. -verbose:jni ローカルメソッド呼び出しを表示する

ローカル環境では、インターフェース呼び出しをループするために起動パラメータ -verbose:class を追加します。

無数の com.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto が生成されていることがわかります。

  1. [ファイル:/C:/Users/yangzhendong01/.m2/repository/com/alibaba/fastjson/1.2.71/fastjson-1.2.71.jarからcom.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto をロードしました]
  2. [ファイル:/C:/Users/yangzhendong01/.m2/repository/com/alibaba/fastjson/1.2.71/fastjson-1.2.71.jarからcom.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto をロードしました]
  3. [ファイル:/C:/Users/yangzhendong01/.m2/repository/com/alibaba/fastjson/1.2.71/fastjson-1.2.71.jarからcom.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto をロードしました]
  4. [ファイル:/C:/Users/yangzhendong01/.m2/repository/com/alibaba/fastjson/1.2.71/fastjson-1.2.71.jarからcom.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto をロードしました]
  5. [ファイル:/C:/Users/yangzhendong01/.m2/repository/com/alibaba/fastjson/1.2.71/fastjson-1.2.71.jarからcom.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto をロードしました]
  6. [ファイル:/C:/Users/yangzhendong01/.m2/repository/com/alibaba/fastjson/1.2.71/fastjson-1.2.71.jarからcom.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto をロードしました]

何度も呼び出され、一定数のクラスが蓄積されたら、手動でフルGCを実行してクラスローダーをリサイクルすると、大量のfastjson関連のクラスがリサイクルされていることがわかります。

「リサイクル前に jmap を使用してクラスの読み込みステータスを表示すると、fastjson 関連のクラスも多数見つかります。」

  1. jmap -clstats 7984

ここで、「今回はコードを注意深くチェックする」という指示があり、コード ロジック内で fastjson が使用されている場所を確認し、次のコードを見つけます。

  1. /**
  2. * Json文字列を返します。キャメルケース変換_
  3. * @param bean エンティティ クラス。
  4. */
  5. 公共 静的文字列buildData(オブジェクトbean) {
  6. 試す {
  7. SerializeConfig CONFIG = 新しい SerializeConfig();
  8. CONFIG.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase;
  9. jsonString = JSON.toJSONString(bean, CONFIG)を返します
  10. } キャッチ (例外 e) {
  11. 戻る ヌル;
  12. }
  13. }

問題の根本原因

wcs を呼び出す前に、camelCase フィールドのエンティティ クラスをアンダースコア フィールドにシリアル化します。これには、静的メソッドでインスタンス化する fastjson の SerializeConfig を使用する必要があります。 SerializeConfig が作成されると、ターゲット オブジェクトをシリアル化するために ASM プロキシ クラスがデフォルトで作成されます。これは、上記の頻繁に作成されるクラス com.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto です。 SerializeConfig を再利用すると、fastjson は作成されたプロキシ クラスを探して再利用します。ただし、新しい SerializeConfig() が呼び出されると、元々生成されたプロキシ クラスが見つからず、新しい WlkCustomerDto プロキシ クラスが生成されます。

次の 2 つの画像は、問題箇所のソース コードです。

SerializeConfig をクラスの静的変数にすることで問題は解決しました。

  1. プライベート静的最終 SerializeConfig CONFIG = new SerializeConfig();
  2.  
  3. 静的{
  4. CONFIG.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase;
  5. }

fastjson SerializeConfig は何をしますか? SerializeConfig の紹介:

SerializeConfig の主な機能は、各 Java 型に対応するシリアル化クラス (ObjectSerializer インターフェースの実装クラス) を設定して記録することです。たとえば、Boolean.class はシリアル化実装クラスとして BooleanCodec (名前が示すように、このクラスはシリアル化とデシリアル化の実装を組み合わせたものです) を使用し、float[].class はシリアル化実装クラスとして FloatArraySerializer を使用します。これらのシリアル化実装クラスの一部は、FastJSON でデフォルトで実装されており (Java 基本クラスなど)、一部は ASM フレームワークによって生成され (ユーザー定義クラスなど)、さらに一部はユーザー定義のシリアル化クラスです (たとえば、Date 型フレームワークのデフォルトの実装ではミリ秒に変換されますが、アプリケーションで秒に変換する必要があります)。もちろん、これには、シリアル化クラスを生成するために ASM を使用するか、シリアル化のために JavaBean シリアル化クラスを使用するかという問題が関係します。ここでの判断は、Android 環境であるかどうか(環境変数「java.vm.name」が「dalvik」か「lemur」かで、Android 環境であるかどうか)で判断しますが、判断はここでだけではなく、後ほどより具体的な判断があります。

理論的には、各 SerializeConfig インスタンスが同じクラスをシリアル化する場合、シリアル化のためにそのクラスの以前に生成されたプロキシ クラスが見つかります。私たちのサービスは、インターフェースが呼び出されるたびに、ParseConfig オブジェクトをインスタンス化して Fastjson の逆シリアル化設定を構成します。 ASM エージェントが無効になっていない場合、ParseConfig への各呼び出しは新しいインスタンスであるため、作成されたプロキシ クラスをチェックすることはできません。そのため、Fastjson は常に新しいプロキシ クラスを作成し、それらをメタスペースにロードします。その結果、メタスペースが拡大し続け、マシンのメモリが枯渇することになります。

この問題はJDK1.8にアップグレードした後にのみ発生します。

問題の原因は依然として注目に値する。アップグレード前にこの問題が発生しなかったのはなぜですか?これには、jdk1.8 と 1.7 に付属するホットスポット仮想マシン間の違いを分析する必要があります。

jdk1.8 以降、組み込みのホストスポット仮想マシンは、以前の永続領域をキャンセルし、メタスペース領域を追加しました。機能的な観点から見ると、メタスペースは永続領域に似ていると考えられ、その主な機能もクラスメタデータを格納することですが、実際のメカニズムはまったく異なります。

まず、メタスペースのデフォルトの最大サイズはマシン全体の物理メモリのサイズであるため、メタスペースが継続的に拡張されると、Java プログラムがシステムの使用可能なメモリを占有し、最終的にはシステムに使用可能なメモリがなくなります。一方、永続領域は固定のデフォルト サイズを持ち、マシン全体の使用可能なメモリまで拡張されることはありません。割り当てられたメモリが枯渇すると、どちらもフル GC がトリガーされますが、違いは、永続領域がフル GC の場合、永続領域内のクラス メタデータ (Class オブジェクト) がヒープ メモリのリサイクルと同様のメカニズムでリサイクルされる点です。ルート参照によってオブジェクトに到達できない限り、そのオブジェクトはリサイクルできます。メタスペースは、これらのクラス メタデータをロードするクラスローダーがリサイクル可能かどうかに基づいて、クラス メタデータがリサイクル可能かどうかを決定します。クラスローダーがリサイクルできない限り、クラスローダーによってロードされたクラス メタデータはリサイクルされません。これは、1.8 にアップグレードした後にのみ 2 つのサービスで問題が発生した理由も説明しています。以前のバージョンの jdk では、fastjson が呼び出されるたびに多くのプロキシ クラスが作成され、多くのプロキシ クラス インスタンスが永続領域にロードされていましたが、これらのクラス インスタンスはメソッドが呼び出されたときに作成され、呼び出しが完了した後はアクセスできませんでした。したがって、永続領域がいっぱいになり、フル GC がトリガーされると、それらはリサイクルされます。

1.8 を使用する場合、これらのプロキシ クラスはメイン スレッドのクラスローダーを介してロードされるため、このクラスローダーはプログラムの実行中にリサイクルされることはなく、このクラスローダーを介してロードされたこれらのプロキシ クラスはリサイクルされることはなく、その結果、メタスペースが拡大し続け、最終的にマシンのメモリを使い果たすことになります。

この問題は fastjson に限定されません。これは、プログラムによってクラスがロードおよび作成する必要がある場所であればどこでも発生する可能性があります。 「特にフレームワークでは、バイトコードの拡張に ASM や javassist などのツールが多用されることが多いです。上記の分析によると、jdk1.8 より前では、ほとんどの場合、フル GC 中に動的にロードされたクラスをリサイクルできるため、問題は発生しにくいです。」したがって、多くのフレームワークやツールキットはこの問題に対処していません。 1.8 にアップグレードすると、これらの問題が明らかになる可能性があります。

要約する

問題は解決しました。次に、トラブルシューティングのプロセス全体を確認しました。このプロセス全体を通して、多くの問題が明らかになりました。最も重要なのは、「さまざまな JVM バージョンのメモリ割り当てに十分精通していない」ため、古い世代とメタスペースを誤って判断したことです。私は多くの回り道をして、直接メモリでトラブルシューティングに長い時間を費やし、多くの時間を無駄にしました。

第二に、調査には「慎重さと包括性」が求められる。事前にあらゆる可能性を整理しておくことが最善です。そうしないと、設定した調査範囲に陥って行き詰まってしまう可能性が高くなります。

最後に、この問題から学んだことをまとめてみましょう。

JDK1.8 以降、組み込みのホストスポット仮想マシンでは、以前の永続領域がキャンセルされ、メタスペース領域が追加されました。機能的な観点から見ると、メタスペースは永続領域に似ていると考えられ、その主な機能もクラスメタデータを格納することですが、実際のメカニズムはまったく異なります。

JVM 内のメモリは、私たちがよく知っているヒープ メモリだけでなく、直接メモリやメタ領域も含めて、起動時に制限する必要があります。これは、オンライン サービスの正常な動作を保証する最終的な保証です。

クラス ライブラリを使用する場合は、コードの記述方法に十分注意し、明らかなメモリ リークを回避するようにしてください。

ASM などのバイトコード拡張ツールを使用するクラスライブラリを使用する場合は注意してください (特に JDK1.8 以降)。

ランタイム オブザーバーのクラス読み込みプロセスを確認します。

blog.csdn.net/tenderheart/article/details/39642275

Metaspace 全体の紹介 (永久世代の置き換えの理由、Metaspace の特徴、Metaspace メモリの閲覧・解析方法)

https://www.cnblogs.com/duanxz/p/3520829.html

Java メモリ使用量例外 (オフヒープ メモリ例外を含む) の一般的なトラブルシューティング プロセス

https://my.oschina.net/haitaohu/blog/3024843

JVM ソースコード分析: オフヒープメモリの完全な説明

http://lovestblog.cn/blog/2015/05/12/direct-buffer/

JVM クラスのアンロード

https://www.cnblogs.com/caoxb/p/12735525.html

fastjson は jdk1.8 で asm を開きます

https://github.com/alibaba/fastjson/issues/385

fastjson:プロパティ命名戦略_cn

https://github.com/alibaba/fastjson/wiki/PropertyNamingStrategy_cn

動的プロキシによるメタスペースメモリリークに注意

https://blog.csdn.net/xyghehehehe/article/details/78820135

この記事はWeChat公開アカウント「Backend Technology Talk」から転載したものです。下のQRコードからフォローできます。この記事を転載する場合は、Backend Technology Talk のパブリック アカウントにお問い合わせください。

<<:  産業企業のニーズにより、エッジコンピューティング市場は爆発的に成長しようとしています

>>:  Windowsは素晴らしいです! Linux 仮想マシンを捨てる時が来ました!

推薦する

alphavpsはどうですか?ロサンゼルスデータセンターのAMDシリーズVPSの簡単なレビュー

今年から運用を開始したVPSブランド「alphavps」は、過去2年間でAMD EPYCやRyzen...

ウェブサイトの最適化によってウェブサイトに実際の取引量をもたらす方法

なぜウェブサイトの最適化が必要なのでしょうか? ウェブサイトの最適化のメリットは何ですか? この質問...

QQ、Weibo、Momoの社会的運命

1994年、中国本土初のインターネットBBS「曙光ステーション」がオンラインになりました。その後、水...

最適化の実践者は、SEO 最適化を盲目的に理解するのではなく、現実と組み合わせる必要があります。

ご存知のとおり、ウェブサイトの最適化は非常に実践的な仕事です。多くの場合、SEO 最適化には、しっか...

ゲームのスムーズさは実は Huawei Cloud の努力によるものだとは思わないかもしれません。

[51CTO.com オリジナル記事] 2017年、中国のゲーム市場の収益は2,189億ドルに達した...

ネットワークマーケティング競争で企業が成功する鍵は、人材の優位性にある

インターネット技術の成熟と発展に伴い、オンラインでのプロモーションとマーケティングも盛んになってきま...

Dianping、百度による共同創業者の20億ドル買収を否定:すべて噂だ

10月24日のフェニックステクノロジーニュースによると、メディアは今朝、DianpingがすでにBa...

Dotvps - 1g メモリ/50g ハードディスク/500g トラフィック/2IP/年間 28 ドル/アトランタ

今回お勧めするアトランタ拠点の openvz VPS は Dotvps のものです。独立した IPv...

Terraform を使用して Azure 仮想マシンを作成する

以前、Terraform を使用して Proxmox 仮想マシンをデプロイする方法についての記事を書...

SEO を行う際には、どのようなコンテンツがユーザーを呼び込み、維持できるかを考慮する必要があります。

SEO に携わる友人は、このタイトルの質問について考えたことがありますか? 実際、これはすべての S...

ガートナー: クラウド戦略策定における 10 のよくある間違い

クラウド戦略は、企業組織におけるクラウド コンピューティングの役割についての概要を提供します。ガート...

6月9日のBaiduの大型アップデートがSEO担当者に教えてくれること

さて!昨夜はまだ、Baidu で何が起こっているのか疑問に思っていて、Baidu のランキングと G...

夜も更けたので、分散ロックについて学んでみましょう

[[354141]]この記事はWeChatの公開アカウント「故郷でJavaを学ぶ」から転載したもので...