序文 この記事では、「JVM メモリ割り当ての原則の分析」や「一般的な JVM 問題のトラブルシューティング方法とツールの共有」など、オンライン JVM オフヒープ メモリ リークの問題をトラブルシューティングするプロセスとアイデアを記録します。皆様のお役に立てれば幸いです。 トラブルシューティングのプロセス全体を通して、私は多くの回り道をしましたが、今後の参考のために、学んだ教訓として、この記事に私の考えやアイデアをすべて書き留めておきます。記事の最後では、メモリ リークの迅速なトラブルシューティングのためのいくつかの原則もまとめました。 「この記事の主な内容:」
障害の説明 8 月 12 日の昼休み中に、当社のビジネス サービスは、サービス プロセスがコンテナーの物理メモリ (16G) の 80% 以上を占有しており、まだ増加しているというアラートを受け取りました。 監視システムはチャートを呼び出して次の情報を表示します。 たとえば、Java プロセスでメモリ リークが発生し、ヒープ メモリの制限が 4G であるとします。これは 4G を超えており、メモリがいっぱいになりつつあります。これは JVM オフヒープ メモリ リークであると考えられます。 その時のサービス プロセスの起動構成を確認します。
その日は新しいコードはリリースされていませんでしたが、「その日の朝、メッセージ キューを使用して履歴データの修復スクリプトをプッシュしていました。このタスクは、サービスの多数のインターフェイスを呼び出します」ため、当初はこのインターフェイスに関連しているのではないかと疑いました。 次の図は、その日の通話インターフェイスへの訪問回数の変化を示しています。 事故発生時の通話量は、通常の状況(1 分あたり 200 回以上)と比較して大幅に増加(1 分あたり 5000 回以上)していることがわかります。 「スクリプトによるメッセージの送信を一時的に停止したところ、API 呼び出しの数は 1 分あたり 200 回以上に減少しました。コンテナ メモリが急激に増加することはなくなり、すべて正常に戻ったようです。」 次に、このインターフェースにメモリ リークがあるかどうかを確認します。 トラブルシューティングのプロセス まず、次のトラブルシューティングのアイデアを説明しやすくするために、Java プロセスのメモリ割り当てを確認しましょう。 「オンラインで使用している JDK1.8 バージョンを例に挙げます。」 JVM のメモリ割り当てについてはネット上にまとめが多数あるので、二次創作は行いません。 JVM メモリ領域は、ヒープ領域と非ヒープ領域の 2 つの部分に分かれています。
「永続世代 (JDK8 ではネイティブ世代) には JVM ランタイムによって使用されるクラスが格納され、永続世代のオブジェクトはフル GC 中にガベージ コレクションされることに注意してください。」 JVM メモリの割り当てを確認した後、障害に戻りましょう。 ヒープメモリ分析 当初はヒープメモリとは関係ないことが基本的に確認されていましたが、漏洩したメモリがヒープメモリの上限である 4G を超えていたため、念のため手がかりを求めてヒープメモリを調べました。 新世代と旧世代のメモリ使用量曲線とリサイクル時間を観察しました。いつも通り、大きな問題はありませんでした。次に、事故現場のコンテナに JVM ヒープ メモリ ログをダンプしました。 ヒープメモリダンプ ヒープメモリスナップショットダンプコマンド:
ナレーション: 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 つの結論を導き出すことができます。
「前述のとおり、プロセスの起動パラメータには直接メモリの制限がないため、-XX:MaxDirectMemorySize 構成を追加し、サンドボックス環境で再度テストしました。」 プロセスが占有する物理メモリが増加し続け、設定した制限を超えてしまったため、構成が機能していないようでした。 これには驚きました。 JVM のメモリ制限に問題がありますか? 「この時点で、トラブルシューティング プロセス中の直接的なメモリ リークに焦点を当てることは永遠になくなったことがわかります。」 「ナレーション: JVM のメモリ制御を信頼する必要があります。パラメータが無効であることがわかった場合は、自分で理由を調べて、パラメータを誤って使用していないかどうかを確認する必要があります。」 直接記憶分析 直接記憶に何があるのかをさらに調査するために、私は直接記憶の研究を始めました。ダイレクト メモリではヒープ メモリのように占有されているすべてのオブジェクトを簡単に表示できないため、ダイレクト メモリを確認するにはいくつかのコマンドが必要です。ダイレクトメモリでどのような問題が発生したかを確認するために、いくつかの方法を使用しました。 プロセスメモリ情報 pmap を表示する pmap - プロセスのメモリマップを報告する pmap コマンドは、プロセスのメモリ マッピング関係を報告するために使用され、Linux のデバッグや操作および保守に適したツールです。
実行すると、次の出力が得られました。要約すると、次のようになります。
一番下の行は 4G を占めるヒープ メモリのマッピングであり、他の行には小さなメモリ占有物が多数あることがわかります。しかし、この情報だけでは問題はわかりません。 ネイティブメモリトラッキング ネイティブ メモリ トラッキング (NMT) は、VM 内のメモリ使用量を分析する Hotspot VM の機能です。 NMT データにアクセスするには、jcmd ツール (JDK に付属) を使用できます。 まず VM 起動パラメータを通じて NMT を有効にする必要がありますが、NMT を有効にするとパフォーマンスが 5% ~ 10% 低下することに注意してください。
次に、プロセスを実行し、次のコマンドを使用して直接メモリを表示します。
使用するのは:
結果は図のようになります。 上の写真の情報では問題が明確に示されていません。少なくとも当時の私には、これらの情報からはまだ問題がわかりませんでした。 捜査は行き詰まったようだ。 山と川は密集していて、出口がないように見える 調査が行き詰まったとき、WCS と SCF から「両者ともカプセル化にメモリ リークがないことを確認しました」という回答を受け取りました。 WCS は直接メモリを使用しません。また、SCF は基礎となる RPC プロトコルですが、このような明らかなメモリ バグは残りません。そうでなければ、オンラインで多くのフィードバックがあるはずです。 JVM メモリ情報を表示する jmap この時点では問題が見つからなかったため、新しいサンドボックス コンテナーを再度開き、サービス プロセスを実行してから、jmap コマンドを実行して JVM メモリの「実際の構成」を確認しました。
結果は次のとおりです。
出力情報から、古い世代と新しい世代の両方がまったく正常であり、メタスペースが占める容量は 20M のみであり、直接メモリも 2G であることがわかります... はぁ? MaxMetaspaceSize = 17592186044415 MB なのはなぜですか? 「制限はないようです。」 起動パラメータを詳しく見てみましょう。
構成は -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 をトリガーし、メタスペース メモリが再利用され、多くのクラスがアンロードされることがわかります。」 次に、メタスペースの制限を削除します。つまり、以前に問題を引き起こしたパラメータを使用します。
図に示すように: 「メタスペースが増加し、呼び出し回数が増えるにつれてロードされるクラスの数も増加しており、正の相関傾向を示していることがわかります。」 未来への新たな希望 問題は突然明らかになりました。「インターフェースを呼び出すたびに、クラスが絶えず作成され、メタスペースのメモリを占有している可能性が非常に高いです。」 JVM クラスの読み込みを監視 - 詳細 プログラムをデバッグするときに、プログラムによってロードされたクラス、メモリのリサイクル状態、呼び出されたローカル インターフェイスなどを表示する必要が生じることがあります。このような場合に -verbose コマンドが必要になります。 myeclipse では、右クリックして設定できます (以下を参照)。または、コマンド ラインで java -verbose と入力して表示することもできます。
ローカル環境では、インターフェース呼び出しをループするために起動パラメータ -verbose:class を追加します。 無数の com.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto が生成されていることがわかります。
何度も呼び出され、一定数のクラスが蓄積されたら、手動でフルGCを実行してクラスローダーをリサイクルすると、大量のfastjson関連のクラスがリサイクルされていることがわかります。 「リサイクル前に jmap を使用してクラスの読み込みステータスを表示すると、fastjson 関連のクラスも多数見つかります。」
ここで、「今回はコードを注意深くチェックする」という指示があり、コード ロジック内で fastjson が使用されている場所を確認し、次のコードを見つけます。
問題の根本原因 wcs を呼び出す前に、camelCase フィールドのエンティティ クラスをアンダースコア フィールドにシリアル化します。これには、静的メソッドでインスタンス化する fastjson の SerializeConfig を使用する必要があります。 SerializeConfig が作成されると、ターゲット オブジェクトをシリアル化するために ASM プロキシ クラスがデフォルトで作成されます。これは、上記の頻繁に作成されるクラス com.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto です。 SerializeConfig を再利用すると、fastjson は作成されたプロキシ クラスを探して再利用します。ただし、新しい SerializeConfig() が呼び出されると、元々生成されたプロキシ クラスが見つからず、新しい WlkCustomerDto プロキシ クラスが生成されます。 次の 2 つの画像は、問題箇所のソース コードです。 SerializeConfig をクラスの静的変数にすることで問題は解決しました。
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 仮想マシンを捨てる時が来ました!
Zorocloudはこれまで、CN2 GIA/AS4809、CUII/AS9929、AS4837など...
今年から運用を開始したVPSブランド「alphavps」は、過去2年間でAMD EPYCやRyzen...
なぜウェブサイトの最適化が必要なのでしょうか? ウェブサイトの最適化のメリットは何ですか? この質問...
1994年、中国本土初のインターネットBBS「曙光ステーション」がオンラインになりました。その後、水...
ご存知のとおり、ウェブサイトの最適化は非常に実践的な仕事です。多くの場合、SEO 最適化には、しっか...
[51CTO.com オリジナル記事] 2017年、中国のゲーム市場の収益は2,189億ドルに達した...
インターネット技術の成熟と発展に伴い、オンラインでのプロモーションとマーケティングも盛んになってきま...
10月24日のフェニックステクノロジーニュースによると、メディアは今朝、DianpingがすでにBa...
今回お勧めするアトランタ拠点の openvz VPS は Dotvps のものです。独立した IPv...
以前、Terraform を使用して Proxmox 仮想マシンをデプロイする方法についての記事を書...
SEO に携わる友人は、このタイトルの質問について考えたことがありますか? 実際、これはすべての S...
spinserversは今月、新しい独立サーバープロモーションを開始しました。e3v5は月額49ドル...
クラウド戦略は、企業組織におけるクラウド コンピューティングの役割についての概要を提供します。ガート...
さて!昨夜はまだ、Baidu で何が起こっているのか疑問に思っていて、Baidu のランキングと G...
[[354141]]この記事はWeChatの公開アカウント「故郷でJavaを学ぶ」から転載したもので...