ホームページ  >  記事  >  Java  >  ミッションクリティカルなJavaアプリケーションのガベージコレクション最適化について詳しく解説(前編)

ミッションクリティカルなJavaアプリケーションのガベージコレクション最適化について詳しく解説(前編)

黄舟
黄舟オリジナル
2017-03-23 11:01:011020ブラウズ

最近、Sun/Oracle JVM で実行されるいくつかの Java ベースのショッピングおよびポータル プログラムをテストし、最適化する機会がありました。最もアクセスされているアプリはドイツにあります。多くの場合、ガベージ コレクションは Java サーバーのパフォーマンスにとって重要な要素です。この記事では、高度なガベージ コレクション アルゴリズムのアイデアといくつかの重要な調整パラメーターについて学びます。これらのパラメーターを実際のさまざまなシナリオで比較します。

ガベージ コレクションの観点から見ると、Java サーバー プログラムにはさまざまなニーズがある可能性があります。

  1. 一部の高トラフィック アプリケーションでは、大量のリクエストに応答し、非常に多数のオブジェクトを作成する必要があります。場合によっては、リソース消費量の多いフレームワークを使用するトラフィックが中程度のアプリケーションでも同じ問題が発生することがあります。つまり、これらの生成されたオブジェクトを効果的にクリーンアップする方法は、ガベージ コレクションにとって大きな課題です。

  2. また、アプリケーションによっては、長時間実行し、運用中に安定したサービスを提供する必要があり、時間の経過とともに徐々にパフォーマンスが低下したり、急激にパフォーマンスが低下したりしないことが求められます。

  3. 一部のシナリオでは、ユーザーの応答時間に厳しい制限が必要であり (オンライン ゲームや賭け事アプリケーションなど)、追加の GC 一時停止はほとんど許可されません。

多くのシナリオでは、優先順位の異なる複数のニーズを組み合わせることができます。私のサンプル プログラムのいくつかは、最初の点で 2 番目の点よりもはるかに高い要件を持っていますが、ほとんどのプログラムは 3 つの側面すべてについて同時に高い要件を持っているわけではありません。これにより、トレードオフの余地が十分に残されます。

デフォルト構成での JVM GC のパフォーマンス

JVM には多くの改善点がありますが、プログラムの実行中にタスクを最適化することはできません。前述の 3 つの点に加えて、デフォルトの JVM 設定には、メモリ使用量の削減という 2 番目に別の優先事項があります。何千人ものユーザーが十分なメモリを備えたサーバー上で実行されていないことを考慮してください。また、多くの電子商取引製品にとっても重要です。これらのアプリケーションは、ほとんどの場合、商品サーバーではなく開発用ラップトップで実行されるように構成されているからです。したがって、次の構成のようにサーバーが最小限のヒープ領域と GC パラメーターで構成されている場合、

java -Xmx1024m -XX:MaxPermSize=256m -cp Portal.jar my.portal.Portal

これによりシステムの実行が確実に非効率になります。まず、最大メモリ制限だけでなく、初期メモリ サイズも設定して、起動時にサーバーがメモリを徐々に増加させないようにすることをお勧めします。そうしないと非常に費用がかかります。サーバーに必要なメモリの量がわかっている場合 (やがてわかるはずです)、初期メモリ サイズを最大メモリ設定と同じに設定することをお勧めします。これは、次の JVM パラメータを通じて設定できます:

-Xms1024m -XX:PermSize=256m

JVM でよく設定される最後の基本オプションは、上記の設定方法と同様に、新世代のヒープ メモリ サイズを設定することです:

-XX:NewSize=200m -XX:MaxNewSize=200m

次の章では、上記の構成と、より複雑な構成について説明します。まず、かなり遅いテスト ホストで実行されているポータル アプリケーションを見てみましょう。負荷テストの際、ガベージ コレクションはどのように機能しますか:

図 1 約 25 時間でヒープ サイズがわずかに最適化された JVM の GC 動作 (-Xms1024m -Xmx1024m -XX:NewSize=200m -XX: MaxNewSize=200m)

その中で、青い曲線は時間の経過に伴う合計ヒープ メモリ使用量の変化を表し、灰色の縦線は GC 一時停止間隔を表します。

グラフに加えて、主要な指標と GC 操作のパフォーマンスが右端に表示されます。まず、このテスト中に作成 (および収集) されたガベージの平均量を見てみましょう。 30.5MB/秒の値は黄色でマークされています。これは、これはかなり大きいものの許容可能なガベージ レートであり、入門的な GC チューニングの例としては問題ありません。他の値は、このガベージをクリーンアップする際の JVM のパフォーマンスを示します。新しい世代ではガベージの 99.55% がクリーンアップされますが、古い世代では 0.45% のみがクリーンアップされます。この結果は非常に良好であるため、緑色でマークされています。

この結果の理由は、GC (およびユーザーリクエストを処理するワーカースレッド) によって導入された一時停止間隔からわかります。新しい世代の GC 間隔はたくさんありますが、非常に短く、平均して 6 秒に 1 回であり、継続時間は長くなりません。 50msを超えます。これらの一時停止により、JVM は合計時間の 0.77% 停止しましたが、サーバーの応答を待っているユーザーには各一時停止はまったく認識されませんでした。

一方、旧世代の GC の一時停止は、総時間の 0.19% にすぎません。ただし、この期間中、旧世代の GC はゴミの 0.45% しかクリーニングしませんでしたが、新世代の GC はゴミの 99.55% をクリーニングするのに 0.77% の時間を要しました。新世代の GC と比較して、旧世代の GC がいかに非効率であるかがわかります。さらに、旧世代の GC 一時停止の平均トリガー率は 1 時間に 1 回未満ですが、平均継続時間は 8 秒に達する可能性があり、最大異常値は 19 秒に達することもあります。これらの一時停止により、ユーザー リクエストを処理する JVM スレッドが実際に停止するため、一時停止はできるだけ頻度を低くし、継続時間を短くする必要があります。

上記の観察を通じて、世代別ガベージ コレクションの基本的な調整目標を導き出すことができます:

  • 新世代 GC は、旧世代 GC の頻繁な発生と短期間の発生を避けるために、できるだけ多くのガベージを収集する必要があります。

分代垃圾回收的基本思想与堆内存大小调整

先从下图开始。这个图可以通过JDK工具得到,比如jstat或者jvisualvm以及它的visualgc插件:

图2 JVM的堆内存结构,包括新生代的子分区(最左列)

Java的堆内存由永久代(Perm),老年代(Old)和新生代(New or Young)组成。新生代进一步划分为一个Eden空间和两个Survivor空间S0、S1。Eden空间是对象被创建时的地方,经过几轮新生代GC后,他们有可能被存放在Survivor空间。如果想了解更多,可以读一下Sun/Oracle的白皮书Memory Management in the Java HotSpot Virtual Machine

默认情况下,作为整体的新生代特别是Survivor空间太小,导致在GC清理大部分内存之前就无法保存更多对象。因此,这些对象被过早地保存在老年代中,这会导致老年代被迅速填满,必须频繁地清理垃圾。这也是图1中产生较多的Full GC暂停的原因。

(译者注:一般新生代的垃圾回收也称为Minor GC,老年代的垃圾回收称为Major GC或Full GC)

优化新生代内存大小

优化分代垃圾回收意味着让新生代,特别是Survivor空间,比默认情形大。但是同时也要考虑虚拟机使用的具体GC算法。

当前硬件上运行的Sun/Oracle虚拟机使用了ParallelGC作为默认GC算法。如果使用的不是默认算法,可以通过显式配置JVM参数来实现:

-XX:+UseParallelGC

默认情况下,这个算法并不在固定大小的Eden和Survivor空间中运行。它使用了一种自适应调整大小的策略,称为“AdaptiveSizePolicy”策略。正如描述的那样,它可以适应很多场景,包括服务器以外的机器的使用。但在服务器上运行时,这并不是最优策略。为了可以显式地设置固定的Survivor空间大小,可以通过以下JVM参数关闭它:

-XX:-UseAdaptiveSizePolicy

一旦这么设置后,就不能进一步增加新生代空间的大小,但我们可以有效地为Survivor空间设置合适的大小:

-XX:NewSize=400m -XX:MaxNewSize=400m -XX:SurvivorRatio=6

“SurvivorRatio=6”表示Survivor空间是Eden空间的1/6或者是整个新生代空间的1/8,在这个例子中就是50MB,而自适应大小策略经常运行在非常小的空间上,大约只有几MB。使用现在的配置,重复上面的负载测试,我们得到了下面的结果:

图3 堆内存优化后的JVM在50小时内的GC行为(-Xms1024m -Xmx1024m -XX:NewSize=400m -XX:MaxNewSize=400m -XX:-UseAdapativeSizePolicy -XX:SurvivorRatio=6)

这次的测试时间是上次的两倍,而垃圾的平均创建速率和之前基本一致(30.2MB/s,之前是30.5MB/s)。然而,整个过程只有两次老年代(Full)GC暂停,25小时左右才发生一次。这是因为老年代垃圾死亡速率(所谓的promation rate)从137kB/s减小到了6kB/s,老年代的垃圾回收只占整体的0.02%。同时新生代GC的暂停持续时间仅仅从平均48ms增加到57ms,两次暂停的间隔从6s增长到10s。总之,关闭了自适应大小调整,合理地优化堆内存大小,使GC暂停占总时间的比例从0.95%减小到0.59%,这是一个非常棒的结果。

优化后,使用ParNew算法作为默认ParallelGC的替代,也能得到相似的结果。这个算法是为了与CMS算法兼容而开发的,可以通过JVM参数来配置-XX:+UseParNewGC。关于CMS下面会提到。这个算法不使用自适应大小策略,可以运行在固定Survivor大小的空间上。因此,即使使用默认的配置SurvivorRatio=8,也比ParallelGC拥有更高的服务器利用率。

避免老年代GC的长时间暂停

上述结果的最后一个问题就是,老年代GC的长时间暂停平均为8s左右。通过适当的优化,老年代GC暂停已经很少了,但是一旦触发,对用户来说还是很烦人的。因为在暂停期间,JVM不能执行工作线程。在我们的例子中,8s的长度是由低速老旧的测试机导致的,在现代硬件上速度能快3倍左右。另一方面,现在的应用一般使用1G以上的堆内存,可以容纳更多的对象。当前的网络应用使用的堆内存能达到64GB,(至少)需要一半的内存来保存存活的对象。在这种情况下,8s对老年代暂停来说是很短的。这些应用中的老年代GC可以很随意地就接近1分钟,对于交互式网络应用来说是绝对不能接受的。

缓解这个问题的一个选择就是采用并行的方式处理老年代GC。默认情况下,在Java 6中,ParallelGC和ParNew GC算法使用多个GC线程来处理新生代GC,而老年代GC是单线程的。以ParallelGC回收器为例,可以在使用时添加以下参数:

-XX:+UseParallelOldGC

从Java 7开始,这个选项和-XX:+UseParallelGC默认被激活。但是,即使你的系统是4核或8核,也不要期望性能可以提高2倍以上。通常的结果会比2被小一些。在某些例子中,比如上述例子中的8s,这种提高还是比较有效的。但在很多极端的例子中,这还远远不够。解决方法是使用低延迟GC算法。

下篇中会讨论CMS(The Concurrent Mark and Sweep Collector)、幽灵般的碎片、G1(Garbage First)垃圾收集器和垃圾收集器的量化比较,最后给出总结。

以上がミッションクリティカルなJavaアプリケーションのガベージコレクション最適化について詳しく解説(前編)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。