ガベージコレクションをゼロから設定するための基本原則

この記事では、ガベージコレクターの原理に焦点を当てたくありません。これは、ここで美しく明確に説明されています: habrahabr.ru/post/112676 。 JVMでガベージコレクションを設定する際の実用的な基本と定量的な特性について詳しく説明し、これがどのように効果的であるかを理解したいと思います。



GCパフォーマンス評価の定量的特性



次のインジケータを考慮してください。







原則として、記載されている特性は妥協であり、そのうちの1つの改善は残りのコストにつながります。 ほとんどのアプリケーションでは、3つの特性すべてが重要ですが、多くの場合、1つまたは2つの特性がアプリケーションにとってより重要です。これが構成の開始点になります。



GCチューニングの基本





GC設定を理解するための3つの基本的な基本ルールを検討してください。





makeObjects()メソッドが複数のスレッドでアクセスされ、ループが継続的に生成される単純なアプリケーション(たとえば、データベースにアクセスし、返された結果が蓄積されるWebアプリケーションの動作をエミュレートできる)の例を考えてみましょうヒープ上の特定のボリュームを占有するオブジェクト、それから計算が実行されます-遅延が発生し、オブジェクトへのリンクがメソッドからリークせず、完了時に、GCはこのオブジェクトをクリーニングする必要があることを理解できます。

package ru.skuptsov; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class MemoryConsumer implements Runnable { private static final int OBJECT_SIZE = 1024 * 1024; private static final int OBJECTS_NUMBER = 8; private static final int ADD_PROCESS_TIME = 1000; private static final int NUMBER_OF_REQUEST_THREADS = 50; private static final long EXPERIMENT_TIME = 30000; private static volatile boolean stop = false; public static void main(String[] args) throws InterruptedException { start(); Thread.sleep(EXPERIMENT_TIME); stop(); } private static void start() { ExecutorService execService = Executors.newCachedThreadPool(); for (int i = 0; i < NUMBER_OF_REQUEST_THREADS; i++) execService.execute(new MemoryConsumer()); } private static void stop() { stop = true; } @Override public void run() { while (true && !stop) { makeObjects(); } } private void makeObjects() { List<byte[]> objectList = new ArrayList<byte[]>(); for (int i = 0; i < OBJECTS_NUMBER; i++) { objectList.add(new byte[OBJECT_SIZE]); } try { Thread.sleep(ADD_PROCESS_TIME); } catch (InterruptedException e) { e.printStackTrace(); } } }
      
      







実験はしばらく続き、その後、ガベージコレクターによって引き起こされる合計遅延時間を使用して、有効性を評価します。 削除するオブジェクトの最終マーキング後に、クリーニング対象のオブジェクトへのリンクが表示されないように、遅延が必要です。 「stop-the-world」一時停止を引き起こさずにオブジェクトをマークおよびクリアできるjvmが存在するという事実と、さまざまなタイプのGCの機能(詳細はこちらhahahahabr.ru/post/148322を参照)は、そのようなオプションを考慮しません。



以下で実験を実行します。

 C:\>java -XX:+PrintCommandLineFlags -version -XX:MaxHeapSize=4290607104 -XX:ParallelGCThreads=8 -XX:+PrintCommandLineFlags -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC java version "1.6.0_16" Java(TM) SE Runtime Environment (build 1.6.0_16-b01) Java HotSpot(TM) 64-Bit Server VM (build 14.2-b01, mixed mode)
      
      





サーバーおよびUseParallelGCモードがデフォルトで有効になっているもの(スモールガベージコレクションフェーズのマルチスレッド操作)



合計休止時間を評価するには、ガベージコレクターをモードで実行します。

 java -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -verbose:gc -Xloggc:gc.log ru.skuptsov.MemoryConsumer
      
      





そして、gc.logによる遅延を要約します。

 0.167: [Full GC [PSYoungGen: 21792K->13324K(152896K)] [PSOldGen: 341095K->349363K(349568K)] 362888K->362687K(502464K) [PSPermGen: 2581K->2581K(21248K)], 0.0079385 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
      
      





real = 0.01秒は、アセンブリに費やされた実際の時間です。



VisualGCプラグインがインストールされたVisualVmユーティリティを使用すると、GCのさまざまな領域(Eden、Survivor1、Survivor2、Old)のメモリ分布を視覚的に観察し、ガベージコレクションの開始と期間の統計を確認できます。



必要なメモリのサイズを決定する



まず、アプリケーションが実際に必要とするよりも可能な限り大きなメモリサイズでアプリケーションを実行する必要があります。 最初にアプリケーションがメモリを占有する量がわからない場合は、-Xmxおよび-Xmsを指定せずにアプリケーションを起動できます。HotSpotVMはメモリサイズ自体を選択します。 アプリケーションの起動時にOutOfMemory(JavaヒープスペースまたはPermGenスペース)を取得した場合、エラーがなくなるまで、使用可能なメモリのサイズ(-Xmxまたは-XX:PermSize)を繰り返し増やすことができます。

次のステップは、長期間有効なライブデータのサイズを計算することです。これは、完全なガベージコレクションフェーズ後のヒープの古い永続的な領域のサイズです。 このサイズは、アプリケーションが機能するために必要なメモリのおおよその量であり、それを取得するには、一連の完全なアセンブリの後に領域のサイズを調べることができます。 原則として、-Xmsおよび-Xmxアプリケーションに必要なメモリのサイズは、ライブデータの量の3〜4倍です。 したがって、上記のログの場合、完全なガベージコレクションのフェーズ後の古い領域の値は349363Kです。 提案された値は-Xmxおよび-Xms〜1400 Mbです。 -XX:PermSizeおよび-XX:MaxPermSize-フルガベージコレクションフェーズ後のPermGenSizeの1.5倍-13324K〜20 Mb。 ライブデータのボリュームのサイズの1〜1.5〜525 MBに等しい若い世代のサイズを受け入れます。 次に、次のパラメーターを使用してjvm起動行を取得します。



 java -Xms1400m -Xmx1400m -Xmn525m -XX:PermSize=20m ru.skuptsov.MemoryConsumer
      
      







VisualVmでは、次の図を取得します。







実験のわずか30秒で、54個のアセンブリ(31個の小型アセンブリと23個のアセンブリ)が作成され、合計停止時間は3.227秒でした。 この遅延値は必要な要件を満たしていない可能性があります。アプリケーションコードを変更せずに状況を改善できるかどうか見てみましょう。



許容応答時間の設定



応答時間を設定する際には、以下のパラメーターを測定して考慮する必要があります。





若い世代と古い世代のサイズを調整する


小さなガベージコレクションのフェーズの実装に必要な時間は、若い世代のオブジェクトの数に直接依存します。サイズが小さいほど、期間は短くなりますが、頻度は増加します。 領域はより頻繁にいっぱいになり始めます。 古い世代のサイズを維持しながら、若い世代のサイズを小さくすることにより、各小さなアセンブリの時間を短縮してみましょう。 若い世代では、毎秒50ストリーム* 8オブジェクト* 1MB〜400MBをクリアする必要があると概算できます。 パラメーターを使用して実行します。



 java -Xms1275m -Xmx1275m -Xmn400m -XX:PermSize=20m ru.skuptsov.MemoryConsumer
      
      







VisualVmでは、次の図を取得します。







小さなガベージコレクションの合計動作時間(1.533秒)に影響を与えることはできませんでしたが、小さなアセンブリの頻度は増加しましたが、全体の時間は悪化しました-古い世代の充填速度の増加とフルガベージコレクションの呼び出し頻度のために3.661 これを克服するには-古い世代のサイズを増やしてみましょう-パラメーターを指定してjvmを実行します:



 java -Xms1400m -Xmx1400m -Xmn400m -XX:PermSize=20m ru.skuptsov.MemoryConsumer
      
      











一時停止の合計が改善され、2.637秒になりましたが、アプリケーションに必要なメモリの合計値は減少しました。したがって、特定のアプリケーションでオブジェクトの有効期間を分散するために、古い世代と若い世代の適切なバランスを繰り返し見つけることができます。



それでも遅延時間が適切でない場合は、-XXオプションをオンにしてコンカレントガベージコレクターに移動できます:+ UseConcMarkSweepGC-アプリケーションスレッドと並行して、別のスレッドでオブジェクトを削除対象としてマークするメインジョブを実行しようとするアルゴリズム。



コンカレントガベージコレクターの構成


ConcMarkSweep GCでは、より慎重なチューニングが必要です。主な目標の1つは、古い世代にオブジェクトを配置するための十分なスペースがない場合に、世界の停止の回数を減らすことです。 このフェーズは、スループットGCを使用した完全なガベージコレクションフェーズよりも平均して時間がかかります。 その結果、最悪の場合のガベージコレクションの期間が長くなる可能性があるため、古い世代の頻繁なオーバーフローを回避する必要があります。 原則として、ConcMarkSweep GCに切り替えるときは、古い世代のサイズを20〜30%増やすことをお勧めします-パラメーターを指定してjvmを実行します。



 java -Xms1680m -Xmx1680m -Xmn400m -XX:+UseConcMarkSweepGC -XX:PermSize=20m ru.skuptsov.MemoryConsumer
      
      











一時停止の合計は1.923秒に短縮されました。



生存者のサイズを調整する


以下のグラフでは、Eden、Survivor1、Survivor2のステージが旧世代に移行する前の移行数によるアプリケーションのメモリの分布を確認できます。 実際、ConcMarkSweep GCで古い世代のオーバーフローの数を減らす方法の1つは、生存者領域をバイパスして、若い世代から古い世代へのオブジェクトの直接フローを防ぐことです。



段階的にオブジェクトの分布を監視するには、-XX:+ PrintTenuringDistributionパラメーターを指定してjvmを実行します。

gc.logで次を確認できます。

 Desired survivor size 20971520 bytes, new threshold 1 (max 4) - age 1: 40900584 bytes, 40900584 total
      
      





生存者オブジェクトの合計サイズは40900584であり、CMSはデフォルトで生存者領域を埋めるために50%のバリアを使用します。 したがって、領域のサイズは約80 Mbになります。 jvmが起動すると、次の式から決定される-XX:SurvivorRatioパラメーターで指定されます。

 survivor space size = -Xmn<value>/(-XX:SurvivorRatio=<ratio> + 2)
      
      









取得します

 java -Xms1680m -Xmx1680m -Xmn400m -XX:SurvivorRatio=3 -XX:+UseConcMarkSweepGC -XX:PermSize=20m ru.skuptsov.MemoryConsumer
      
      





Edenスペースのサイズを同じままにしておくと、次のようになります。

 java -Xms1760m -Xmx1760m -Xmn480m -XX:SurvivorRatio=5 -XX:+UseConcMarkSweepGC -XX:PermSize=20m ru.skuptsov.MemoryConsumer
      
      











分布は良くなりましたが、アプリケーションの仕様により合計時間はそれほど変わりませんでした。事実、頻繁に小さなガベージコレクションを行った後、生存オブジェクトのサイズは常に生存領域の利用可能なサイズよりも大きいため、この場合、edenサイズのために正しい分布を犠牲にすることができますスペース:

 java -Xms1760m -Xmx1760m -Xmn480m -XX:SurvivorRatio=100 -XX:+UseConcMarkSweepGC -XX:PermSize=20m ru.skuptsov.MemoryConsumer
      
      











まとめ



その結果、合計メモリ消費量をわずかに増加させながら、30秒間の実験で合計一時停止のサイズを3.227秒から1.481秒に減らすことができました。 物理メモリのコストを削減する傾向と使用されるメモリを最大化する原理を考えると、それが大量であろうと少量であろうと、具体的な詳細に依存します-GCの異なる領域間のバランスを見つけることは依然として重要であり、このプロセスは科学的というよりも創造的です。



All Articles