String 在 JVM 的儲存結構
一般而言,Java 對象在虛擬機器的結構如下:
對象頭(object header):8 個位元組
Java 原始類型資料:如 int, float, char 等類型的資料,各類型資料占記憶體如 表 1. Java 各資料類型所佔記憶體.
引用(reference):4 個位元組
填充符(padding)
表 1. Java 各資料類型所佔記憶體
然而,一個 Java 對象實際還會佔用些額外的空間,如:對象的 class 資訊、ID、在虛擬機器中的狀態。在 Oracle JDK 的 Hotspot 虛擬機器中,一個普通的對象需要額外 8 個位元組。
如果對於 String(JDK 6)的成員變數聲明如下:
private final char value[]; private final int offset; private final int count; private int hash;
那麼因該如何計算該 String 所佔的空間?
首先計算一個空的 char 數組所佔空間,在 Java 裡數組也是對象,因而數組也有對象頭,故一個數組所佔的空間為對象頭所佔的空間加上數組長度,即 8 + 4 = 12 位元組 , 經過填充後為 16 位元組。
那麼一個空 String 所佔空間為:
對象頭(8 位元組)+ char 數組(16 位元組)+ 3 個 int(3 × 4 = 12 位元組)+1 個 char 數組的引用 (4 位元組 ) = 40 位元組。
因此一個實際的 String 所佔空間的計算公式如下:
8*( ( 8+2*n+4+12)+7 ) / 8 = 8*(int) ( ( ( (n) *2 )+43) /8 )
其中,n 為字串長度。
案例分析
在我們的大規模文本分析的案例中,程式需要統計一個 300MB 的 csv 檔案所有單詞的出現次數,分析發現共有 20,000 左右的唯一單詞,假設每個單詞平均包含 15 個字母,這樣根據上述公式,一個單詞平均佔用 75 bytes. 那麼這樣 75 * 20,000 = 1500000,即約為 1.5M 左右。但實際發現有上百兆的空間被佔用。 實際使用的記憶體之所以與預估的產生如此大的差異是因為程式大量使用 String.split() 或 String.substring()來擷取單詞。在 JDK 1.6 中 String.substring(int, int)的源碼為:
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); }
調用的 String 建構函式源碼為:
String(int offset, int count, char value[]) { this.value = value; this.offset = offset; this.count = count; }