文字列はどのアプリケーションでも多くのメモリを消費します。特に、個々の UTF-16 文字を含む char[] 配列は、各文字が 2 ビットを占めるため、JVM メモリ消費に最も大きく影響します。
実際、メモリの 30% が文字列によって消費されるのは非常に一般的です。これは、文字列が対話に最適な形式であるだけでなく、人気の HTTP API が大量の文字列を使用しているためでもあります。 Java 8 Update 20 では、文字列重複排除と呼ばれる新機能にアクセスできるようになりました。これには、デフォルトではオフになっている G1 ガベージ コレクターが必要です。
文字列の重複排除は、文字列の内部が実際には char 配列であり、最終的なものであるという事実を利用するため、JVM はそれらを任意に操作できます。
文字列の重複排除について、開発者は多数の戦略を検討しましたが、最終的な実装では次のアプローチが採用されました:
ガベージコレクターが String オブジェクトにアクセスするたびに、char 配列をマークします。 char 配列のハッシュ値を取得し、それを配列への弱参照とともに保存します。ガベージ コレクターが char 配列と同じハッシュ コードを持つ別の文字列を見つける限り、2 つは文字ごとに比較されます。
たまたま一致した場合、1 つの文字列が 2 番目の文字列の char 配列を指すように変更されます。最初の char 配列は参照されなくなり、リサイクルできます。
もちろん、このプロセス全体にはある程度のオーバーヘッドがかかりますが、それは非常に厳しい上限によって制御されます。たとえば、文字の繰り返しが見つからない場合、その文字は一定期間チェックされません。
では、この機能は実際にどのように機能するのでしょうか?まず、リリースされたばかりの Java 8 Update 20 が必要です。次に、この構成に従って次のコードを実行します: -Xmx256m -XX:+UseG1GC:
public class LotsOfStrings { private static final LinkedList<String> LOTS_OF_STRINGS = new LinkedList<>(); public static void main(String[] args) throws Exception { int iteration = 0; while (true) { for (int i = 0; i < 100; i++) { for (int j = 0; j < 1000; j++) { LOTS_OF_STRINGS.add(new String("String " + j)); } } iteration++; System.out.println("Survived Iteration: " + iteration); Thread.sleep(100); } } }
このコードは、30 回の反復を実行した後に OutOfMemoryError を報告します。
次に、文字列の重複排除を有効にし、次の構成を使用して上記のコードを実行します:
-Xmx256m -XX:+UseG1GC -XX:+UseStringDeduplication -XX:+PrintStringDeduplicationStatistics
この時点では、より長い時間実行できます。そして 50 回の反復後に終了します。
JVM は実行内容も出力します。見てみましょう:
[GC concurrent-string-deduplication, 4658.2K->0.0B(4658.2K), avg 99.6%, 0.0165023 secs] [Last Exec: 0.0165023 secs, Idle: 0.0953764 secs, Blocked: 0/0.0000000 secs] [Inspected: 119538] [Skipped: 0( 0.0%)] [Hashed: 119538(100.0%)] [Known: 0( 0.0%)] [New: 119538(100.0%) 4658.2K] [Deduplicated: 119538(100.0%) 4658.2K(100.0%)] [Young: 372( 0.3%) 14.5K( 0.3%)] [Old: 119166( 99.7%) 4643.8K( 99.7%)] [Total Exec: 4/0.0802259 secs, Idle: 4/0.6491928 secs, Blocked: 0/0.0000000 secs] [Inspected: 557503] [Skipped: 0( 0.0%)] [Hashed: 556191( 99.8%)] [Known: 903( 0.2%)] [New: 556600( 99.8%) 21.2M] [Deduplicated: 554727( 99.7%) 21.1M( 99.6%)] [Young: 1101( 0.2%) 43.0K( 0.2%)] [Old: 553626( 99.8%) 21.1M( 99.8%)] [Table] [Memory Usage: 81.1K] [Size: 2048, Min: 1024, Max: 16777216] [Entries: 2776, Load: 135.5%, Cached: 0, Added: 2776, Removed: 0] [Resize Count: 1, Shrink Threshold: 1365(66.7%), Grow Threshold: 4096(200.0%)] [Rehash Count: 0, Rehash Threshold: 120, Hash Seed: 0x0] [Age Threshold: 3] [Queue] [Dropped: 0]
便宜上、すべてのデータの合計を自分で計算する必要はなく、便利な合計を使用するだけです。
上記のコードスニペットは文字列の重複排除を実行することを規定しており、約12万個の文字列を表示するのに16ミリ秒かかりました。
上記の機能はリリースされたばかりなので、十分にレビューされていない可能性があります。実際のアプリケーション、特に文字列が複数回使用され渡されるアプリケーションでは、正確なデータが異なる場合があります。そのため、一部の文字列がスキップされたり、すでにハッシュコードが含まれている可能性があります (ご存知のとおり、文字列ハッシュ コードは遅延してロードされます)。
上記の場合、すべての文字列が重複排除され、メモリ内の 4.5MB のデータが削除されました。
[テーブル]セクションには内部追跡テーブルに関する統計が表示され、[キュー]には負荷によりドロップされた重複排除リクエストの数がリストされます。これもオーバーヘッド削減メカニズムの一部です。
それでは、文字列の重複排除と文字列の永続化の違いは何でしょうか?私のブログに、String Interning がメモリ効率にどれほど優れているかという記事があります。実際、文字列の重複排除と永続化は似ていますが、永続化メカニズムが文字配列だけでなく文字列インスタンス全体を再利用する点が異なります。
JDK Enhancement Proposal 192 の作成者の主張は、開発者が常駐文字列をどこに配置すればよいかわからない、または適切な場所がフレームワークによって隠されていることが多い、というものです。先ほども書きましたが、文字列 (国名など) の重複を排除するときに発生します。 )、文字列の重複排除は、同じ JVM 内のアプリケーションの文字列の重複にも適しています。これには、一般に文字列が複数回出現するとは考えられない XML スキーマ、URL、jar 名なども含まれます。文字列の永続化はアプリケーション スレッドで発生し、ガベージ コレクションは非同期かつ同時に処理されます。文字列の重複排除によってランタイムの消費が増加することはありません。これは、スリープなしで Thread.sleep() が見つかる理由も説明します。ただし、これはサンプル コードで発生する問題にすぎません。実際には、アプリケーションでは文字列の重複排除を実行するときに数ミリ秒がかかります。