9

Oracle Java 1.7 update 6 以降では、 を使用するString.substringと文字列の内部文字配列がコピーされ、古いバージョンでは共有されることがわかっています。しかし、現在の動作を教えてくれる公式の API は見つかりませんでした。

使用事例

String.substring私のユースケースは次のとおりです。パーサーでは、基になる文字配列がコピーまたは共有されているかどうかを検出するのが好きです。問題は、文字配列が共有されている場合、new String(s)メモリの問題を回避するためにパーサーが明示的に「共有解除」する必要があることです。ただし、String.substringとにかくデータをコピーする場合、これは必要なく、パーサーでデータを明示的にコピーすることは回避できます。使用事例:

// possibly the query is very very large
String query = "select * from test ...";
// the identifier is used outside of the parser
String identifier = query.substring(14, 18);

// avoid if possible for speed,
// but needed if identifier internally 
// references the large query char array
identifier = new String(identifier);

私が必要なもの

基本的に、必要がないboolean isSubstringCopyingForSure()かどうかを検出する静的メソッドがnew String(..)必要です。があれば検知しなくてもOKですSecurityManager。基本的に、検出は控えめに行う必要があります (メモリの問題を回避するために、必要でなくても使用したいと思いますnew String(..))。

オプション

いくつかのオプションがありますが、特にOracle以外のJVMの場合、それらが信頼できるかどうかはわかりません:

String.offset フィールドの確認

/**
 * @return true if substring is copying, false if not or if it is not clear
 */
static boolean isSubstringCopyingForSure() {
    if (System.getSecurityManager() != null) {
        // we can not reliably check it
        return false;
    }
    try {
        for (Field f : String.class.getDeclaredFields()) {
            if ("offset".equals(f.getName())) {
                return false;
            }
        }
        return true;
    } catch (Exception e) {
        // weird, we do have a security manager?
    }
    return false;
}

JVM バージョンの確認

static boolean isSubstringCopyingForSure() {
    // but what about non-Oracle JREs?
    return System.getProperty("java.vendor").startsWith("Oracle") &&
           System.getProperty("java.version").compareTo("1.7.0_45") >= 0;
}

動作の確認 2 つのオプションがあり、どちらもかなり複雑です。1 つは、カスタム文字セットを使用して文字列を作成し、部分文字列を使用して新しい文字列 b を作成し、元の文字列を変更して b も変更されているかどうかを確認することです。2 番目のオプションは、巨大な文字列を作成してから、いくつかの部分文字列を作成し、メモリ使用量を確認することです。

4

4 に答える 4

3

確かに、この変更は 7u6 で行われました。この変更は厳密には実装の変更であり、API の変更ではなく、実行中の JDK の動作を検出する API もないため、これに関する API の変更はありません。ただし、変更により、アプリケーションがパフォーマンスやメモリ使用率の違いに気付く可能性は確かにあります。実際、7u4 では動作するが 7u6 では失敗する、またはその逆のプログラムを作成することは難しくありません。このトレードオフは大部分のアプリケーションにとって有利であると予想されますが、この変更の影響を受けるアプリケーションがあることは間違いありません。

文字列値が共有されている場合 (7u6 より前) について懸念しているのは興味深いことです。私が聞いたほとんどの人は反対の懸念を持っています。彼らは共有が好きで、共有されていない値への 7u6 の変更が問題を引き起こしている (または、問題が発生することを恐れている)。

いずれにせよ、やるべきことは推測ではなく測定です。

最初に、7u4 と 7u6 など、変更の有無にかかわらず、類似の JDK 間でアプリケーションのパフォーマンスを比較します。おそらく、GC ログまたはその他のメモリ監視ツールを確認する必要があります。違いが許容できる場合は、完了です。

7u6 より前の共有文字列値が問題の原因であると仮定すると、次のステップはnew String(s.substring(...))、文字列値を強制的に非共有にする簡単な回避策を試すことです。それからそれを測定します。繰り返しますが、パフォーマンスが両方の JDK で許容できる場合は、完了です!

共有されていないケースで への余分な呼び出しが受け入れられないことが判明した場合、new String()おそらくこのケースを検出して「非共有」呼び出しを条件付きにする最善の方法は、文字列のvalueフィールド ( )を反映し、char[]その長さを取得することです。 :

int getValueLength(String s) throws Exception {
    Field field = String.class.getDeclaredField("value");
    field.setAccessible(true);
    return ((char[])field.get(s)).length;
}

substring()元の文字列よりも短い文字列を返すへの呼び出しの結果の文字列を考えてみましょう。共有の場合、部分文字列は、上記のように取得されlength()た配列の長さとは異なります。value非共有の場合、それらは同じになります。例えば:

String s = "abcdefghij".substring(2, 5);
int logicalLength = s.length();
int valueLength = getValueLength(s);

System.out.printf("%d %d ", logicalLength, valueLength);
if (logicalLength != valueLength) {
    System.out.println("shared");
else
    System.out.println("unshared");

7u6 より古い JDK では、値の長さは 10 になりますが、7u6 以降では、値の長さは 3 になります。もちろん、どちらの場合も、論理的な長さは 3 になります。

于 2013-11-28T21:43:15.503 に答える
2

古い Java バージョンでString.substring(..)は、オリジナルと同じ char 配列を使用しますが、 と は異なりoffsetますcount

最新の Java バージョン (Thomas Mueller のコメントによると: 1.7 Update 6 以降) では、これが変更され、部分文字列が新しい char 配列で作成されるようになりました。

多くのソースを解析する場合、それに対処する最善の方法は、文字列の内部のチェックを避けることですが、この効果を予測して、必要な場所に常に新しい文字列を作成します (質問の最初のコード ブロックのように)。

String identifier = query.substring(14, 18);
// older Java versions: backed by same char array, different offset and count
// newer Java versions: copy of the desired run of the original char array

identifier = new String(identifier);
// older Java versions: when the backed char array is larger than count, a copy of the desired run will be made
// newer Java versions: trivial operation, create a new String instance which is backed by the same char array, no copy needed.

そうすれば、両方のバリアントを区別する必要がなく、不要な配列コピーのオーバーヘッドもなく、両方のバリアントで同じ結果が得られます。

于 2013-11-28T08:36:09.100 に答える
0

文字列のコピーを作成するのは本当にコストがかかると思いますか? JVMオプティマイザには文字列に関する組み込み機能があり、不要なコピーを回避していると私は信じています。また、大きなテキストは、コンパイラ コンパイラによって生成される LALR オートマトンなどのワンパス アルゴリズムで解析されます。そのため、パーサー入力は通常java.io.Reader、 solid ではなく、または別のストリーミング インターフェイスStringです。通常、構文解析はそれ自体が高価ですが、型チェックほど高価ではありません。文字列のコピーが実際のボトルネックになるとは思いません。仮定の前に、プロファイラーとマイクロベンチマークをよりよく体験してください。

于 2013-11-28T08:00:46.863 に答える