JavaのString Flyweight実装の最良の選択肢
-
04-10-2019 - |
質問
私のアプリケーションは、集中的な文字列処理でマルチスレッドされています。私たちは過度のメモリの消費を経験しており、プロファイリングはこれが文字列データによるものであることを実証しています。メモリの消費は、ある種のフライウェイトパターンの実装やキャッシュを使用することで大きな恩恵を受けると思います(文字列がしばしば複製されることは確かですが、その点でハードデータはありませんが)。
Java Constant Pool and String.internを見ましたが、Permgenの問題を引き起こす可能性があるようです。
Javaでアプリケーション全体でマルチスレッドの弦のプールを実装するための最良の代替手段は何ですか?
編集:以前の関連する質問も参照してください。 Javaは、フードの下で弦のフライ級パターンをどのように実装していますか?
解決
注:この回答では、最新のランタイムJVMライブラリでは関連しない可能性のある例を使用しています。特に、 substring
例は、OpenJDK/Oracle 7+ではもはや問題ではありません。
私はそれが人々がよくあなたに言うことに反することを知っていますが、時には明示的に新しいものを作成します String
インスタンス できる あなたの記憶を減らすための重要な方法になります。
文字列は不変であるため、いくつかの方法はその事実を活用し、バッキング文字配列を共有してメモリを保存します。ただし、これらの配列の未使用部分のごみ収集を防ぐことにより、実際にメモリを増やすことがあります。
たとえば、ログファイルのメッセージIDを解析して警告IDを抽出していると仮定します。あなたのコードは次のようになります:
//Format:
//ID: [WARNING|ERROR|DEBUG] Message...
String testLine = "5AB729: WARNING Some really really really long message";
Matcher matcher = Pattern.compile("([A-Z0-9]*): WARNING.*").matcher(testLine);
if ( matcher.matches() ) {
String id = matcher.group(1);
//...do something with id...
}
しかし、実際に保存されているデータを見てください:
//...
String id = matcher.group(1);
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);
char[] data = ((char[])valueField.get(id));
System.out.println("Actual data stored for string \"" + id + "\": " + Arrays.toString(data) );
マッチャーは同じ文字データの周りに新しい文字列インスタンスをラップするだけなので、テストライン全体です。交換したときの結果を比較してください String id = matcher.group(1);
と String id = new String(matcher.group(1));
.
他のヒント
これはすでにJVMレベルで行われています。作成していないことを確認するだけです new String
s毎回、明示的または暗黙的に。
つまり、しません:
String s1 = new String("foo");
String s2 = new String("foo");
これにより、ヒープに2つのインスタンスが作成されます。むしろそうします:
String s1 = "foo";
String s2 = "foo";
これにより、ヒープに1つのインスタンスが作成され、両方とも同じものが参照されます(証拠として、 s1 == s2
戻ります true
ここ)。
も使用しないでください +=
文字列を連結する(ループ内):
String s = "";
for (/* some loop condition */) {
s += "new";
}
+=
暗黙的に作成します new String
毎回ヒープで。むしろそうします
StringBuilder sb = new StringBuilder();
for (/* some loop condition */) {
sb.append("new");
}
String s = sb.toString();
可能であれば、むしろ使用してください StringBuilder
またはその同期された兄弟 StringBuffer
それ以外の String
「集中文字列処理」用。それは、それらの目的のために有用な方法を提供します。 append()
, insert()
, delete()
, 、なども参照してください そのジャバドック.
メモリにひもを効率的に詰めましょう!私はかつて、ハイパーメモリ効率の高いセットクラスを書きました。そこでは、弦がツリーとして保存されていました。文字を横断することで葉に到達した場合、エントリはセットに含まれていました。一緒に作業するのも速く、大きな辞書を保存するのに理想的です。
また、私がプロファイリングしたほぼすべてのアプリで、文字列がメモリの最大の部分であることが多いことを忘れないでください。そのため、必要に応じて気にしないでください。
図:
ビール、豆、血の3つの弦があります。このようなツリー構造を作成できます。
B
+-e
+-er
+-ans
+-lood
たとえば、ストリート名のリストに対して非常に効率的です。これは、挿入物を効率的に実行できないため、固定辞書では明らかに最も合理的です。実際、構造を1回作成し、その後シリアル化し、その後ロードしただけです。
Java 7/8
あなたが受け入れられた答えが言っていることをしている場合、Java 7以降を使用している場合、あなたはそれがあなたが言っていることをしていません。
の実装 subString()
変更されました。
劇的に変化する可能性のある実装に依存しているコードを記述しないでください。古い行動に依存している場合、事態を悪化させる可能性があります。
1950 public String substring(int beginIndex, int endIndex) {
1951 if (beginIndex < 0) {
1952 throw new StringIndexOutOfBoundsException(beginIndex);
1953 }
1954 if (endIndex > count) {
1955 throw new StringIndexOutOfBoundsException(endIndex);
1956 }
1957 if (beginIndex > endIndex) {
1958 throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
1959 }
1960 return ((beginIndex == 0) && (endIndex == count)) ? this :
1961 new String(offset + beginIndex, endIndex - beginIndex, value);
1962 }
したがって、Java 7以降で受け入れられた回答を使用する場合は、収集する必要があるメモリの使用量とごみを2倍にします。
まず、その解析の一部を排除した場合、アプリケーションと開発者がどれだけ苦しむかを決定します。より速いアプリケーションは、従業員の売上高を2倍にしても、うまくいきません!あなたの質問に基づいて、私たちはあなたがすでにこのテストに合格したと仮定できると思います。
第二に、オブジェクトの作成を排除できない場合は、次の目標がエデンコレクションに耐えられないようにすることです。そして、解析はその問題を解決することができます。ただし、「適切に実装された」キャッシュ(その基本的な前提には同意しませんが、アテンダントの暴言であなたを退屈させることはありません)は通常、スレッドの競合をもたらします。ある種類のメモリ圧力を別の種類に置き換えることになります。
フルオンキャッシングから得られる一種の付随的損害から少なくなることが少ないパースルックアップイディオムのバリエーションがあり、それは単純な前計算されたルックアップテーブルです(「メモ」も参照)。これに通常見られるパターンはです 安全な列挙を入力します (TSE)。 TSEを使用すると、文字列を解析し、TSEに渡して関連する列挙型を取得し、文字列を捨てます。
テキストはフリーフォームを処理していますか、それとも入力は剛性仕様に従う必要がありますか?多くのテキストが可能な値の固定セットにレンダリングされた場合、TSEはここであなたを助けることができ、より大きなマスターに役立ちます。 。