天天看點

Java 性能優化之 String 篇Java 性能優化之 String 篇

原文:

string 方法用于文本分析及大量字元串處理時會對記憶體性能造成不可低估的影響。我們在一個大文本資料分析的項目中(我們統計一個約 300mb 的 csv

檔案中所有單詞出現的次數)發現,用于存放結果的 collection 占用了幾百兆的記憶體,遠遠超出唯一單詞總數 20000 個。 本文将通過分析 string

在 jvm 中的存儲結構,以及常見 string 操作對記憶體的影響闡述問題産生的原因及解決。.

13

Java 性能優化之 String 篇Java 性能優化之 String 篇

, 軟體工程師, ibm

, 技術文檔工程師, ibm

2012 年 5 月 14 日

内容

一般而言,java 對象在虛拟機的結構如下:

對象頭(object header):8 個位元組

java 原始類型資料:如 int, float, char 等類型的資料,各類型資料占記憶體如 .

引用(reference):4 個位元組

填充符(padding)

資料類型

占用記憶體(位元組數)

boolean

1

byte

char

2

short

int

4

float

long

8

double

然而,一個 java 對象實際還會占用些額外的空間,如:對象的 class 資訊、id、在虛拟機中的狀态。在 oracle jdk 的 hotspot

虛拟機中,一個普通的對象需要額外 8 個位元組。

如果對于 string(jdk 6)的成員變量聲明如下:

那麼因該如何計算該 string 所占的空間?

首先計算一個空的 char 數組所占空間,在 java 裡數組也是對象,因而數組也有對象頭,故一個數組所占的空間為對象頭所占的空間加上數組長度,即 8 + 4

= 12 位元組 , 經過填充後為 16 位元組。

那麼一個空 string 所占空間為:

對象頭(8 位元組)+ char 數組(16 位元組)+ 3 個 int(3 × 4 = 12 位元組)+1 個 char 數組的引用 (4 位元組 ) = 40

位元組。

是以一個實際的 string 所占空間的計算公式如下:

其中,n 為字元串長度。

在我們的大規模文本分析的案例中,程式需要統計一個 300mb 的 csv 檔案所有單詞的出現次數,分析發現共有 20,000

左右的唯一單詞,假設每個單詞平均包含 15 個字母,這樣根據上述公式,一個單詞平均占用 75 bytes. 那麼這樣 75 * 20,000 =

1500000,即約為 1.5m 左右。但實際發現有上百兆的空間被占用。 實際使用的記憶體之是以與預估的産生如此大的差異是因為程式大量使用 <code>string.split()</code> 或<code>string.substring()</code>來擷取單詞。在

jdk 1.6 中 <code>string.substring(int, int)</code>的源碼為:

調用的 string 構造函數源碼為:

仔細觀察粗體這行代碼我們發現 <code>string.substring()</code>所傳回的

string 仍然會儲存原始 string, 這就是 20,000 個平均長度的單詞竟然占用了上百兆的記憶體的原因。 一個 csv

檔案中每一行都是一份很長的資料,包含了上千的單詞,最後被 <code>string.split()</code> 或 <code>string.substring()</code>截取出的每一個單詞仍舊包含了其原先所在的上下文中,因而導緻了出乎意料的大量的記憶體消耗。

當然,jdk string 的源碼設計當然有着其合理之處,對于通過 <code>string.split()</code>或 <code>string.substring()</code>截取出大量

string 的操作,這種設計在很多時候可以很大程度的節省記憶體,因為這些 string 都複用了原始 string,隻是通過 int 類型的 start,

end 等值來辨別每一個 string。 而對于我們的案例,從一個巨大的 string 截取少數 string 為以後所用,這樣的設計則造成大量備援資料。

是以有關通過 <code>string.split()</code>或<code>string.substring()</code>截取

string 的操作的結論如下:

對于從大文本中截取少量字元串的應用,<code>string.substring()</code>将會導緻記憶體的過度浪費。

對于從一般文本中截取一定數量的字元串,截取的字元串長度總和與原始文本長度相差不大,現有的 <code>string.substring()</code>設計恰好可以共享原始文本進而達到節省記憶體的目的。

既然導緻大量記憶體占用的根源是 <code>string.substring()</code>傳回結果中包含大量原始

string,那麼一個顯而易見的減少記憶體浪費的的途徑就是去除這些原始 string。辦法有很多種,在此我們采取比較直覺的一種,即再次調用 <code>newstring</code>構造一個的僅包含截取出的字元串的

string,我們可調用<code>string.</code><code>tochararray</code><code>()</code>方法:

舉一個極端例子,假設要從一個字元串中擷取所有連續的非空子串,字元串長度為 n,如果用 jdk 本身提供的 <code>string.substring() 方</code>法,則總共的連續非空子串個數為:

由于每個子串所占的空間為常數,故空間複雜度也為 o(n2)。

如果用本文建議的方法,即構造一個内容相同的新的字元串,則所需空間正比于子串的長度,則所需空間複雜度為:

是以,從以上定量的分析看來,當需要截取的字元串長度總和大于等于原始文本長度,本文所建議的方法帶來的空間複雜度反而高了,而現有的 <code>string.substring()</code>設計恰好可以共享原始文本進而達到節省記憶體的目的。反之,當所需要截取的字元串長度總和遠小于原始文本長度時,用本文所推薦的方法将在很大程度上節省記憶體,在大文本資料進行中其優勢顯而易見。

以上我們描述了在我們的大量文本分析案例中調用 string 的 <code>substring</code><code>方法</code>導緻記憶體消耗的問題,下面再列舉一些其他将導緻記憶體浪費的

string 的 api 的使用:

在拼接靜态字元串時,盡量用 +,因為通常編譯器會對此做優化,如:

編譯器會把它視為:

在拼接動态字元串時,盡量用 <code>stringbuffer</code> 或 <code>stringbuilder</code>的 <code>append</code>,這樣可以減少構造過多的臨時

string 對象。

常見的建立一個 string 可以用指派操作符"=" 或用 new 和相應的構造函數。初學者一定會想這兩種有何差別,舉例如下:

第一種方法建立字元串時 jvm

會檢視内部的緩存池是否已有相同的字元串存在:如果有,則不再使用構造函數構造一個新的字元串,直接傳回已有的字元串執行個體;若不存在,則配置設定新的記憶體給新建立的字元串。

第二種方法直接調用構造函數來建立字元串,如果所建立的字元串在字元串緩存池中不存在則調用構造函數建立全新的字元串,如果所建立的字元串在字元串緩存池中已有則再拷貝一份到

java 堆中。

盡管這是一個簡單明顯的例子,然而在實際項目中程式設計者卻不那麼容易洞察因為這兩種方式的選擇而帶來的性能問題。

仍然以之前的從 csv 檔案中截取 string 為例,先前我們通過用 new string() 去除傳回的 string 中附帶的原始 string

的方法優化了 <code>substring</code>導緻的記憶體消耗問題。然而,當我們下意識地使用 <code>newstring</code>去構造一個全新的字元串而不是用指派符來建立(重用)一個字元串時,就導緻了另一個潛在的性能問題,即:重複建立大量相同的字元串。說到這裡,您也許會想到使用緩存池的技術來解決這一問題,大概有如下兩種方法:

方法一,使用 string 的 <code>intern()</code>方法傳回

jvm 對字元串緩存池裡相應已存在的字元串引用,進而解決記憶體性能問題,但這個方法并不推薦!原因在于:首先,<code>intern()</code> 所使用的池會是

jvm 中一個全局的池,很多情況下我們的程式并不需要如此大作用域的緩存;其次,intern() 所使用的是 jvm heap 中 permgen 相應的區域,在

jvm 中 permgen 是用來存放裝載類和建立類執行個體時用到的中繼資料。程式運作時所使用的記憶體絕大部分存放在 jvm heap

的其他區域,過多得使用 <code>intern()</code>将導緻

permgen 過度增長而最後傳回 <code>outofmemoryerror</code>,因為垃圾收集器不會對被緩存的

string 做垃圾回收。是以我們建議使用第二種方式。

方法二,使用者自己建構緩存,這種方式的優點是更加靈活。建立 hashmap,将需緩存的 string 作為 key 和 value 存放入

hashmap。假設我們準備建立的字元串為 key,将 map cachemap 作為緩沖池,那麼傳回 key 的代碼如下:

本文通過一個實際項目中遇到的因使用 string 而導緻的性能問題講述了 string 在 jvm 中的存儲結構,string 的 api

使用可能造成的性能問題以及解決方法。相信這些建議能對處理大文本分析的朋友有所幫助,同時希望文中提到的某些優化方法能被舉一反三的應用在其他有關 string

的性能優化的場合。

:文章主要闡述了 java 中字元串所占用的記憶體空間已經不合理使用某些字元串操作方法導緻的記憶體增長隐患。

:書中詳細探讨了 java 對象在 jvm 中的生命周期及垃圾回收的原理,閱讀此書可以使程式員在寫代碼時更注重性能問題。

:這裡有數百篇關于 java 程式設計各個方面的文章。

加入 。檢視開發人員推動的部落格、論壇、組和維基,并與其他

developerworks 使用者交流。