字符串性能優(yōu)化
String對象是我們使用最頻繁的一個對象類型,但它的性能問題卻是最容易被忽略的。String對象作為Java語言中重要的數(shù)據(jù)類型,可以說是在內(nèi)存中占據(jù)空間最大的一個對象。高效地使用字符串,可以提升系統(tǒng)的整體性能。
我們從String對象的實現(xiàn)、特性以及實際使用中的優(yōu)化這三個方面入手,深入了解。
先看如下代碼,創(chuàng)建3個對象,依次兩兩匹配,每組的結(jié)果是否相等?
String str1 = "abc";
String str2 = new String("abc");
String str3 = str2.intern();
asserSame(str1 == str2);
asserSame(str2 == str3);
asserSame(str1 == str3);
String對象是如何實現(xiàn)的?
在Java語言中,對String對象做了大量優(yōu)化,來節(jié)約內(nèi)存空間,提升String對象在系統(tǒng)中的性能,優(yōu)化過程如圖所示:

- 在Java6及之前版本中,String對象是對char數(shù)組進行封裝實現(xiàn)的對象,主要有四個成員變量:char數(shù)組、偏移量offset、字符數(shù)量count、哈希值hash。String對象通過offset和count 兩個屬性來定位char[]數(shù)組,獲取字符串。這樣做可以高效、快速地共享數(shù)組對象,同時節(jié)省內(nèi)存空間,但這種方式很有可能會導(dǎo)致內(nèi)存泄漏。
- Java7版本和Java8版本,Java對String類做了一些改變。String類中不再有offset和count兩個變量了。這樣做的好處是String對象占用的內(nèi)存減少了,同時String.substring()方法不再共享原對象的char[],從而解決了使用該方法可能導(dǎo)致的內(nèi)存泄露問題。
- 從Java9開始,Java將char[]改為了byte[]字段,維護了新的屬性coder,它是一個編碼格式的標識。我們知道一個char字符占16位,2個字節(jié)。這個情況下,存儲單字節(jié)編碼內(nèi)的字符就顯得非常浪費。JDK9的String類為了節(jié)約內(nèi)存空間,使用了占8位,一個字節(jié)的byte數(shù)組來存放字符串。新屬性coder的作用是,在計算字符串長度或者使用indexOf()函數(shù)時,需要根據(jù)這個字段,判斷如何計算字符串長度。coder屬性默認0或1,0代表Latin-1單字節(jié),1代表UTF-16。如果String判斷字符串只含有Latin-1則coder等于0否則等于1。
String對象的不可變性
觀察源碼可以知道,String類是被final關(guān)鍵字修飾的,并且變量char[]也被final修飾了。我們知道被final修飾的類不可繼承,變量被final+private修飾就不可更改。Java實現(xiàn)的這個特性叫做String對象的不可變性,即String對象一旦創(chuàng)建成功,就不能修改。這樣做有什么好處呢?
- 保證了String對象的安全性。保證不會被惡意修改。
- 保證hash屬性值不會頻繁變更,使得類似HashMap容器能實現(xiàn)key-value緩存。
- 能夠?qū)崿F(xiàn)字符串常量池,當(dāng)代碼使用String str = "abc"; 創(chuàng)建對象時,JVM首先會檢查該對象是否在字符串常量池中,如果在,則返回其引用,否則在常量池中被創(chuàng)建,這種實現(xiàn)可以減少相同對象的重復(fù)創(chuàng)建,節(jié)約內(nèi)存。當(dāng)使用String str = new String("abc");創(chuàng)建對象時,首先在編譯類文件時,會將"abc"常量放到常量結(jié)構(gòu)中,在類加載時,"abc"會在常量池中創(chuàng)建;其次,在調(diào)用new時,JVM將會調(diào)用String的構(gòu)造函數(shù),同時引用常量池的"abc",在堆中創(chuàng)建一個String對象;最后str會引用String對象。
而我們平時的使用中會發(fā)現(xiàn),String str="abc";str="bcd";這樣的語句,這里str是可變的。其實,這里的str只是對String對象的引用,原來的對象仍舊存在于內(nèi)存中。
String對象的優(yōu)化
接下來我們根據(jù)String對象的特性,看看如何優(yōu)化String對象,優(yōu)化的過程中有什么需要注意的地方。
-
構(gòu)建超大字符串
String str = "a"+"b"+"c";對于上面的代碼,我們知道,JVM首先會生成a、b、c三個對象,最后生成abc對象,理論上來講這樣的代碼效率會很低。但在實際運行中,我們就會發(fā)現(xiàn),編譯器自動將這條語句優(yōu)化為
String str = "abc";上述代碼是字符串常量的累加,那么對于字符串變量,編譯器是否會進行同樣的優(yōu)化呢?
對于String str="abc"; for(int i=0;i<100;i++){ str+=i; }這段代碼,編譯器同樣會進行優(yōu)化,優(yōu)化的結(jié)果是這樣的
String str="abc"; for(int i=0;i<100;i++){ str=(new StringBuilder(String.valueOf(str))).append(i).toString(); }綜上,即使使用+進行字符串拼接,也同樣會被編譯器優(yōu)化為StringBuilder方式,但我們發(fā)現(xiàn),編譯器的優(yōu)化,每次循環(huán)就會創(chuàng)建一個新的StringBuilder對象,同樣會降低系統(tǒng)性能。所以,平時進行字符串拼接的時候,建議顯式使用StringBuilder提升系統(tǒng)性能。在多線程編程中String對象的拼接涉及到線程安全,我們可以使用StringBuffer,但是StringBuffer涉及到鎖競爭,所以從性能上來說,要比StringBuilder差一些。
-
使用String.intern節(jié)省內(nèi)存,每次賦值時使用String的intern方法,可以大幅度降低重復(fù)信息的內(nèi)存占用率。調(diào)用intern方法,JVM回去檢查字符串常量池中是否有等于該對象的字符串的引用,如果沒有,在JDK1.6中會復(fù)制堆內(nèi)存中的字符串到常量池中,并返回引用,堆內(nèi)存中的字符串會通過垃圾回收器回收。在JDK1.7后,常量池合并到堆中,不需要再復(fù)制字符串,只會把首次遇到的字符串的引用添加到常量池中;如果有,就返回引用。
image.png使用intern方法需要注意,一定要結(jié)合場景。常量池是類似HashTable的實現(xiàn),存儲的數(shù)據(jù)越大,遍歷的時間復(fù)雜度越大,數(shù)據(jù)如果過大,會增大字符串常量池的負擔(dān)。
謹慎選擇字符串分割,Split()作為分割字符串的方法,其內(nèi)部是使用正則表達式來實現(xiàn)的,會出現(xiàn)回溯的風(fēng)險,建議使用indexOf方法代替Split方法完成分割。如果一定要使用Split方法,就需要對回溯問題加以重視。
