会社のプロジェクトがSpring5.3.xにアップグレードされた後、GCの数が急増し、私は愚かでした。

問題の背景

最近、私たちのプロジェクトはSpring Boot 2.4.6 + Spring Cloud 2020.0.xにアップグレードされましたが、アップグレード後、YoungGCが大幅に増加し、オブジェクトの割り当て率が大幅に増加したことがわかりましたが、プロモートされたオブジェクトの数は増加していません。これは、それらがすべて新しく作成されたオブジェクトであり、まもなくリサイクルされることを証明しています。プロセスの1つを監視しているところを見てみましょう。現時点では、httpリクエストレートは約100です。

これは非常に奇妙で、要求率はそれほど大きくありませんが、監視を通じて、1秒あたり約2ギガバイトのメモリが割り当てられていることがわかります。アップグレード前は、この割り当てレートは同じ要求レートで約100〜200MBでした。では、この余分なメモリはどこから来るのでしょうか?

ターゲティング

つまり、jmapコマンドを使用して、メモリ内のさまざまなオブジェクトの統計を確認する必要があります。同時に、監視からは、昇格したオブジェクトが増えていないため、老後のオブジェクトが多すぎないことがわかるため、生き残ったオブジェクトの統計だけを見ることができません。現在まだ生きているオブジェクトを除外できればもっと良いでしょう。同時に、GCは非常に頻繁であるため、1秒前後に1回あります。したがって、基本的に、一度に必要なjmapをキャッチすることは期待できません。同時に、jmapはすべてのスレッドをセーフポイントに入れ、したがってSTWに入れます。これは回線に一定の影響を与えるため、jmapはあまり頻繁に使用しないでください。したがって、次の戦略を採用します。

  1. インスタンスを展開してから、登録センターと現在のリミッターを介してインスタンスのトラフィックを半分に減らします。
  2. このインスタンスでは、jmap -histo(すべてのオブジェクトをカウントする)とjmap -histo:live(ライブオブジェクトのみをカウントする)を継続的に実行します。
  3. 2番目のステップを5回繰り返します。各間隔は、100ミリ秒、300ミリ秒、500ミリ秒、700ミリ秒です。
  4. このインスタンスの現在の制限を削除し、新しく展開されたインスタンスを閉じます。

これらのjmapの比較を通じて、jmap統計の上位のオブジェクトタイプにはSpringFrameworkがあることがわかりました。

 num     #instances         #bytes  class name (module)
-------------------------------------------------------
   1:       7993252      601860528  [B ([email protected])
   2:        360025      296261160  [C ([email protected])
   3:      10338806      246557984  [Ljava.lang.Object; ([email protected])
   4:       6314471      151547304  java.lang.String ([email protected])
   5:         48170      135607088  [J ([email protected])
   6:        314420      126487344  [I ([email protected])
   7:       4591109      110100264  [Ljava.lang.Class; ([email protected])
   8:        245542       55001408  org.springframework.core.ResolvableType
   9:        205234       29042280  [Ljava.util.HashMap$Node; ([email protected])
  10:        386252       24720128  [org.springframework.core.ResolvableType;
  11:        699929       22397728  java.sql.Timestamp ([email protected])
  12:         89150       21281256  [Ljava.beans.PropertyDescriptor; ([email protected])
  13:        519029       16608928  java.util.HashMap$Node ([email protected])
  14:        598728       14369472  java.util.ArrayList ([email protected])

このオブジェクトはどのように作成されますか?もはや生きていない頻繁に作成されたオブジェクトを見つける方法、そしてこのオブジェクトタイプはフレームワークの内部にありますか?

まず第一に、MAT(Eclipse Memory Analyzer)+ jmapダンプのヒープ分析全体は、次の理由であまり適用できません。

  1. オブジェクトはもう生きていません。MATはメモリリークの分析に適しています。ここで多くの予期しないオブジェクトを作成し、多くのメモリを占有しましたが、これらのオブジェクトはまもなく存続しなくなります。
  2. 生きていないオブジェクトの場合、MATは作成者を正確に分析できません。これは主に、ダンプ時に必要な情報をキャプチャできるかどうかが不明であるか、情報ノイズが多いためです。

この問題をこのように特定することはできませんが、収集したjmapダンプの結果をここに配置し、MAT分析の結果を使用して、すべての人が確認できるように表示します。

では、次の分析は何ですか?そして、それは私たちの旧友、JFR+JMCに戻ります。古い読者が知っているように、私はオンラインの問題を見つけるためにJFRをよく使用しますが、ここでどのように使用しますか?オブジェクトが頻繁に作成される直接的なJFRイベント統計はありませんが、非常に多くのオブジェクトを作成した人を間接的に反映できる間接的なイベントがあります。私は通常、次のように配置します。

  1. Thread Allocation Object Statisticsイベントを使用して、どのスレッドがあまりにも多くのオブジェクト(Thread Allocation Statistics)を割り当てているかを確認します。
  2. ホットスポットコード(メソッドプロファイリングサンプル)によって、これらのオブジェクトを生成する可能性のあるホットスポットコードを分析します。このように多数のオブジェクトの場合、Runnableを取得するためのコードは取得される可能性が高く、イベントの割合が高くなります。

最初にスレッド割り当て統計イベントを確認し、基本的にすべてのサーブレットスレッド(つまり、Httpリクエストを処理するスレッド、Undertowを使用するため、スレッド名はXNIOで始まる)に多くの割り当てられたオブジェクトがあり、それらを見つけることができないことを確認します。問題:

次に、ホットコードの統計を確認し、[メソッドプロファイリングのサンプル]イベントをクリックして、スタックトレースの統計を表示し、どのアカウントが比較的高いかを確認します。

上位の割合がこのResolvableTypeに関連しているようです。さらに検索するには、最初のメソッドをダブルクリックして、呼び出しスタックの統計を表示します。

BeanUtils.copyPropertiesという名前であることがわかりました。すべてBeanUtils.copyPropertiesに関連する他のResolvableType関連の呼び出しを参照してください。このメソッドは、同じタイプまたは異なるタイプ間でプロパティをコピーするために、プロジェクトで頻繁に使用されるメソッドです。このメソッドが非常に多くのResolvableTypeを作成するのはなぜですか?

ソースコードと問題の場所を表示する

ソースコードを見ると、Spring 5.3.x以降、BeanUtilsはResolvableTypeの統合クラス情報カプセル化を作成することでプロパティの複製を開始していることがわかりました。


/**
 * 
 * <p>As of Spring Framework 5.3, this method honors generic type information
 */
private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
		@Nullable String... ignoreProperties) throws BeansException {
}

内部のソースコードは、ソースオブジェクトとターゲットオブジェクトのタイプの属性メソッドごとに毎回新しいResolvableTypeを作成し、キャッシュを実行しませんこれにより、コピーが作成され、多数のResolvableTypeが作成されます。実験してみましょう。

public class Test {
    public static void main(String[] args)  {
        TestBean testBean1 = new TestBean("1", "2", "3", "4", "5", "6", "7", "8", "1", "2", "3", "4", "5", "6", "7", "8");
        TestBean testBean2 = new TestBean();
        for (int i = 0; i > -1; i++) {
            BeanUtils.copyProperties(testBean1, testBean2);
            System.out.println(i);
        }
    }
}

このコードを実行するには、spring-beans5.2.16.RELEASEとspring-beans5.3.9をそれぞれ使用します。JVMパラメーターは-XX:+
UnlockExperimentalVMOptions -XX:+ UseEpsilonGC -Xmx512mを使用します。つまり、これらのパラメーターはEpsilonGCを使用します。ヒープメモリがいっぱいで、GCが実行されず、OutofMemory例外が直接スローされてプログラムが終了し、最大ヒープメモリは512mです。このようにして、プログラムは実際に次のことを確認します。メモリが使い果たされる前に、BeanUtils.copyPropertiesのさまざまなバージョンを実行できる回数

テスト結果は次のとおりです。春豆5.2.16.RELEASEは444489回、春豆5.3.9は27456回です。それはかなり大きな違いです。

そこで、この問題に対応して、Spring-Frameworkgithubに問題を提起しました。

次に、プロジェクトでBeanUtils.copyPropertiesが頻繁に使用される場所については、BeanCopierに置き換えて、単純なクラスをカプセル化します。

public class BeanUtils {
    private static final Cache<String, BeanCopier> CACHE = Caffeine.newBuilder().build();

    public static void copyProperties(Object source, Object target) {
        Class<?> sourceClass = source.getClass();
        Class<?> targetClass = target.getClass();
        BeanCopier beanCopier = CACHE.get(sourceClass.getName() + " to " + targetClass.getName(), k -> {
            return BeanCopier.create(sourceClass, targetClass, false);
        });
        beanCopier.copy(source, target, null);
    }
}

ただし、BeanUtils.copyPropertiesを置き換えるBeanCopierの最も直接的な問題は、異なる名前で異なる名前のプロパティにコピーできないことです。たとえば、1つはintで、もう1つはIntegerです。同時に、ディープコピーにはいくつかの違いがあり、単体テストを行う必要があります。

変更後、問題は解決します。

元のリンク:
https ://www.cnblogs.com/zhxdick/p/15110071.html

著者:乾物でいっぱいの張ハッシュ

この記事が役に立ったと思われる場合は、リツイート、フォロー、サポートを行うことができます

おすすめ

転載: blog.csdn.net/m0_67645544/article/details/124435674