JVMの文字列のリビルド

8225 ワード

Java 8 update 20に導入された文字列の重み付けの特性を簡単に紹介します.
平均すると、アプリケーション内のStringオブジェクトは大量のメモリを消費します.この一部は冗長です.同じ文字列には複数の異なるインスタンス(a!=bですが、a.equals(b))が存在します.実際には、異なる理由で冗長性をもたらす文字列がたくさんあります.
最初にJDKはString.intern()法を提供して文字列冗長性の問題を解決した.この方法の欠点は、どの文字列が駐在する必要があるかを見つけなければならないことです.これは、通常、冗長文字列検索機能を備えたスタック分析ツールが必要です.たとえば、
Youkit profiler .適切に使用すると、文字列の常駐はメモリを節約するのに非常に効果的なツールになります.これにより、文字列オブジェクト全体を再利用できます(各文字列オブジェクトは、最下位char[]に基づいて24バイトの追加オーバーヘッドが増加します).
Java 7 update 6から、Stringオブジェクトごとに独自のプライベートchar[]があります.これにより、JVMは自動的に最適化されます.最下位のchar[]が外部のクライアントに露出していない以上、JVMは2つの文字列の内容が一致しているかどうかを判断し、1つの文字列の最下位のchar[]を別の文字列の最下位のchar[]配列に置き換えることができます.
文字列のリダイレクトという特性はこれに用いられ、Java 8 update 20に導入されている.次に、その動作原理を示します.
1.G 1ゴミ回収機を使用して、この機能を有効にする必要があります:-XX:+UseG 1 GC-XX:+UseStringDeduplication.この特性は、G 1ゴミ回収器のオプションのステップとして実現され、他の回収器では使用できない場合に実現されます.
2.この特性はG 1回収器のminor GCフェーズで実行される.私の観察では、どれだけの空きCPUサイクルが実行されるかによって異なります.したがって、ローカルデータを処理するデータアナライザで実行されることを期待しないでください.つまり、WEBサーバではこの最適化が実行される可能性が高い.
3.文字列のリダイレクトは、処理されていない文字列を検索し、hash値(アプリケーションのコードで計算されていない場合)を計算し、他の文字列のhash値があるかどうかを確認し、下位のchar[]と同じです.見つかった場合、既存のchar[]を新しい文字列のchar[]で置き換えます.
4.文字列のリビルドは、数回GCを経ても生存している文字列のみを処理します.これにより、ほとんどの短いライフサイクルの文字列が処理されないことが保証されます.文字列のこの最小生存年齢は-XX:S t r i n g D u p l icationAgeThreshold=3のJVMパラメータで指定できます(3はこのパラメータのデフォルトです).
以下はこの実現のいくつかの重要な結論です.
  • 間違いなく、文字列を重んじて特性を重んじるこの無料ランチを楽しみたいなら、G 1回収器を使わなければなりません.parellel GCを使用すると使用できませんが、遅延期間よりもスループットの要求が高いアプリケーションではparellel GCがより良い選択になるはずです.
  • 文字列のリロードは、ロードされたシステムで実行できません.実行されたかどうかを確認するには、-XX:+P r i n t S t r i n g D e d u plicationStatisticsパラメータを使用してJVMを実行し、コンソールの出力を確認します.
  • メモリを節約したい場合は、アプリケーションで文字列を常駐させることができます.文字列を重くする機能に依存しないで、手を放してください.文字列の重さに注意する必要があるのは、すべての文字列を処理することです(少なくとも大部分でしょう).つまり、指定された文字列の内容が唯一であることを知っていますが(例えばGUID)、JVMはそれを知らないので、この文字列を他の文字列とマッチングしようとします.その結果、文字列のリダイレクトによって生じるCPUオーバーヘッドは、スタック内の文字列の数(新しい文字列と他の文字列を比較する)にも、文字列のリダイレクト間隔で作成した文字列の数(これらの文字列はスタック内の文字列と比較する)にも依存する.いくつかのGのスタックを持つJVMでは、-XX:+P r i n t S t r i n g D e d u plicationStatisticsオプションを使用して、この特性がどのように影響するかを見ることができます.
  • 一方、それは基本的にブロックされていない方法で完成しています.もしあなたのサーバーに十分な空きCPUがあれば、なぜ使わないのですか.
  • 最後に、String.internは、アプリケーションで指定された一部の既知の冗長文字列のみを生成できることを覚えておいてください.通常、より小さな駐在文字列のプールを比較するだけでいいです.つまり、CPUをより効率的に使用することができます.それだけでなく、文字列オブジェクト全体を常駐させることもできます.これにより、文字列ごとに24バイトも節約できます.

  • ここは私がこの特性を試験するためのテストクラスです.この3つのテストはJVMがOOMを放出するまで実行されるので、それぞれ単独で実行する必要があります.
    最初のテストでは、コンテンツのような文字列が作成されます.スタックに文字列が多い場合、文字列が重くなるのにどれだけの時間がかかるかを知りたい場合は、このテストが非常に役立ちます.最初のテストにできるだけ多くのメモリを割り当てます.文字列が多ければ多いほど、最適化の効果が高くなります.
    2、3番目のテストは、重量除去(2番目のテスト)と駐在(interning、3番目のテスト)の違いを比較します.同じXmx設定で実行する必要があります.プログラムではこの定数をXmx 256 Mに設定しましたが、もちろん、もっと割り当てることができます.しかし、interningテストに比べて、再テストはもっと早く切られることがわかります.これはなぜですか.このテストでは100個の異なる文字列しか存在しないため、それらを常駐させることは、メモリがこれらの文字列を格納するのに必要な空間にすぎないことを意味します.文字列を重くすると、最下位のchar[]配列のみを共有する異なる文字列オブジェクトが生成されます.
    
    
    
    /**
     * String deduplication vs interning test
     */
    public class StringDedupTest {
        private static final int MAX_EXPECTED_ITERS = 300;
        private static final int FULL_ITER_SIZE = 100 * 1000;
     
        //30M entries = 120M RAM (for 300 iters)
        private static List<String> LIST = new ArrayList<>( MAX_EXPECTED_ITERS * FULL_ITER_SIZE );
     
        public static void main(String[] args) throws InterruptedException {
            //24+24 bytes per String (24 String shallow, 24 char[])
            //136M left for Strings
     
            //Unique, dedup
            //136M / 2.9M strings = 48 bytes (exactly String size)
     
            //Non unique, dedup
            //4.9M Strings, 100 char[]
            //136M / 4.9M strings = 27.75 bytes (close to 24 bytes per String + small overhead
     
            //Non unique, intern
            //We use 120M (+small overhead for 100 strings) until very late, but can't extend ArrayList 3 times - we don't have 360M
     
            /*
              Run it with: -XX:+UseG1GC -XX:+UseStringDeduplication -XX:+PrintStringDeduplicationStatistics
              Give as much Xmx as you can on your box. This test will show you how long does it take to
              run a single deduplication and if it is run at all.
              To test when deduplication is run, try changing a parameter of Thread.sleep or comment it out.
              You may want to print garbage collection information using -XX:+PrintGCDetails -XX:+PrintGCTimestamps
            */
     
            //Xmx256M - 29 iterations
            fillUnique();
     
            /*
             This couple of tests compare string deduplication (first test) with string interning.
             Both tests should be run with the identical Xmx setting. I have tuned the constants in the program
             for Xmx256M, but any higher value is also good enough.
             The point of this tests is to show that string deduplication still leaves you with distinct String
             objects, each of those requiring 24 bytes. Interning, on the other hand, return you existing String
             objects, so the only memory you spend is for the LIST object.
             */
     
            //Xmx256M - 49 iterations (100 unique strings)
            //fillNonUnique( false );
     
            //Xmx256M - 299 iterations (100 unique strings)
            //fillNonUnique( true );
        }
     
        private static void fillUnique() throws InterruptedException {
            int iters = 0;
            final UniqueStringGenerator gen = new UniqueStringGenerator();
            while ( true )
            {
                for ( int i = 0; i < FULL_ITER_SIZE; ++i )
                    LIST.add( gen.nextUnique() );
                Thread.sleep( 300 );
                System.out.println( "Iteration " + (iters++) + " finished" );
            }
        }
     
        private static void fillNonUnique( final boolean intern ) throws InterruptedException {
            int iters = 0;
            final UniqueStringGenerator gen = new UniqueStringGenerator();
            while ( true )
            {
                for ( int i = 0; i < FULL_ITER_SIZE; ++i )
                    LIST.add( intern ? gen.nextNonUnique().intern() : gen.nextNonUnique() );
                Thread.sleep( 300 );
                System.out.println( "Iteration " + (iters++) + " finished" );
            }
        }
     
        private static class UniqueStringGenerator
        {
            private char upper = 0;
            private char lower = 0;
     
            public String nextUnique()
            {
                final String res = String.valueOf( upper ) + lower;
                if ( lower < Character.MAX_VALUE )
                    lower++;
                else
                {
                    upper++;
                    lower = 0;
                }
                return res;
            }
     
            public String nextNonUnique()
            {
                final String res = "a" + lower;
                if ( lower < 100 )
                    lower++;
                else
                    lower = 0;
                return res;
            }
        }
    }
    
    
    

    まとめ
    Java 8 update 20には文字列の重み付けの特性が追加されている.G 1ゴミ回収機の一部なので、G 1回収機を使用して有効にする必要があります:-XX:+UseG 1 GC-XX:+UseStringDeduplication.
  • 文字列デリバリーは、G 1のオプションのステージである.現在のシステム負荷に依存します.
  • 文字列は、同じ内容の文字列をクエリし、最下位の文字を格納するchar[]配列を統合します.このプロパティを使用すると、コードを書く必要はありませんが、最後に異なる文字列オブジェクトが得られ、各オブジェクトが24バイトを占有することを意味します.String.internを明示的に呼び出して駐在する必要がある場合があります.
  • 文字列の重さは、若い文字列を処理しません.文字列処理の最小年齢は-XX:S t r i n g D e d u p l icationAgeThreshold=3のJVMパラメータによって管理される(3はこのパラメータのデフォルト値).

  • オリジナル記事の転載は出典を明記してください.
    Java翻訳ステーション
    英語のテキストリンク