String 對(duì)象是我們使用最頻繁的一個(gè)對(duì)象類型,但它的性能問題卻是最容易被忽略的。String 對(duì)象作為 Java 語言中重要的數(shù)據(jù)類型,是內(nèi)存中占據(jù)空間最大的一個(gè)對(duì)象。高效地使用字符串,可以提升系統(tǒng)的整體性能。
接下來我們就從 String 對(duì)象的實(shí)現(xiàn)、特性以及實(shí)際使用中的優(yōu)化這三個(gè)方面入手,深入了解。
在開始之前,我想先問你一個(gè)小問題,也是我在招聘時(shí),經(jīng)常會(huì)問到面試者的一道題。雖是老生常談了,但錯(cuò)誤率依然很高,當(dāng)然也有一些面試者答對(duì)了,但能解釋清楚答案背后原理的人少之又少。問題如下:
通過三種不同的方式創(chuàng)建了三個(gè)對(duì)象,再依次兩兩匹配,每組被匹配的兩個(gè)對(duì)象是否相等?代碼如下:
String str1= "abc";
String str2= new String("abc");
String str3= str2.intern();
assertSame(str1==str2);
assertSame(str2==str3);
assertSame(str1==str3);
你可以先想想答案,以及這樣回答的原因。希望通過今天的學(xué)習(xí),你能拿到滿分。
1、String 對(duì)象是如何實(shí)現(xiàn)的?
在 Java 語言中,Sun 公司的工程師們對(duì) String 對(duì)象做了大量的優(yōu)化,來節(jié)約內(nèi)存空間,提升 String 對(duì)象在系統(tǒng)中的性能。一起來看看優(yōu)化過程,如下圖所示:
1.1、在 Java6 以及之前的版本中
String 對(duì)象是對(duì) char 數(shù)組進(jìn)行了封裝實(shí)現(xiàn)的對(duì)象,主要有四個(gè)成員變量:char 數(shù)組、偏移量 offset、字符數(shù)量 count、哈希值 hash。
String 對(duì)象是通過 offset 和 count 兩個(gè)屬性來定位 char[] 數(shù)組,獲取字符串。這么做可以高效、快速地共享數(shù)組對(duì)象,同時(shí)節(jié)省內(nèi)存空間,但這種方式很有可能會(huì)導(dǎo)致內(nèi)存泄漏。
1.2、從 Java7 版本開始到 Java8 版本
Java 對(duì) String 類做了一些改變。String 類中不再有 offset 和 count 兩個(gè)變量了。這樣的好處是 String 對(duì)象占用的內(nèi)存稍微少了些,同時(shí),String.substring 方法也不再共享 char[],從而解決了使用該方法可能導(dǎo)致的內(nèi)存泄漏問題。
1.3、從 Java9 版本開始
工程師將 char[] 字段改為了 byte[] 字段,又維護(hù)了一個(gè)新的屬性 coder,它是一個(gè)編碼格式的標(biāo)識(shí)。
工程師為什么這樣修改呢?
我們知道一個(gè) char 字符占 16 位,2 個(gè)字節(jié)。這個(gè)情況下,存儲(chǔ)單字節(jié)編碼內(nèi)的字符(占一個(gè)字節(jié)的字符)就顯得非常浪費(fèi)。JDK1.9 的 String 類為了節(jié)約內(nèi)存空間,于是使用了占 8 位,1 個(gè)字節(jié)的 byte 數(shù)組來存放字符串。
而新屬性 coder 的作用是,在計(jì)算字符串長度或者使用 indexOf()函數(shù)時(shí),我們需要根據(jù)這個(gè)字段,判斷如何計(jì)算字符串長度。coder 屬性默認(rèn)有 0 和 1 兩個(gè)值,0 代表 Latin-1(單字節(jié)編碼),1 代表 UTF-16。如果 String 判斷字符串只包含了 Latin-1,則 coder 屬性值為 0,反之則為 1。
2、String 對(duì)象的不可變性
了解了 String 對(duì)象的實(shí)現(xiàn)后,你有沒有發(fā)現(xiàn)在實(shí)現(xiàn)代碼中 String 類被 final 關(guān)鍵字修飾了,而且變量 char 數(shù)組也被 final 修飾了。
我們知道類被 final 修飾代表該類不可繼承,而 char[] 被 final+private 修飾,代表了 String 對(duì)象不可被更改。Java 實(shí)現(xiàn)的這個(gè)特性叫作 String 對(duì)象的不可變性,即 String 對(duì)象一旦創(chuàng)建成功,就不能再對(duì)它進(jìn)行改變。
2.1、Java 這樣做的好處在哪里呢?
第一,保證 String 對(duì)象的安全性。假設(shè) String 對(duì)象是可變的,那么 String 對(duì)象將可能被惡意修改。
第二,保證 hash 屬性值不會(huì)頻繁變更,確保了唯一性,使得類似 HashMap 容器才能實(shí)現(xiàn)相應(yīng)的 key-value 緩存功能。
第三,可以實(shí)現(xiàn)字符串常量池。在 Java 中,通常有兩種創(chuàng)建字符串對(duì)象的方式,一種是通過字符串常量的方式創(chuàng)建,如 String str=“abc”;另一種是字符串變量通過 new 形式的創(chuàng)建,如 String str = new String(“abc”)。
當(dāng)代碼中使用第一種方式創(chuàng)建字符串對(duì)象時(shí),JVM 首先會(huì)檢查該對(duì)象是否在字符串常量池中,如果在,就返回該對(duì)象引用,否則新的字符串將在常量池中被創(chuàng)建。這種方式可以減少同一個(gè)值的字符串對(duì)象的重復(fù)創(chuàng)建,節(jié)約內(nèi)存。
String str = new String(“abc”) 這種方式,首先在編譯類文件時(shí),"abc"常量字符串將會(huì)放入到常量結(jié)構(gòu)中,在類加載時(shí),“abc"將會(huì)在常量池中創(chuàng)建;其次,在調(diào)用 new 時(shí),JVM 命令將會(huì)調(diào)用 String 的構(gòu)造函數(shù),同時(shí)引用常量池中的"abc” 字符串,在堆內(nèi)存中創(chuàng)建一個(gè) String 對(duì)象;最后,str 將引用 String 對(duì)象。
2.2、經(jīng)典反例
平常編程時(shí),對(duì)一個(gè) String 對(duì)象 str 賦值“hello”,然后又讓 str 值為“world”,這個(gè)時(shí)候 str 的值變成了“world”。那么 str 值確實(shí)改變了,為什么我還說 String 對(duì)象不可變呢?
首先,我來解釋下什么是對(duì)象和對(duì)象引用。Java 初學(xué)者往往對(duì)此存在誤區(qū),特別是一些從 PHP 轉(zhuǎn) Java 的同學(xué)。在 Java 中要比較兩個(gè)對(duì)象是否相等,往往是用 ==,而要判斷兩個(gè)對(duì)象的值是否相等,則需要用 equals 方法來判斷。
這是因?yàn)?str 只是 String 對(duì)象的引用,并不是對(duì)象本身。對(duì)象在內(nèi)存中是一塊內(nèi)存地址,str 則是一個(gè)指向該內(nèi)存地址的引用。所以在剛剛我們說的這個(gè)例子中,第一次賦值的時(shí)候,創(chuàng)建了一個(gè)“hello”對(duì)象,str 引用指向“hello”地址;第二次賦值的時(shí)候,又重新創(chuàng)建了一個(gè)對(duì)象“world”,str 引用指向了“world”,但“hello”對(duì)象依然存在于內(nèi)存中。
也就是說 str 并不是對(duì)象,而只是一個(gè)對(duì)象引用。真正的對(duì)象依然還在內(nèi)存中,沒有被改變。
3、String 對(duì)象的優(yōu)化
了解了 String 對(duì)象的實(shí)現(xiàn)原理和特性,接下來我們就結(jié)合實(shí)際場景,看看如何優(yōu)化 String 對(duì)象的使用,優(yōu)化的過程中又有哪些需要注意的地方。
3.1、如何構(gòu)建超大字符串?
編程過程中,字符串的拼接很常見。前面我講過 String 對(duì)象是不可變的,如果我們使用 String 對(duì)象相加,拼接我們想要的字符串,是不是就會(huì)產(chǎn)生多個(gè)對(duì)象呢?例如以下代碼:
String str= "ab" + "cd" + "ef";
分析代碼可知:首先會(huì)生成 ab 對(duì)象,再生成 abcd 對(duì)象,最后生成 abcdef 對(duì)象,從理論上來說,這段代碼是低效的。
但實(shí)際運(yùn)行中,我們發(fā)現(xiàn)只有一個(gè)對(duì)象生成,這是為什么呢?難道我們的理論判斷錯(cuò)了?我們?cè)賮砜淳幾g后的代碼,你會(huì)發(fā)現(xiàn)編譯器自動(dòng)優(yōu)化了這行代碼,如下:
String str= "abcdef";
上面我介紹的是字符串常量的累計(jì),我們?cè)賮砜纯醋址兞康睦塾?jì)又是怎樣的呢?
String str = "abcdef";
for(int i=0; i<1000; i++) {
str = str + i;
}
上面的代碼編譯后,你可以看到編譯器同樣對(duì)這段代碼進(jìn)行了優(yōu)化。不難發(fā)現(xiàn),Java 在進(jìn)行字符串的拼接時(shí),偏向使用 StringBuilder,這樣可以提高程序的效率。
String str = "abcdef";
for(int i=0; i<1000; i++) {
str = (new StringBuilder(String.valueOf(str))).append(i).toString();
}
綜上已知:即使使用 + 號(hào)作為字符串的拼接,也一樣可以被編譯器優(yōu)化成 StringBuilder 的方式。但再細(xì)致些,你會(huì)發(fā)現(xiàn)在編譯器優(yōu)化的代碼中,每次循環(huán)都會(huì)生成一個(gè)新的 StringBuilder 實(shí)例,同樣也會(huì)降低系統(tǒng)的性能。
所以平時(shí)做字符串拼接的時(shí)候,我建議你還是要顯示地使用 String Builder 來提升系統(tǒng)性能。
如果在多線程編程中,String 對(duì)象的拼接涉及到線程安全,你可以使用 StringBuffer。但是要注意,由于 StringBuffer 是線程安全的,涉及到鎖競爭,所以從性能上來說,要比 StringBuilder 差一些。
3.2、如何使用 String.intern 節(jié)省內(nèi)存?
講完了構(gòu)建字符串,我們?cè)賮碛懻撓?String 對(duì)象的存儲(chǔ)問題。先看一個(gè)案例。
Twitter 每次發(fā)布消息狀態(tài)的時(shí)候,都會(huì)產(chǎn)生一個(gè)地址信息,以當(dāng)時(shí) Twitter 用戶的規(guī)模預(yù)估,服務(wù)器需要 32G 的內(nèi)存來存儲(chǔ)地址信息。
public class Location {
private String city;
private String region;
private String countryCode;
private double longitude;
private double latitude;
}
考慮到其中有很多用戶在地址信息上是有重合的,比如,國家、省份、城市等,這時(shí)就可以將這部分信息單獨(dú)列出一個(gè)類,以減少重復(fù),代碼如下:
public class SharedLocation {
private String city;
private String region;
private String countryCode;
}
public class Location {
private SharedLocation sharedLocation;
double longitude;
double latitude;
}
通過優(yōu)化,數(shù)據(jù)存儲(chǔ)大小減到了 20G 左右。但對(duì)于內(nèi)存存儲(chǔ)這個(gè)數(shù)據(jù)來說,依然很大,怎么辦呢?
這個(gè)案例來自一位 Twitter 工程師在 QCon 全球軟件開發(fā)大會(huì)上的演講,他們想到的解決方法,就是使用 String.intern 來節(jié)省內(nèi)存空間,從而優(yōu)化 String 對(duì)象的存儲(chǔ)。
具體做法就是,在每次賦值的時(shí)候使用 String 的 intern 方法,如果常量池中有相同值,就會(huì)重復(fù)使用該對(duì)象,返回對(duì)象引用,這樣一開始的對(duì)象就可以被回收掉。這種方式可以使重復(fù)性非常高的地址信息存儲(chǔ)大小從 20G 降到幾百兆。
SharedLocation sharedLocation = new SharedLocation();
sharedLocation.setCity(messageInfo.getCity().intern());
sharedLocation.setCountryCode(messageInfo.getRegion().intern());
sharedLocation.setRegion(messageInfo.getCountryCode().intern());
Location location = new Location();
location.set(sharedLocation);
location.set(messageInfo.getLongitude());
location.set(messageInfo.getLatitude());
為了更好地理解,我們?cè)賮硗ㄟ^一個(gè)簡單的例子,回顧下其中的原理:
String a =new String("abc").intern();
String b = new String("abc").intern();
if(a==b) {
System.out.print("a==b");
}
輸出結(jié)果:
a==b
在字符串常量中,默認(rèn)會(huì)將對(duì)象放入常量池;在字符串變量中,對(duì)象是會(huì)創(chuàng)建在堆內(nèi)存中,同時(shí)也會(huì)在常量池中創(chuàng)建一個(gè)字符串對(duì)象,復(fù)制到堆內(nèi)存對(duì)象中,并返回堆內(nèi)存對(duì)象引用。
如果調(diào)用 intern 方法,會(huì)去查看字符串常量池中是否有等于該對(duì)象的字符串,如果沒有,就在常量池中新增該對(duì)象,并返回該對(duì)象引用;如果有,就返回常量池中的字符串引用。堆內(nèi)存中原有的對(duì)象由于沒有引用指向它,將會(huì)通過垃圾回收器回收。
了解了原理,我們?cè)僖黄鹂纯瓷线叺睦印?/p>
在一開始創(chuàng)建 a 變量時(shí),會(huì)在堆內(nèi)存中創(chuàng)建一個(gè)對(duì)象,同時(shí)會(huì)在加載類時(shí),在常量池中創(chuàng)建一個(gè)字符串對(duì)象,在調(diào)用 intern 方法之后,會(huì)去常量池中查找是否有等于該字符串的對(duì)象,有就返回引用。
在創(chuàng)建 b 字符串變量時(shí),也會(huì)在堆中創(chuàng)建一個(gè)對(duì)象,此時(shí)常量池中有該字符串對(duì)象,就不再創(chuàng)建。調(diào)用 intern 方法則會(huì)去常量池中判斷是否有等于該字符串的對(duì)象,發(fā)現(xiàn)有等于"abc"字符串的對(duì)象,就直接返回引用。而在堆內(nèi)存中的對(duì)象,由于沒有引用指向它,將會(huì)被垃圾回收。所以 a 和 b 引用的是同一個(gè)對(duì)象。
下面我用一張圖來總結(jié)下 String 字符串的創(chuàng)建分配內(nèi)存地址情況:
?使用 intern 方法需要注意的一點(diǎn)是,一定要結(jié)合實(shí)際場景。因?yàn)槌A砍氐膶?shí)現(xiàn)是類似于一個(gè) HashTable 的實(shí)現(xiàn)方式,HashTable 存儲(chǔ)的數(shù)據(jù)越大,遍歷的時(shí)間復(fù)雜度就會(huì)增加。如果數(shù)據(jù)過大,會(huì)增加整個(gè)字符串常量池的負(fù)擔(dān)。
3.3、如何使用字符串的分割方法?
最后我想跟你聊聊字符串的分割,這種方法在編碼中也很最常見。
Split() 方法使用了正則表達(dá)式實(shí)現(xiàn)了其強(qiáng)大的分割功能,而正則表達(dá)式的性能是非常不穩(wěn)定的,使用不恰當(dāng)會(huì)引起回溯問題,很可能導(dǎo)致 CPU 居高不下。
所以我們應(yīng)該慎重使用 Split() 方法,我們可以用 String.indexOf() 方法代替 Split() 方法完成字符串的分割。
如果實(shí)在無法滿足需求,你就在使用 Split() 方法時(shí),對(duì)回溯問題加以重視就可以了。
4、總結(jié)
這一講中,我們認(rèn)識(shí)到做好 String 字符串性能優(yōu)化,可以提高系統(tǒng)的整體性能。在這個(gè)理論基礎(chǔ)上,Java 版本在迭代中通過不斷地更改成員變量,節(jié)約內(nèi)存空間,對(duì) String 對(duì)象進(jìn)行優(yōu)化。
我們還特別提到了 String 對(duì)象的不可變性,正是這個(gè)特性實(shí)現(xiàn)了字符串常量池,通過減少同一個(gè)值的字符串對(duì)象的重復(fù)創(chuàng)建,進(jìn)一步節(jié)約內(nèi)存。文章來源:http://www.zghlxwxcb.cn/news/detail-650609.html
但也是因?yàn)檫@個(gè)特性,我們?cè)谧鲩L字符串拼接時(shí),需要顯示使用 StringBuilder,以提高字符串的拼接性能。最后,在優(yōu)化方面,我們還可以使用 intern 方法,讓變量字符串對(duì)象重復(fù)使用常量池中相同值的對(duì)象,進(jìn)而節(jié)約內(nèi)存。文章來源地址http://www.zghlxwxcb.cn/news/detail-650609.html
到了這里,關(guān)于03 - 字符串性能優(yōu)化不容小覷,百M(fèi)內(nèi)存輕松存儲(chǔ)幾十G數(shù)據(jù)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!