1. 程式人生 > >[Java]String記憶體陷阱簡介

[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     ――返回引用