1. 概要

JavaのStringsは、Stringの文字を含むchar[]によって内部的に表されます。 また、 Javaは内部でUTF-16を使用するため、すべてのcharは2バイトで構成されます。

たとえば、 String に英語の単語が含まれている場合、ASCII文字は1バイトを使用して表すことができるため、charごとに先頭の8ビットはすべて0になります。

多くの文字はそれらを表すために16ビットを必要としますが、統計的にほとんどは8ビットのみを必要とします—LATIN-1文字表現。 したがって、メモリ消費とパフォーマンスを改善する余地があります。

また重要なのは、文字列は通常、JVMヒープスペースの大部分を占めるということです。 また、JVMによる保存方法のため、ほとんどの場合、Stringインスタンスは実際に必要なの2倍のスペースを占める可能性があります。

この記事では、JDK6で導入された圧縮文字列オプションと、最近JDK9で導入された新しいコンパクト文字列について説明します。 これらは両方とも、JMVでの文字列のメモリ消費を最適化するように設計されています。

2. 圧縮された文字列– Java 6

JDK 6アップデート21パフォーマンスリリースでは、新しいVMオプションが導入されました。

-XX:+UseCompressedStrings

このオプションを有効にすると、Stringschar[] –ではなくbyte [] として保存されるため、多くのメモリを節約できます。 ただし、このオプションは、主に意図しないパフォーマンスへの影響があったため、最終的にJDK7で削除されました。

3. コンパクトストリング– Java 9

Java 9は、コンパクトな Strings back。の概念をもたらしました。

つまり、文字列を作成するときは常に、文字列のすべての文字をバイトを使用して表すことができます— LATIN-1表現では、バイト配列が内部で使用され、1文字に1バイトが与えられます。 。

その他の場合、文字を表すために8ビット以上が必要な場合、すべての文字は、UTF-16表現ごとに2バイトを使用して格納されます。

したがって、基本的には、可能な限り、各文字に1バイトを使用します。

ここで問題は、すべてのString操作はどのように機能するのかということです。 LATIN-1表現とUTF-16表現をどのように区別しますか?

この問題に対処するために、Stringの内部実装に別の変更が加えられています。 この情報を保持する最後のフィールドcoderがあります。

3.1. String Java9での実装

これまで、Stringchar[]として保存されていました。

private final char[] value;

今後はbyte[]:になります

private final byte[] value;

変数coder

private final byte coder;

コーダーは次のようになります。

static final byte LATIN1 = 0;
static final byte UTF16 = 1;

ほとんどのString操作は、コーダーをチェックし、特定の実装にディスパッチするようになりました。

public int indexOf(int ch, int fromIndex) {
    return isLatin1() 
      ? StringLatin1.indexOf(value, ch, fromIndex) 
      : StringUTF16.indexOf(value, ch, fromIndex);
}  

private boolean isLatin1() {
    return COMPACT_STRINGS && coder == LATIN1;
}

JVMに必要なすべての情報が準備できて利用可能になると、 CompactStringVMオプションがデフォルトで有効になります。 これを無効にするには、次を使用できます。

+XX:-CompactStrings

3.2. コーダーのしくみ

Java 9 String クラスの実装では、長さは次のように計算されます。

public int length() {
    return value.length >> coder;
}

String にLATIN-1のみが含まれている場合、 coder の値は0になるため、Stringの長さはバイト配列。

その他の場合、 String がUTF-16表現の場合、 coder の値は1になるため、長さは実際のバイト配列の半分のサイズになります。

Compact String、に対して行われたすべての変更は、 String クラスの内部実装にあり、Stringを使用する開発者には完全に透過的であることに注意してください。

4. コンパクトストリング対。 圧縮された文字列

JDK 6 Compressed Stringsの場合、が直面した主な問題は、Stringコンストラクターが引数としてchar[]のみを受け入れることでした。 これに加えて、多くの String 操作は、バイト配列ではなく char[]表現に依存していました。 このため、多くの開梱を行う必要があり、パフォーマンスに影響を及ぼしました。

Compact Stringの場合、が余分なフィールド「コーダー」を維持すると、オーバーヘッドも増加する可能性があります。 コーダーのコストとバイトcharへのアンパック(UTF-16表現の場合)を軽減するために、いくつかの方法は[ X157X]intrinsifiedおよびJITコンパイラーによって生成されるASMコードも改善されました。

この変更により、直感に反する結果が生じました。 LATIN-1 indexOf(String)は組み込みメソッドを呼び出しますが、 indexOf(char)は呼び出しません。 UTF-16の場合、これらのメソッドは両方とも組み込みメソッドを呼び出します。 この問題はLATIN-1String にのみ影響し、将来のリリースで修正される予定です。

したがって、パフォーマンスの点では、コンパクトストリングは圧縮ストリングよりも優れています。

Compact Stringsを使用して節約されたメモリの量を調べるために、さまざまなJavaアプリケーションのヒープダンプが分析されました。 また、結果は特定のアプリケーションに大きく依存していましたが、全体的な改善はほとんどの場合かなりのものでした。

4.1. パフォーマンスの違い

Compact Strings:の有効化と無効化のパフォーマンスの違いの非常に簡単な例を見てみましょう。

long startTime = System.currentTimeMillis();
 
List strings = IntStream.rangeClosed(1, 10_000_000)
  .mapToObj(Integer::toString) 
  .collect(toList());
 
long totalTime = System.currentTimeMillis() - startTime;
System.out.println(
  "Generated " + strings.size() + " strings in " + totalTime + " ms.");

startTime = System.currentTimeMillis();
 
String appended = (String) strings.stream()
  .limit(100_000)
  .reduce("", (l, r) -> l.toString() + r.toString());
 
totalTime = System.currentTimeMillis() - startTime;
System.out.println("Created string of length " + appended.length() 
  + " in " + totalTime + " ms.");

ここでは、1,000万個の String を作成し、それらを単純な方法で追加しています。 このコードを実行すると(コンパクト文字列はデフォルトで有効になっています)、次の出力が得られます。

Generated 10000000 strings in 854 ms.
Created string of length 488895 in 5130 ms.

同様に、 -XX:-CompactStrings オプションを使用してコンパクト文字列を無効にして実行すると、出力は次のようになります。

Generated 10000000 strings in 936 ms.
Created string of length 488895 in 9727 ms.

明らかに、これは表面レベルのテストであり、非常に代表的なものではありません。これは、この特定のシナリオでパフォーマンスを向上させるために新しいオプションが実行できることのスナップショットにすぎません。

5. 結論

このチュートリアルでは、 String をメモリ効率の高い方法で格納することにより、JVMのパフォーマンスとメモリ消費を最適化する試みを確認しました。

いつものように、コード全体はGithub利用できます。