[Java]String記憶體陷阱簡介
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;
JDK6的空字串所佔的空間為40位元組
JDK7:
private final char value[];
private int hash;
private transient int hash32;
JDK7的空字串所佔的空間也是40位元組
JDK6字串記憶體佔用的計算方式:
首先計算一個空的 char 陣列所佔空間,在 Java 裡陣列也是物件,因而陣列也有物件頭,故一個數組所佔的空間為物件頭所佔的空間加上陣列長度,即 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(new File(“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個字元,檔案大小有35M。
用JDK6來執行上面的程式碼,可以看到strsub只是substring(0,1)只取一個,count確實只有1,但其佔用的記憶體卻高達接近67M。
然而用JDK7運行同樣的上面的程式碼,strsub物件卻只有40位元組
什麼原因呢?
來看下JDK的原始碼:
JDK6:
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > count) {
throw new StringIndexOutOfBoundsException(endIndex);
}
if (beginIndex > endIndex) {
throw new StringIndexOutOfBoundsException(endIndex – beginIndex);
}
return ((beginIndex == 0) && (endIndex == count)) ? this :
new String(offset + beginIndex, endIndex – beginIndex, value);
}
// Package private constructor which shares value array for speed.
String(int offset, int count, char value[]) {
this.value = value;
this.offset = offset;
this.count = count;
}
JDK7:
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex – beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length – count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
可以看到原來是因為JDK6的String.substring()所返回的 String 仍然會儲存原始 String的引用,所以原始String無法被釋放掉,因而導致了出乎意料的大量的記憶體消耗。
JDK6這樣設計的目的其實也是為了節約記憶體,因為這些 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方法
public class TestBigString
{
private String strsub;
private String strempty = new String();
private String[] strSplit;
public static void main(String[] args) throws Exception
{
TestBigString obj = new TestBigString();
obj.strsub = obj.readString().substring(0,1);
obj.strSplit = obj.readString().split(“Address:”,5);
Thread.sleep(30*60*1000);
}
JDK6中分割的字串陣列中,每個String元素佔用的記憶體都是原始字串的記憶體大小(67M):
而JDK7中分割的字串陣列中,每個String元素都是實際的記憶體大小:
原因:
JDK6原始碼:
public String[] split(String regex, int limit) {
return Pattern.compile(regex).split(this, limit);
}
public String[] split(CharSequence input, int limit) {
int index = 0;
boolean matchLimited = limit > 0;
ArrayList<String> matchList = new ArrayList<String>();
Matcher m = matcher(input);
// Add segments before each match found
while(m.find()) {
if (!matchLimited || matchList.size() < limit – 1) {
String match = input.subSequence(index, m.start()).toString();
matchList.add(match);
public CharSequence subSequence(int beginIndex, int endIndex) {
return this.substring(beginIndex, endIndex);
}
三、其他方面:
1、String a1 = “Hello”; //常量字串,JVM預設都已經intern到常量池了。 建立字串時 JVM 會檢視內部的快取池是否已有相同的字串存在:如果有,則不再使用建構函式構造一個新的字串, 直接返回已有的字串例項;若不存在,則分配新的記憶體給新建立的字串。 String a2 = new String(“Hello”); //每次都建立全新的字串
2、在拼接靜態字串時,儘量用 +,因為通常編譯器會對此做優化。
public String constractStr()
{
return “str1” + “str2” + “str3”;
}
對應的位元組碼:
Code:
0: ldc #24; //String str1str2str3 –將字串常量壓入棧頂
2: areturn
3、在拼接動態字串時,儘量用 StringBuffer 或 StringBuilder的 append,這樣可以減少構造過多的臨時 String 物件(javac編譯器會對String連線做自動優化):
public String constractStr(String str1, String str2, String str3)
{
return str1 + str2 + str3;
}
對應位元組碼(JDK1.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/Objec
t;)Ljava/lang/String;
8: invokespecial #32; //Method java/lang/StringBuilder.”<init>”:(Ljava/la
ng/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/l
ang/String;
22: areturn ――返回引用