java String 最長長度和佔用內存大小

一 序


String在內存中的最大長度理論上是int型變量的最大值,Integer.MAX_VALUE,
String的字面常量的最大長度爲CONSTANT_Utf8_info表決定,一般爲65535.

二 介紹


1、String的內部實現
通過一個字符數組來維護字符序列,其聲名如下:

private final char value[];
2
所以,String的最大長度取決於字符數組的最大長度,因爲字符數組長度只能是byte,char,short,int而不能是long型,所以這也說明最大長度,另一方面,我們知道String類有一個方法,str.length() 它的返回值是int型變量,聲明如下:

public int length()
3
所以這也說明了最大長度的理論值,但在實際中,要比理論值小,

public class mainClass {    public static void main(String[] args) {
        // TODO Auto-generated method stub
        char[] value=new char[Integer.MAX_VALUE];   
        System.out.println("");
    }

}

這個錯誤是內存溢出錯誤,所以系統無法分配這麼大的內存空間。
現在的問題是,計算機系統可以分配多大的內存呢?
 

三 分析源碼

 

java.lang.String.java

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
java String類以char[]數組存儲字符元素,因而,String類的最大長度實際上取決於char[]數組能夠包含的數組長度。
我們可以簡單做下試驗,看看char[]數組的最大長度MAX_LENGTH是多少。
當我們將len值調到320339961的時候,系統剛剛好報錯,


因此,char[]數組的最大長度可以達到 320339960,約爲2^28.255,每個字符佔用空間1個字節,也就是2^28.255字節,而4個G等於2^30字節。

因而char[]數組的最大長度約等於(不到)4個G。

String類型的長度爲320339960,其最大容量不超過4個G。

 

 

String內部是以char數組的形式存儲,數組的長度是int類型,那麼String允許的最大長度就是Integer.MAX_VALUE了。又由於java中的字符是以16位存儲的,因此大概需要4GB的內存才能存儲最大長度的字符串。不過這僅僅是對字符串變量而言,如果是字符串字面量(string literals),如“abc"、"1a2b"之類寫在代碼中的字符串literals,那麼允許的最大長度取決於字符串在常量池中的存儲大小,也就是字符串在class格式文件中的存儲格式:

CONSTANT_Utf8_info {
        u1 tag;
        u2 length;
        u1 bytes[length];
}


    u2是無符號的16位整數,因此理論上允許的string literal的最大長度是2^16-1=65535。然而實際測試表明,允許的最大長度僅爲65534

四 不同運行階段分析

 

編譯期

首先,我們先來合理的推斷一下,當我們在代碼中使用String s = “”;的形式來定義String對象的時候,""中字符的個數有沒有限制呢?
既然是合理的推斷,那就要要足夠的依據,所以我們可以從String的源碼入手,根據public String(char value[], int offset, int count)的定義,count是int類型的,所以,char value[]中最多可以保存Integer.MAX_VALUE個,即2147483647字符。(jdk1.8.0_73)
但是,實驗證明,String s = “”;中,最多可以有65534個字符。如果超過這個個數。就會在編譯期報錯。

public static void main(String[] args) {

    String s = "a...a";// 共65534個a
    System.out.println(s.length());

    String s1 = "a...a";// 共65535個a
    System.out.println(s1.length());
}

 

以上代碼,會在String s1 = “a…a”;// 共65535個a處編譯失敗:

✗ javac StringLenghDemo.java
StringLenghDemo.java:11: 錯誤: 常量字符串過長

 

明明說好的長度限制是2147483647,爲什麼65535個字符就無法編譯了呢?

當我們使用字符串字面量直接定義String的時候,是會把字符串在常量池中存儲一份的。那麼上面提到的65534其實是常量池的限制。
常量池中的每一種數據項也有自己的類型。Java中的UTF-8編碼的Unicode字符串在常量池中以CONSTANT_Utf8類型表示。
CONSTANTUtf8info是一個CONSTANTUtf8類型的常量池數據項,它存儲的是一個常量字符串。常量池中的所有字面量幾乎都是通過CONSTANTUtf8info描述的。CONSTANTUtf8_info的定義如下:

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

 

由於本文的重點並不是CONSTANTUtf8info的介紹,這裏就不詳細展開了,我們只需要我們使用字面量定義的字符串在class文件中,是使用CONSTANTUtf8info存儲的,而CONSTANTUtf8info中有u2 length;表明了該類型存儲數據的長度。
u2是無符號的16位整數,因此理論上允許的的最大長度是2^16=65536。而 java class 文件是使用一種變體UTF-8格式來存放字符的,null 值使用兩個 字節來表示,因此只剩下 65536- 2 = 65534個字節。
關於這一點,在the class file format spec中也有明確說明:

The length of field and method names, field and method descriptors, and other constant string values is limited to 65535 characters by the 16-bit unsigned length item of the CONSTANTUtf8info structure (§4.4.7). Note that the limit is on the number of bytes in the encoding and not on the number of encoded characters. UTF-8 encodes some characters using two or three bytes. Thus, strings incorporating multibyte characters are further constrained.

也就是說,在Java中,所有需要保存在常量池中的數據,長度最大不能超過65535,這當然也包括字符串的定義。

運行期

上面提到的這種String長度的限制是編譯期的限制,也就是使用String s= “”;這種字面值方式定義的時候纔會有的限制。
那麼。String在運行期有沒有限制呢,答案是有的,就是我們前文提到的那個Integer.MAX_VALUE ,這個值約等於4G,在運行期,如果String的長度超過這個範圍,就可能會拋出異常。(在jdk 1.9之前)
int 是一個 32 位變量類型,取正數部分來算的話,他們最長可以有

2^31-1 =2147483647 個 16-bit Unicodecharacter

2147483647 * 16 = 34359738352 位
34359738352 / 8 = 4294967294 (Byte)
4294967294 / 1024 = 4194303.998046875 (KB)
4194303.998046875 / 1024 = 4095.9999980926513671875 (MB)
4095.9999980926513671875 / 1024 = 3.99999999813735485076904296875 (GB)

五 佔用內存大小 

1、先介紹一下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 爲字符串長度。

2、舉個例子:

A,substringA

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字節

 

B,什麼原因呢?

來看下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());

C、同樣,再看看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元素都是實際的內存大小:

 

D,原因:

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);

    }

4、其他方面:

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

 

備註 

自動裝箱拆箱

https://www.cnblogs.com/wang-yaz/p/8516151.html

https://blog.csdn.net/wolfking0608/article/details/78583944

https://www.iteye.com/blog/lin-yp-168367

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章