この日記は私的なものであり所属会社の見解とは無関係です。 GitHub: takahashikzn

実はStringはメモリリークの原因だった(※1.7.0_06未満)

InfoQから。

http://www.infoq.com/news/2013/12/Oracle-Tunes-Java-String


Stringのような基本型に大きな仕様変更が入ることは稀ですが、
1.7.0_06でアグレッシブな変更が入ったらしいです。
以下、適当和訳。

In an ongoing effort to improve Java performance, Oracle has announced a change in the internal representation of strings in the String class as of Java 1.7.0_06.


The change, removing two non-static fields from the underlying String implementation, was done to help prevent memory leaks.

The original String implementation is based on four non-static fields.

The first is char value, which contains the characters comprising the String.

The second is int offset which holds the index of the first character from the value array.

The third is int count storing the number of characters to be used.

Fourth is int hash, which holds a cached value of the String hash code.


Oracle reported that a performance issue could arise in the original implementation when a String is created using the String.substring() call.

Substring() is called internally by many other API calls like Pattern.split().

When String.substring() is called, it refers to the internal char value from the original String characters.


The previous implementation was designed that way in order to produce a memory savings, since the substring would still refer to the original character data.

In addition String.substring() would run in constant time (O (1)) unlike the new implementation that runs in linear (O(n)) time.


However the old implementation had the possibility of producing a memory leak in cases where an application would extract a small String from an originally large String and then discard the original String.

In such a scenario, a live reference to the underlying original large char value from the original String is still retained, holding on to possibly many unused bytes of data.


To avoid this situation in earlier versions, Oracle suggests calling the new String(String) constructor on the small String.

That API copies only the required section of the underlying char thereby unlinking the new smaller String from the original large parent String.


In the new paradigm, the String offset and count fields have been removed, so substrings no longer share the underlying char [] value.

継続的に進められているJavaのパフォーマンス改善の一環として、OracleはStringの内部表現を1.7.0_06リリースで変更したことをアナウンスした。


変更とは、2つのインスタンスフィールドを削除したことだ。これはメモリリークを引き起こす可能性があったものだ。
変更前のStringの実装は、4つのインスタンスフィールドにより実現されていた。

  • 1つ目はchar[ ]。Stringが表す文字列となる文字を保持する。
  • 2つ目はoffset。これはchar[ ]を参照する最初の位置を表す。
  • 3つ目はcount。これはStringの文字数を表す。
  • 4つ目はhash。hashCode()の値をキャッシュするものだ。


Oracleは、String#substringで新たに作成された文字列にパフォーマンス上の問題が有ることを報告していた。substringは、例えばPattern#splitを初めとしてJDKの内部で大量に使われている。substringは、新しい文字列インスタンスを作成する際に、元の文字列のchar[ ]を参照する。


変更前の実装は、メモリの節約に主眼をおいて設計されていた一方で、元のchar[ ]を参照し続けることになる。付け加えると、変更前のsubstringの実装は定数時間( O(1) )で動作する一方で、新しい実装は線形時間( O(n) )で動作する。


変更前の実装では、元は大きな文字列だったものからsubstringで小さな文字列を切り出した場合に、元のchar[ ]への参照が生き続ける。つまり、もはや不要なはずの領域がガベージコレクトされずに残り続ける。


この問題を回避するために、Oracleはかつて、小さな文字列を作成する場合にはnew String(String)を使うことを推奨していた。このコンストラクタは、char[]の中から必要な物だけをコピーする実装になっているので、元の大きなchar[ ]を参照し続ける問題を回避できる。


新しい実装では、Stringクラスのoffsetとcountは削除されており、char[ ]の参照を共有しないようになっている。

JDK1.7.0_45の実装は?

まあこんな感じです。確かに上記通りの仕様になってます。

public final class String
    implements ... {

    // インスタンスフィールドはこの2つしか無い

    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    ...

    public String(char value[], int offset, int count) {
        ...(略) // 引数をチェックするコード

        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }

    ...

    public String substring(int beginIndex) {
        ...

        int subLen = value.length - beginIndex;

        ...

        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }
}