當(dāng)前位置:首頁 > 技術(shù)學(xué)院 > 技術(shù)前線
[導(dǎo)讀]String 類型是我們使用最頻繁的數(shù)據(jù)類型,沒有之一。那么提高 String 的運行效率,無疑是提升程序性能的最佳手段。

String 類型是我們使用最頻繁的數(shù)據(jù)類型,沒有之一。那么提高 String 的運行效率,無疑是提升程序性能的最佳手段。

String字符串是系統(tǒng)里最常用的類型之一,在系統(tǒng)中占據(jù)了很大的內(nèi)存,因此,高效地使用字符串,對系統(tǒng)的性能有較好的提升。

我們本文將從 String 的源碼入手,一步步帶你實現(xiàn)字符串優(yōu)化的小目標(biāo)。不但教你如何有效的使用字符串,還為你揭曉這背后的深層次原因。

本文涉及的知識點,如下圖所示:


String性能提升10倍的幾個方法!(源碼+原理分析)

在看如何優(yōu)化 String 之前,我們先來了解一下 String 的特性,畢竟知己知彼,才能百戰(zhàn)不殆。

字符串的特性想要了解 String 的特性就必須從它的源碼入手,如下所示:

// 源碼基于 JDK 1.8public final class String implements java.io.Serializable, Comparable, CharSequence { // String 值的實際存儲容器 private final char value[]; public String() { this.value = "".value; } public String(String original) { this.value = original.value; this.hash = original.hash; } // 忽略其他信息}

從他的源碼我們可以看出,String 類以及它的 value[] 屬性都被 final 修飾了,其中 value[] 是實現(xiàn)字符串存儲的最終結(jié)構(gòu),而 final 則表示“最后的、最終的”。

我們知道,被 final 修飾的類是不能被繼承的,也就是說此類將不能擁有子類,而被 final 修飾的變量即為常量,它的值是不能被改變的。這也就說當(dāng) String 一旦被創(chuàng)建之后,就不能被修改了。

String 為什么不能被修改?

String 的類和屬性 value[] 都被定義為 final 了,這樣做的好處有以下三點:

安全性:當(dāng)你在調(diào)用其他方法時,比如調(diào)用一些系統(tǒng)級操作指令之前,可能會有一系列校驗,如果是可變類的話,可能在你校驗過后,它的內(nèi)部的值又被改變了,這樣有可能會引起嚴(yán)重的系統(tǒng)崩潰問題,所以迫使 String 設(shè)計為 final 類的一個重要原因就是出于安全考慮;

高性能:String 不可變之后就保證的 hash 值的唯一性,這樣它就更加高效,并且更適合做 HashMap 的 key- value 緩存;

節(jié)約內(nèi)存:String 的不可變性是它實現(xiàn)字符串常量池的基礎(chǔ),字符串常量池指的是字符串在創(chuàng)建時,先去“常量池”查找是否有此“字符串”,如果有,則不會開辟新空間創(chuàng)作字符串,而是直接把常量池中的引用返回給此對象,這樣就能更加節(jié)省空間。例如,通常情況下 String 創(chuàng)建有兩種方式,直接賦值的方式,如 String str="Java";另一種是 new 形式的創(chuàng)建,如 String str = new String("Java")。當(dāng)代碼中使用第一種方式創(chuàng)建字符串對象時,JVM 首先會檢查該對象是否在字符串常量池中,如果在,就返回該對象引用,否則新的字符串將在常量池中被創(chuàng)建。這種方式可以 減少同一個值的字符串對象的重復(fù)創(chuàng)建,節(jié)約內(nèi)存。String str = new String("Java") 這種方式,首先在編譯類文件時,“Java”常量字符串將會放入到常量結(jié)構(gòu)中,在類加載時,“Java”將會在常量池中創(chuàng)建;其次,在調(diào)用 new 時,JVM 命令將會調(diào)用 String 的構(gòu)造函數(shù),同時引用常量池中的“Java”字符串,在堆內(nèi)存中創(chuàng)建一個 String 對象,最后 str 將引用 String 對象。

1.不要直接+=字符串通過上面的內(nèi)容,我們知道了 String 類是不可變的,那么在使用 String 時就不能頻繁的 += 字符串了。

優(yōu)化前代碼:

public static String doAdd() { String result = ""; for (int i = 0; i < 10000; i++) { result += (" i:" + i); } return result;}

有人可能會問,我的業(yè)務(wù)需求是這樣的,那我該如何實現(xiàn)?

官方為我們提供了兩種字符串拼加的方案:StringBuffer 和 StringBuilder,其中 StringBuilder 為非線程安全的,而 StringBuffer 則是線程安全的,StringBuffer 的拼加方法使用了關(guān)鍵字 synchronized 來保證線程的安全,源碼如下:

@Overridepublic synchronized StringBuffer append(CharSequence s) { toStringCache = null; super.append(s); return this;}

也因為使用 synchronized 修飾,所以 StringBuffer 的拼加性能會比 StringBuilder 低。

那我們就用 StringBuilder 來實現(xiàn)字符串的拼加,優(yōu)化后代碼:

public static String doAppend() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10000; i++) { sb.append(" i:" + i); } return sb.toString();}

我們通過代碼測試一下,兩個方法之間的性能差別:

public class StringTest { public static void main(String[] args) { for (int i = 0; i < 5; i++) { // String long st1 = System.currentTimeMillis(); // 開始時間 doAdd(); long et1 = System.currentTimeMillis(); // 開始時間 System.out.println("String 拼加,執(zhí)行時間:" + (et1 - st1)); // StringBuilder long st2 = System.currentTimeMillis(); // 開始時間 doAppend(); long et2 = System.currentTimeMillis(); // 開始時間 System.out.println("StringBuilder 拼加,執(zhí)行時間:" + (et2 - st2)); System.out.println(); } } public static String doAdd() { String result = ""; for (int i = 0; i < 10000; i++) { result += ("Java中文社群:" + i); } return result; } public static String doAppend() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10000; i++) { sb.append("Java中文社群:" + i); } return sb.toString(); }}

以上程序的執(zhí)行結(jié)果如下:

String 拼加,執(zhí)行時間:429

StringBuilder 拼加,執(zhí)行時間:1

String 拼加,執(zhí)行時間:325

StringBuilder 拼加,執(zhí)行時間:1

String 拼加,執(zhí)行時間:287

StringBuilder 拼加,執(zhí)行時間:1

String 拼加,執(zhí)行時間:265

StringBuilder 拼加,執(zhí)行時間:1

String 拼加,執(zhí)行時間:249

StringBuilder 拼加,執(zhí)行時間:1

從結(jié)果可以看出,優(yōu)化前后的性能相差很大。

注意:此性能測試的結(jié)果與循環(huán)的次數(shù)有關(guān),也就是說循環(huán)的次數(shù)越多,他們性能相除的結(jié)果也越大。

接下來,我們要思考一個問題:為什么 StringBuilder.append() 方法比 += 的性能高?而且拼接的次數(shù)越多性能的差距也越大?

當(dāng)我們打開 StringBuilder 的源碼,就可以發(fā)現(xiàn)其中的“小秘密”了,StringBuilder 父類 AbstractStringBuilder 的實現(xiàn)源碼如下:

abstract class AbstractStringBuilder implements Appendable, CharSequence { char[] value; int count; @Override public AbstractStringBuilder append(CharSequence s, int start, int end) { if (s == null) s = "null"; if ((start < 0) || (start > end) || (end > s.length())) throw new IndexOutOfBoundsException( "start " + start + ", end " + end + ", s.length() " + s.length()); int len = end - start; ensureCapacityInternal(count + len); for (int i = start, j = count; i < end; i++, j++) value[j] = s.charAt(i); count += len; return this; } // 忽略其他信息...}

而 StringBuilder 使用了父類提供的 char[] 作為自己值的實際存儲單元,每次在拼加時會修改 char[] 數(shù)組,StringBuilder toString() 源碼如下:

@Overridepublic String toString() { // Create a copy, don't share the array return new String(value, 0, count);}

綜合以上源碼可以看出:StringBuilder 使用了 char[] 作為實際存儲單元,每次在拼加時只需要修改 char[] 數(shù)組即可,只是在 toString() 時創(chuàng)建了一個字符串;而 String 一旦創(chuàng)建之后就不能被修改,因此在每次拼加時,都需要重新創(chuàng)建新的字符串,所以 StringBuilder.append() 的性能就會比字符串的 += 性能高很多。

2.善用 intern 方法善用 String.intern() 方法可以有效的節(jié)約內(nèi)存并提升字符串的運行效率,先來看 intern() 方法的定義與源碼:

/*** Returns a canonical representation for the string object.*

* A pool of strings, initially empty, is maintained privately by the* class {@code String}.*

* When the intern method is invoked, if the pool already contains a* string equal to this {@code String} object as determined by* the {@link #equals(Object)} method, then the string from the pool is* returned. Otherwise, this {@code String} object is added to the* pool and a reference to this {@code String} object is returned.*

* It follows that for any two strings {@code s} and {@code t},* {@code s.intern() == t.intern()} is {@code true}* if and only if {@code s.equals(t)} is {@code true}.*

* All literal strings and string-valued constant expressions are* interned. String literals are defined in section 3.10.5 of the* The Java? Language Specification.** @return a string that has the same contents as this string, but is* guaranteed to be from a pool of unique strings.*/public native String intern();

可以看出 intern() 是一個高效的本地方法,它的定義中說的是,當(dāng)調(diào)用 intern 方法時,如果字符串常量池中已經(jīng)包含此字符串,則直接返回此字符串的引用,如果不包含此字符串,先將字符串添加到常量池中,再返回此對象的引用。

那什么情況下適合使用 intern() 方法?

Twitter 工程師曾分享過一個 String.intern() 的使用示例,Twitter 每次發(fā)布消息狀態(tài)的時候,都會產(chǎn)生一個地址信息,以當(dāng)時 Twitter 用戶的規(guī)模預(yù)估,服務(wù)器需要 32G 的內(nèi)存來存儲地址信息。

public class Location { private String city; private String region; private String countryCode; private double longitude; private double latitude;}

考慮到其中有很多用戶在地址信息上是有重合的,比如,國家、省份、城市等,這時就可以將這部分信息單獨列出一個類,以減少重復(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ù)存儲大小減到了 20G 左右。但對于內(nèi)存存儲這個數(shù)據(jù)來說,依然很大,怎么辦呢?

Twitter 工程師使用 String.intern() 使重復(fù)性非常高的地址信息存儲大小從 20G 降到幾百兆,從而優(yōu)化了 String 對象的存儲。

實現(xiàn)的核心代碼如下:

SharedLocation sharedLocation = new SharedLocation();sharedLocation.setCity(messageInfo.getCity().intern()); sharedLocation.setCountryCode(messageInfo.getRegion().intern());sharedLocation.setRegion(messageInfo.getCountryCode().intern());

從 JDK1.7 版本以后,常量池已經(jīng)合并到了堆中,所以不會復(fù)制字符串副本,只是會把首次遇到的字符串的引用添加到常量池中。此時只會判斷常量池中是否已經(jīng)有此字符串,如果有就返回常量池中的字符串引用。

這就相當(dāng)于以下代碼:

String s1 = new String("Java中文社群").intern();String s2 = new String("Java中文社群").intern();System.out.println(s1 == s2);

執(zhí)行的結(jié)果為:true

此處如果有人問為什么不直接賦值(使用 String s1 = "Java中文社群"),是因為這段代碼是簡化了上面 Twitter 業(yè)務(wù)代碼的語義而創(chuàng)建的,他使用的是對象的方式,而非直接賦值的方式。更多關(guān)于 intern() 的內(nèi)容可以查看《別再問我new字符串創(chuàng)建了幾個對象了!我來證明給你看!》這篇文章。

3.慎重使用 Split 方法之所以要勸各位慎用 Split 方法,是因為 Split 方法大多數(shù)情況下使用的是正則表達(dá)式,這種分割方式本身沒有什么問題,但是由于正則表達(dá)式的性能是非常不穩(wěn)定的,使用不恰當(dāng)會引起回溯問題,很可能導(dǎo)致 CPU 居高不下。

例如以下正則表達(dá)式:

String badRegex = "^([hH][tT]{2}[pP]://|[hH][tT]{2}[pP][sS]://)(([A-Za-z0-9-~]+).)+([A-Za-z0-9-~\\\\/])+$";String bugUrl = "http://www.apigo.com/dddp-web/pdf/download?request=6e7JGxxxxx4ILd-kExxxxxxxqJ4-CHLmqVnenXC692m74H38sdfdsazxcUmfcOH2fAfY1Vw__%5EDadIfJgiEf";if (bugUrl.matches(badRegex)) { System.out.println("match!!");} else { System.out.println("no match!!");}

執(zhí)行效果如下圖所示:


String性能提升10倍的幾個方法!(源碼+原理分析)

可以看出,此代碼導(dǎo)致了 CPU 使用過高。

Java 正則表達(dá)式使用的引擎實現(xiàn)是 NFA(Non deterministic Finite Automaton,不確定型有窮自動機(jī))自動機(jī),這種正則表達(dá)式引擎在進(jìn)行字符匹配時會發(fā)生回溯(backtracking),而一旦發(fā)生回溯,那其消耗的時間就會變得很長,有可能是幾分鐘,也有可能是幾個小時,時間長短取決于回溯的次數(shù)和復(fù)雜度。

為了更好地解釋什么是回溯,我們使用以下面例子進(jìn)行解釋:

text = "abbc";regex = "ab{1,3}c";

上面的這個例子的目的比較簡單,匹配以 a 開頭,以 c 結(jié)尾,中間有 1-3 個 b 字符的字符串。

NFA 引擎對其解析的過程是這樣子的:

首先,讀取正則表達(dá)式第一個匹配符 a 和 字符串第一個字符 a 比較,匹配上了,于是讀取正則表達(dá)式第二個字符;

讀取正則表達(dá)式第二個匹配符 b{1,3} 和字符串的第二個字符 b 比較,匹配上了。但因為 b{1,3} 表示 1-3 個 b 字符串,以及 NFA 自動機(jī)的貪婪特性(也就是說要盡可能多地匹配),所以此時并不會再去讀取下一個正則表達(dá)式的匹配符,而是依舊使用 b{1,3} 和字符串的第三個字符 b 比較,發(fā)現(xiàn)還是匹配上了,于是繼續(xù)使用 b{1,3} 和字符串的第四個字符 c 比較,發(fā)現(xiàn)不匹配了,此時就會發(fā)生回溯;

發(fā)生回溯后,我們已經(jīng)讀取的字符串第四個字符 c 將被吐出去,指針回到第三個字符串的位置,之后程序讀取正則表達(dá)式的下一個操作符 c,然后再讀取當(dāng)前指針的下一個字符 c 進(jìn)行對比,發(fā)現(xiàn)匹配上了,于是讀取下一個操作符,然后發(fā)現(xiàn)已經(jīng)結(jié)束了。

這就是正則匹配執(zhí)行的流程和簡單的回溯執(zhí)行流程,而上面的示例在匹配到“com/dzfp-web/pdf/download?request=6e7JGm38jf.....”時因為貪婪匹配的原因,所以程序會一直讀后面的字符串進(jìn)行匹配,最后發(fā)現(xiàn)沒有點號,于是就一個個字符回溯回去了,于是就會導(dǎo)致了 CPU 運行過高。

所以我們應(yīng)該慎重使用 Split() 方法,我們可以用 String.indexOf() 方法代替 Split() 方法完成字符串的分割。如果實在無法滿足需求,你就在使用 Split() 方法時,對回溯問題加以重視就可以了。

總結(jié)本文通過 String 源碼分析,發(fā)現(xiàn)了 String 的不可變特性,以及不可變特性的 3 大優(yōu)點講解;然后講了字符串優(yōu)化的三個手段:不要直接 += 字符串、善用 intern() 方法和慎重使用 Split() 方法。并且通過 StringBuilder 的源碼分析,了解了 append() 性能高的主要原因,以及正則表達(dá)式不穩(wěn)定性導(dǎo)致回溯問題,進(jìn)入導(dǎo)致 CPU 使用過高的案例分析,希望可以切實的幫助到你。

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內(nèi)容真實性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫毥谦F公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉(zhuǎn)型技術(shù)解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關(guān)鍵字: AWS AN BSP 數(shù)字化

倫敦2024年8月29日 /美通社/ -- 英國汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認(rèn)證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動 BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運行,同時企業(yè)卻面臨越來越多業(yè)務(wù)中斷的風(fēng)險,如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報道,騰訊和網(wǎng)易近期正在縮減他們對日本游戲市場的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機(jī) 衛(wèi)星通信

要點: 有效應(yīng)對環(huán)境變化,經(jīng)營業(yè)績穩(wěn)中有升 落實提質(zhì)增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競爭力 堅持高質(zhì)量發(fā)展策略,塑強核心競爭優(yōu)勢...

關(guān)鍵字: 通信 BSP 電信運營商 數(shù)字經(jīng)濟(jì)

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術(shù)學(xué)會聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(shù)(集團(tuán))股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉