Stringメモリトラップの概要
23075 ワード
Stringメソッドは、テキスト解析や大量の文字列処理に使用される場合、メモリのパフォーマンスに影響を与えます.メモリの消費量が大きすぎたり、OOMになったりする可能性があります.
一、まずStringオブジェクトのメモリ使用量を紹介する
一般に、Javaオブジェクトの仮想マシンの構造は、•オブジェクトヘッダ(object header):8バイト(オブジェクトのclass情報、ID、仮想マシン内の状態を保存する)•Java元のタイプデータ:int,float,charなどのタイプのデータ•リファレンス(reference):4バイト•パディング(padding)
String定義:
JDK6:private final char value[];private final int offset;private final int count;private int hash;
JDK 6の空の文字列が占めるスペースは40バイトです
JDK7:private final char value[];private int hash;private transient int hash32;
JDK 7の空文字列が占めるスペースも40バイト
JDK 6文字列メモリ占有の計算方式:まず1つの空のchar配列が占める空間を計算し、Javaでは配列もオブジェクトであるため、配列にもオブジェクトヘッダがあるため、1つの配列が占める空間はオブジェクトヘッダが占める空間に配列長、すなわち8+4=12バイトを加え、充填後16バイトとなる.
空のStringが占める空間は次のとおりです.
オブジェクトヘッダ(8バイト)+char配列(16バイト)+3 int(3× 4=12バイト)+1 char配列の参照(4バイト)=40バイト.
したがって、実際のStringが占める空間の計算式は次のようになります.
8*( ( 8+12+2*n+4+12)+7 )/8 = 8*(int) ( ( ( (n) *2 )+43)/8 )
ここで、nは文字列長である.
二、例を挙げる.
1、substring
このうちファイル「d:\teststring.txt」には33475740文字、ファイルサイズは35 Mあります.
JDK 6で上記のコードを実行すると、strsubはsubstring(0,1)だけを取り、countは確かに1しかありませんが、67 M近くのメモリを消費していることがわかります.
しかし、JDK 7で同じ上のコードを実行すると、strsubオブジェクトは40バイトしかありません.
何が原因ですか.
JDKのソースコードを見てみましょう.
JDK6:
JDK7:
元はJDK 6のStringによるものが見えます.substring()が返すStringは元のStringの参照を保存するため、元のStringは解放されず、予想外の大量のメモリ消費を招く.
JDK 6のこのような設計の目的も実はメモリを節約するためで、これらのStringはすべて元のStringを多重化したため、intタイプのofferset、count等値だけでsubstring後の新しいStringを識別します.
しかし、上記の例では、巨大なStringから少数のStringを切り取ることが後に用いられ、このような設計は冗長データを大量にもたらす.従ってStringを通過することについて.split()またはString.substring()Stringの操作を切り取る結論は以下の通りである.
•大きなテキストから少量の文字列を切り取るアプリケーションに対してString.substring()は、メモリの浪費を引き起こします.•通常のテキストから一定数切り取る文字列については、切り取る文字列の長いの総和が元のテキストの長さと大きく異なる、既存のString.substring()設計は、メモリを節約するために元のテキストを共有するのに適しています.
大量のメモリを消費する原因はStringである以上.substring()は、結果に大量の元のStringが含まれているので、メモリの浪費を減らす方法は、これらの元のStringを除去することです.新たにnewStringを呼び出すように、切り出した文字列のみを含むStringを構築する、Stringを呼び出すことができる.toCharArray()メソッド:
String newString = new String(smallString.toCharArray());
2、同じように、splitの方法を見てみましょう
JDK 6で分割された文字列配列では、String要素ごとに使用されるメモリは元の文字列のメモリサイズ(67 M):
JDK 7で分割された文字列配列では、各String要素は実際のメモリサイズです.
理由:
JDK 6ソースコード:
三、その他の方面:
2、静的文字列をつなぎ合わせるときは、通常コンパイラが最適化するため、できるだけ+を使用します.
対応するバイトコード:
Code:
0: ldc #24;//String str 1 str 2 str 3--文字列定数をスタックトップに押し込む
2: areturn
3、動的文字列をつなぎ合わせる時、できるだけStringBufferあるいはStringBuilderのappendを使って、このように構造の多すぎる臨時Stringオブジェクトを減らすことができます(javacコンパイラはString接続に対して自動的に最適化します):
対応バイトコード(JDK 1.5以降、StringBuilder.appendメソッドを呼び出すように変換):
Code:
一、まずStringオブジェクトのメモリ使用量を紹介する
一般に、Javaオブジェクトの仮想マシンの構造は、•オブジェクトヘッダ(object header):8バイト(オブジェクトのclass情報、ID、仮想マシン内の状態を保存する)•Java元のタイプデータ:int,float,charなどのタイプのデータ•リファレンス(reference):4バイト•パディング(padding)
String定義:
JDK6:private final char value[];private final int offset;private final int count;private int hash;
JDK 6の空の文字列が占めるスペースは40バイトです
JDK7:private final char value[];private int hash;private transient int hash32;
JDK 7の空文字列が占めるスペースも40バイト
JDK 6文字列メモリ占有の計算方式:まず1つの空のchar配列が占める空間を計算し、Javaでは配列もオブジェクトであるため、配列にもオブジェクトヘッダがあるため、1つの配列が占める空間はオブジェクトヘッダが占める空間に配列長、すなわち8+4=12バイトを加え、充填後16バイトとなる.
空のStringが占める空間は次のとおりです.
オブジェクトヘッダ(8バイト)+char配列(16バイト)+3 int(3× 4=12バイト)+1 char配列の参照(4バイト)=40バイト.
したがって、実際のStringが占める空間の計算式は次のようになります.
8*( ( 8+12+2*n+4+12)+7 )/8 = 8*(int) ( ( ( (n) *2 )+43)/8 )
ここで、nは文字列長である.
二、例を挙げる.
1、substring
package demo;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
public class TestBigString
{
private String strsub;
private String strempty = new String();
public static void main(String[] args) throws Exception
{
TestBigString obj = new TestBigString();
obj.strsub = obj.readString().substring(0,1);
Thread.sleep(30*60*1000);
}
private String readString() throws Exception
{
BufferedReader bis = null;
try
{
bis = new BufferedReader(new InputStreamReader(new FileInputStream(newFile("d:\\teststring.txt"))));
StringBuilder sb = new StringBuilder();
String line = null;
while((line = bis.readLine()) != null)
{
sb.append(line);
}
System.out.println(sb.length());
return sb.toString();
}
finally
{
if (bis != null)
{
bis.close();
}
}
}
}
このうちファイル「d:\teststring.txt」には33475740文字、ファイルサイズは35 Mあります.
JDK 6で上記のコードを実行すると、strsubはsubstring(0,1)だけを取り、countは確かに1しかありませんが、67 M近くのメモリを消費していることがわかります.
しかし、JDK 7で同じ上のコードを実行すると、strsubオブジェクトは40バイトしかありません.
何が原因ですか.
JDKのソースコードを見てみましょう.
JDK6:
1 public String substring(int beginIndex, int endIndex) {
2
3 if (beginIndex < 0) {
4
5 throw new StringIndexOutOfBoundsException(beginIndex);
6
7 }
8
9 if (endIndex > count) {
10
11 throw new StringIndexOutOfBoundsException(endIndex);
12
13 }
14
15 if (beginIndex > endIndex) {
16
17 throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
18
19 }
20
21 return ((beginIndex == 0) && (endIndex == count)) ? this :
22
23 new String(offset + beginIndex, endIndex - beginIndex, value);
24
25 }
26
27 // Package private constructor which shares value array for speed.
28
29 String(int offset, int count, char value[]) {
30
31 this.value = value;
32
33 this.offset = offset;
34
35 this.count = count;
36
37 }
JDK7:
1 public String substring(int beginIndex, int endIndex) {
2
3 if (beginIndex < 0) {
4
5 throw new StringIndexOutOfBoundsException(beginIndex);
6
7 }
8
9 if (endIndex > value.length) {
10
11 throw new StringIndexOutOfBoundsException(endIndex);
12
13 }
14
15 int subLen = endIndex - beginIndex;
16
17 if (subLen < 0) {
18
19 throw new StringIndexOutOfBoundsException(subLen);
20
21 }
22
23 return ((beginIndex == 0) && (endIndex == value.length)) ? this
24
25 : new String(value, beginIndex, subLen);
26
27 }
28
29 public String(char value[], int offset, int count) {
30
31 if (offset < 0) {
32
33 throw new StringIndexOutOfBoundsException(offset);
34
35 }
36
37 if (count < 0) {
38
39 throw new StringIndexOutOfBoundsException(count);
40
41 }
42
43 // Note: offset or count might be near -1>>>1.
44
45 if (offset > value.length - count) {
46
47 throw new StringIndexOutOfBoundsException(offset + count);
48
49 }
50
51 this.value = Arrays.copyOfRange(value, offset, offset+count);
52
53 }
元はJDK 6のStringによるものが見えます.substring()が返すStringは元のStringの参照を保存するため、元のStringは解放されず、予想外の大量のメモリ消費を招く.
JDK 6のこのような設計の目的も実はメモリを節約するためで、これらのStringはすべて元のStringを多重化したため、intタイプのofferset、count等値だけでsubstring後の新しいStringを識別します.
しかし、上記の例では、巨大なStringから少数のStringを切り取ることが後に用いられ、このような設計は冗長データを大量にもたらす.従ってStringを通過することについて.split()またはString.substring()Stringの操作を切り取る結論は以下の通りである.
•大きなテキストから少量の文字列を切り取るアプリケーションに対してString.substring()は、メモリの浪費を引き起こします.•通常のテキストから一定数切り取る文字列については、切り取る文字列の長いの総和が元のテキストの長さと大きく異なる、既存のString.substring()設計は、メモリを節約するために元のテキストを共有するのに適しています.
大量のメモリを消費する原因はStringである以上.substring()は、結果に大量の元のStringが含まれているので、メモリの浪費を減らす方法は、これらの元のStringを除去することです.新たにnewStringを呼び出すように、切り出した文字列のみを含むStringを構築する、Stringを呼び出すことができる.toCharArray()メソッド:
String newString = new String(smallString.toCharArray());
2、同じように、splitの方法を見てみましょう
1 public class TestBigString
2
3 {
4
5 private String strsub;
6
7 private String strempty = new String();
8
9 private String[] strSplit;
10
11 public static void main(String[] args) throws Exception
12
13 {
14
15 TestBigString obj = new TestBigString();
16
17 obj.strsub = obj.readString().substring(0,1);
18
19 obj.strSplit = obj.readString().split("Address:",5);
20
21 Thread.sleep(30*60*1000);
22
23 }
JDK 6で分割された文字列配列では、String要素ごとに使用されるメモリは元の文字列のメモリサイズ(67 M):
JDK 7で分割された文字列配列では、各String要素は実際のメモリサイズです.
理由:
JDK 6ソースコード:
1 public String[] split(String regex, int limit) {
2
3 return Pattern.compile(regex).split(this, limit);
4
5 }
6
7 public String[] split(CharSequence input, int limit) {
8
9 int index = 0;
10
11 boolean matchLimited = limit > 0;
12
13 ArrayList<String> matchList = new ArrayList<String>();
14
15 Matcher m = matcher(input);
16
17 // Add segments before each match found
18
19 while(m.find()) {
20
21 if (!matchLimited || matchList.size() < limit - 1) {
22
23 String match = input.subSequence(index, m.start()).toString();
24
25 matchList.add(match);
26
27 public CharSequence subSequence(int beginIndex, int endIndex) {
28
29 return this.substring(beginIndex, endIndex);
30
31 }
三、その他の方面:
1、String a1 = “Hello”; // ,JVM intern 。
JVM : , ,
; , 。
String a2 = new String(“Hello”); //
2、静的文字列をつなぎ合わせるときは、通常コンパイラが最適化するため、できるだけ+を使用します.
1 public String constractStr()
2
3 {
4
5 return "str1" + "str2" + "str3";
6
7 }
対応するバイトコード:
Code:
0: ldc #24;//String str 1 str 2 str 3--文字列定数をスタックトップに押し込む
2: areturn
3、動的文字列をつなぎ合わせる時、できるだけStringBufferあるいはStringBuilderのappendを使って、このように構造の多すぎる臨時Stringオブジェクトを減らすことができます(javacコンパイラはString接続に対して自動的に最適化します):
1 public String constractStr(String str1, String str2, String str3)
2
3 {
4
5 return str1 + str2 + str3;
6
7 }
対応バイトコード(JDK 1.5以降、StringBuilder.appendメソッドを呼び出すように変換):
Code:
0: new #24; //class java/lang/StringBuilder
3: dup
4: aload_1
5: invokestatic #26; //Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
8: invokespecial #32; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
11: aload_2
12: invokevirtual #35; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: aload_3
16: invokevirtual #35; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; ―― StringBuilder append
19: invokevirtual #39; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: areturn ――