字符串性能優(yōu)化
String對象是我們使用最頻繁的一個對象類型磕道,但它的性能問題卻是最容易被忽略的。String對象作為Java語言中重要的數(shù)據(jù)類型讳侨,可以說是在內(nèi)存中占據(jù)空間最大的一個對象呵萨。高效地使用字符串,可以提升系統(tǒng)的整體性能跨跨。
我們從String對象的實(shí)現(xiàn)潮峦、特性以及實(shí)際使用中的優(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對象是如何實(shí)現(xiàn)的?
在Java語言中萨螺,對String對象做了大量優(yōu)化窄做,來節(jié)約內(nèi)存空間愧驱,提升String對象在系統(tǒng)中的性能慰技,優(yōu)化過程如圖所示:
- 在Java6及之前版本中,String對象是對char數(shù)組進(jìn)行封裝實(shí)現(xiàn)的對象组砚,主要有四個成員變量:char數(shù)組吻商、偏移量offset、字符數(shù)量count糟红、哈希值hash艾帐。String對象通過offset和count 兩個屬性來定位char[]數(shù)組,獲取字符串盆偿。這樣做可以高效柒爸、快速地共享數(shù)組對象,同時(shí)節(jié)省內(nèi)存空間事扭,但這種方式很有可能會導(dǎo)致內(nèi)存泄漏捎稚。
- Java7版本和Java8版本,Java對String類做了一些改變求橄。String類中不再有offset和count兩個變量了今野。這樣做的好處是String對象占用的內(nèi)存減少了,同時(shí)String.substring()方法不再共享原對象的char[]罐农,從而解決了使用該方法可能導(dǎo)致的內(nèi)存泄露問題条霜。
- 從Java9開始,Java將char[]改為了byte[]字段涵亏,維護(hù)了新的屬性coder宰睡,它是一個編碼格式的標(biāo)識蒲凶。我們知道一個char字符占16位,2個字節(jié)夹厌。這個情況下豹爹,存儲單字節(jié)編碼內(nèi)的字符就顯得非常浪費(fèi)。JDK9的String類為了節(jié)約內(nèi)存空間矛纹,使用了占8位臂聋,一個字節(jié)的byte數(shù)組來存放字符串。新屬性coder的作用是或南,在計(jì)算字符串長度或者使用indexOf()函數(shù)時(shí)孩等,需要根據(jù)這個字段,判斷如何計(jì)算字符串長度采够。coder屬性默認(rèn)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實(shí)現(xiàn)的這個特性叫做String對象的不可變性步清,即String對象一旦創(chuàng)建成功,就不能修改虏肾。這樣做有什么好處呢廓啊?
- 保證了String對象的安全性。保證不會被惡意修改封豪。
- 保證hash屬性值不會頻繁變更谴轮,使得類似HashMap容器能實(shí)現(xiàn)key-value緩存。
- 能夠?qū)崿F(xiàn)字符串常量池吹埠,當(dāng)代碼使用String str = "abc"; 創(chuàng)建對象時(shí)第步,JVM首先會檢查該對象是否在字符串常量池中,如果在藻雌,則返回其引用雌续,否則在常量池中被創(chuàng)建,這種實(shí)現(xiàn)可以減少相同對象的重復(fù)創(chuàng)建胯杭,節(jié)約內(nèi)存驯杜。當(dāng)使用String str = new String("abc");創(chuàng)建對象時(shí),首先在編譯類文件時(shí)做个,會將"abc"常量放到常量結(jié)構(gòu)中鸽心,在類加載時(shí)滚局,"abc"會在常量池中創(chuàng)建;其次顽频,在調(diào)用new時(shí)藤肢,JVM將會調(diào)用String的構(gòu)造函數(shù),同時(shí)引用常量池的"abc"糯景,在堆中創(chuàng)建一個String對象嘁圈;最后str會引用String對象。
而我們平時(shí)的使用中會發(fā)現(xiàn)蟀淮,String str="abc";str="bcd";這樣的語句最住,這里str是可變的。其實(shí)怠惶,這里的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對象拄丰,理論上來講這樣的代碼效率會很低府树。但在實(shí)際運(yùn)行中,我們就會發(fā)現(xiàn)料按,編譯器自動將這條語句優(yōu)化為
String str = "abc";
上述代碼是字符串常量的累加奄侠,那么對于字符串變量,編譯器是否會進(jìn)行同樣的優(yōu)化呢载矿?
對于String str="abc"; for(int i=0;i<100;i++){ str+=i; }
這段代碼垄潮,編譯器同樣會進(jìn)行優(yōu)化税弃,優(yōu)化的結(jié)果是這樣的
String str="abc"; for(int i=0;i<100;i++){ str=(new StringBuilder(String.valueOf(str))).append(i).toString(); }
綜上怕磨,即使使用+進(jìn)行字符串拼接,也同樣會被編譯器優(yōu)化為StringBuilder方式赁严,但我們發(fā)現(xiàn)逢勾,編譯器的優(yōu)化牡整,每次循環(huán)就會創(chuàng)建一個新的StringBuilder對象,同樣會降低系統(tǒng)性能溺拱。所以逃贝,平時(shí)進(jìn)行字符串拼接的時(shí)候谣辞,建議顯式使用StringBuilder提升系統(tǒng)性能。在多線程編程中String對象的拼接涉及到線程安全沐扳,我們可以使用StringBuffer泥从,但是StringBuffer涉及到鎖競爭,所以從性能上來說沪摄,要比StringBuilder差一些躯嫉。
-
使用String.intern節(jié)省內(nèi)存,每次賦值時(shí)使用String的intern方法杨拐,可以大幅度降低重復(fù)信息的內(nèi)存占用率和敬。調(diào)用intern方法,JVM回去檢查字符串常量池中是否有等于該對象的字符串的引用戏阅,如果沒有昼弟,在JDK1.6中會復(fù)制堆內(nèi)存中的字符串到常量池中,并返回引用奕筐,堆內(nèi)存中的字符串會通過垃圾回收器回收舱痘。在JDK1.7后,常量池合并到堆中离赫,不需要再復(fù)制字符串芭逝,只會把首次遇到的字符串的引用添加到常量池中;如果有渊胸,就返回引用旬盯。
image.png使用intern方法需要注意,一定要結(jié)合場景翎猛。常量池是類似HashTable的實(shí)現(xiàn)胖翰,存儲的數(shù)據(jù)越大,遍歷的時(shí)間復(fù)雜度越大切厘,數(shù)據(jù)如果過大萨咳,會增大字符串常量池的負(fù)擔(dān)。
謹(jǐn)慎選擇字符串分割疫稿,Split()作為分割字符串的方法培他,其內(nèi)部是使用正則表達(dá)式來實(shí)現(xiàn)的,會出現(xiàn)回溯的風(fēng)險(xiǎn)遗座,建議使用indexOf方法代替Split方法完成分割舀凛。如果一定要使用Split方法,就需要對回溯問題加以重視途蒋。