メモリ管理の用語集を見ていたら、中国語で「Python の Pig (注: 貪欲で足りない蛇が象を飲み込むのと似ています)」という定義を偶然見つけたので、この記事を思いつきました。表面的には、この用語は、GC が大きなオブジェクトをある世代から別の世代に継続的に昇格させることを指します。これは、ニシキヘビが獲物を丸呑みして、消化している間動くことができないようにするのと同じです。
それから24時間、私の心はこの窒息するニシキヘビのイメージでいっぱいで、取り除くことができませんでした。精神科医が言うように、恐怖を和らげる最善の方法は、それを話すことです。そこでこの記事です。しかし、次に話したいのは Python ではなく、GC チューニングです。神に誓います。
GC の一時停止がパフォーマンスのボトルネックを引き起こしやすいことは誰もが知っています。最新の JVM には、リリース時に高度なガベージ コレクターが付属していますが、私の経験からすると、特定のアプリケーションに最適な構成を見つけるのは非常に困難です。手動チューニングにはまだ希望の光があるかもしれませんが、GC アルゴリズムの仕組みを正確に理解する必要があります。この点に関しては、この記事が役立ちます。以下では、JVM 構成の小さな変更がアプリケーションのスループットにどのような影響を与えるかを例を使って説明します。
例
スループットに対する GC の影響を実証するために使用したアプリケーションは、単純なプログラムでした。これには 2 つのスレッドが含まれています。
PigEater 巨大なニシキヘビが太った大きな豚を食べる過程を模倣します。このコードは、java.util.List に 32MB バイトを追加し、飲み込むたびに 100 ミリ秒スリープすることでこれを実行します。
PigDgester 非同期消化のプロセスをシミュレートします。ダイジェストを実装するコードは、豚のリストを空に設定するだけです。これは面倒なプロセスなので、このスレッドはリファレンスをクリアした後、毎回 2000 ミリ秒スリープします。
どちらのスレッドも while ループで実行され、ヘビが満杯になるまで食べて消化します。これには約5,000頭の豚を食べる必要がある。
次のようにコードをコピーします。
パッケージ eu.plumbr.demo;
パブリック クラス PigInThePython {
静的揮発性リスト pigs = new ArrayList();
static volatile int pigsEaten = 0;
静的最終整数 ENOUGH_PIGS = 5000;
public static void main(String[] args) throws InterruptedException {
新しい PigEater().start();
新しい PigDigester().start();
}
静的クラス PigEater は Thread { を拡張します
@オーバーライド
public void run() {
while (true) {
pigs.add(new byte[32 * 1024 * 1024]); //豚あたり 32MB
if (pigsEaten > ENOUGH_PIGS) が返される;
takeANap(100);
}
}
}
静的クラス PigDigester は Thread { を拡張します
@オーバーライド
public void run() {
ロングスタート = System.currentTimeMillis();
while (true) {
takeANap(2000);
pigsEaten+=pigs.size();
豚 = 新しい ArrayList();
if (豚を食べた > ENOUGH_PIGS) {
System.out.format("%d ミリ秒で %d 個の豚を消化しました。%n",pigsEaten, System.currentTimeMillis()-start);
戻る;
}
}
}
}
静的 void takeANap(int ms) {
試す {
Thread.sleep(ms);
} catch (例外 e) {
e.printStackTrace();
}
}
}
ここで、このシステムのスループットを「1 秒あたりに消化できる豚の数」として定義します。 100 ミリ秒ごとに豚がこの Python に詰め込まれることを考慮すると、このシステムの理論上の最大スループットは 1 秒あたり 10 豚に達する可能性があることがわかります。
GC構成例
2 つの異なる構成システムを使用した場合のパフォーマンスを見てみましょう。構成に関係なく、アプリケーションは 8GB RAM を搭載したデュアルコア Mac (OS X10.9.3) 上で実行されます。
最初の構成:
1.4G ヒープ (-Xms4g -Xmx4g)
2. CMS を使用して古い世代をクリーンアップし (-XX:+UseConcMarkSoupGC)、パラレル コレクターを使用して新しい世代をクリーンアップします (-XX:+UseParNewGC)
3. ヒープ (-Xmn512m) の 12.5% を新しい世代に割り当て、Eden 領域と Survivor 領域のサイズが同じになるように制限します。
2 番目の構成は少し異なります。
1.2G ヒープ (-Xms2g -Xms2g)
2. 新世代も旧世代も並列 GC (-XX:+UseParallelGC) を使用します。
3. ヒープの 75% を新しい世代に割り当てます (-Xmn 1536m)
4. 今度は、どちらの構成がパフォーマンスを向上させるかを賭けます (1 秒間に何頭の豚を食べられるか、覚えておいてください)。最初の構成にチップを搭載した人はがっかりするでしょう。結果はまったく逆です。
1. 最初の構成 (大規模なヒープ、大規模な旧世代、CMS GC) は 1 秒あたり 8.2 個の豚を食べることができます
2. 2 番目の構成 (小さなヒープ、大きな新世代、並列 GC) は 1 秒あたり 9.2 個の豚を食べることができます。
では、この結果を客観的に見てみましょう。割り当てられるリソースは 2 分の 1 に減少しますが、スループットは 12% 増加します。これは常識に反するため、何が起こっているのかをさらに分析する必要があります。
GC結果を分析する
その理由は実際には複雑ではありません。テストの実行時に GC が何を行っているかを詳しく調べることで答えが見つかります。ここで、使用するツールを選択します。 jstat の助けを借りて、その背後にある秘密を発見しました。コマンドは次のとおりです。
次のようにコードをコピーします。
jstat -gc -t -h20 PID 1s
データを分析すると、構成 1 では 1129 GC サイクル (YGCT_FGCT) が実行され、合計 63.723 秒かかったことがわかりました。
次のようにコードをコピーします。
タイムスタンプ S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT
594.0 174720.0 174720.0 163844.1 0.0 174848.0 131074.1 3670016.0 2621693.5 21248.0 2580.9 1006 63.182 116 0.236 63.419
595.0 174720.0 174720.0 163842.1 0.0 174848.0 65538.0 3670016.0 3047677.9 21248.0 2580.9 1008 63.310 117 0.236 63.546
596.1 174720.0 174720.0 98308.0 163842.1 174848.0 163844.2 3670016.0 491772.9 21248.0 2580.9 1010 63.354 118 0.240 63.595
597.0 174720.0 174720.0 0.0 163840.1 174848.0 131074.1 3670016.0 688380.1 21248.0 2580.9 1011 63.482 118 0.240 63.723
2 番目の構成では、合計 168 回 (YGCT+FGCT) 一時停止し、かかった時間はわずか 11.409 秒でした。
次のようにコードをコピーします。
タイムスタンプ S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT
539.3 164352.0 164352.0 0.0 0.0 1211904.0 98306.0 524288.0 164352.2 21504.0 2579.2 27 2.969 141 8.441 11.409
540.3 164352.0 164352.0 0.0 0.0 1211904.0 425986.2 524288.0 164352.2 21504.0 2579.2 27 2.969 141 8.441 11.409
541.4 164352.0 164352.0 0.0 0.0 1211904.0 720900.4 524288.0 164352.2 21504.0 2579.2 27 2.969 141 8.441 11.409
542.3 164352.0 164352.0 0.0 0.0 1211904.0 1015812.6 524288.0 164352.2 21504.0 2579.2 27 2.969 141 8.441 11.409
したがって、両方の場合のワークロードが等しいことを考慮すると、この豚を食べる実験では、GC が寿命の長いオブジェクトを見つけられなかった場合、GC はガベージ オブジェクトをより速くクリーンアップできます。最初の構成では、GC 動作の頻度は約 6 ~ 7 回、合計の一時停止時間は 5 ~ 6 回になります。
この物語を伝えることには 2 つの目的があります。まず最も重要なことは、このけいれんするニシキヘビのことを頭から追い出したかったということです。もう 1 つのより明白な利点は、GC チューニングが非常に熟練した経験であり、基礎となる概念を完全に理解する必要があることです。この記事で使用されているものは非常に一般的なアプリケーションにすぎませんが、選択のさまざまな結果もスループットと容量計画に大きな影響を与えます。実際のアプリケーションでは、この違いはさらに大きくなります。したがって、これらの概念をマスターするか、日常業務に集中してニーズに最適な GC 構成を Plumbr に判断させるかはあなた次第です。