String 是 Java 語(yǔ)言非?;A(chǔ)和重要的類(lèi),提供了構(gòu)造和管理字符串的各種基本邏輯。它是典型的 Immutable 類(lèi),被聲明成為 fnal class ,所有屬性也都是 fnal 的。也由于它的不可變性,類(lèi)似拼接、裁剪字符串等動(dòng)作,都會(huì)產(chǎn)生新的 String 對(duì)象。由于字符串操作的普遍性,所以相關(guān)操作的效率往往對(duì)應(yīng)用性能有明顯影響。
StringBufer 是為解決上面提到拼接產(chǎn)生太多中間對(duì)象的問(wèn)題而提供的一個(gè)類(lèi),我們可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。 StringBufer 本質(zhì)是一個(gè)線(xiàn)程安全的可修改字符序列,它保證了線(xiàn)程安全,也隨之帶來(lái)了額外的性能開(kāi)銷(xiāo),所以除非有線(xiàn)程安全的需要,不然還是推薦使用它的后繼者,也就是 StringBuilder 。
StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBufer 沒(méi)有本質(zhì)區(qū)別,但是它去掉了線(xiàn)程安全的部分,有效減小了開(kāi)銷(xiāo),是絕大部分情況下進(jìn)行字符串拼接的首選。
至少你要知道 String 是 Immutable 的,字符串操作不當(dāng)可能會(huì)產(chǎn)生大量臨時(shí)字符串,以及線(xiàn)程安全方面的區(qū)別。
如果繼續(xù)深入,面試官可以從各種不同的角度考察,比如可以:
1.通過(guò)String和相關(guān)類(lèi),考察基本的線(xiàn)程安全設(shè)計(jì)與實(shí)現(xiàn),各種基礎(chǔ)編程實(shí)踐。
2.考察JVM對(duì)象緩存機(jī)制的理解以及如何良好地使用。
3.考察JVM優(yōu)化Java代碼的一些技巧。
4.String相關(guān)類(lèi)的演進(jìn),比如Java 9中實(shí)現(xiàn)的巨大變化。
知識(shí)擴(kuò)展
1. 字符串設(shè)計(jì)和實(shí)現(xiàn)考量
我在前面介紹過(guò), String 是 Immutable 類(lèi)的典型實(shí)現(xiàn),原生的保證了基礎(chǔ)線(xiàn)程安全,因?yàn)槟銦o(wú)法對(duì)它內(nèi)部數(shù)據(jù)進(jìn)行任何修改,這種便利甚至體現(xiàn)在拷貝構(gòu)造函數(shù)中,由于不可變, Immutable 對(duì)象在拷貝時(shí)不需要額外復(fù)制數(shù)據(jù)。
我們?cè)賮?lái)看看 StringBufer 實(shí)現(xiàn)的一些細(xì)節(jié),它的線(xiàn)程安全是通過(guò)把各種修改數(shù)據(jù)的方法都加上 synchronized 關(guān)鍵字實(shí)現(xiàn)的,非常直白。其實(shí),這種簡(jiǎn)單粗暴的實(shí)現(xiàn)方式,非常適合我們常見(jiàn)的線(xiàn)程安全類(lèi)實(shí)現(xiàn),不必糾結(jié)于 synchronized 性能之類(lèi)的,有人說(shuō) “ 過(guò)早優(yōu)化是萬(wàn)惡之源 ” ,考慮可靠性、正確性和代碼可讀性才是大多數(shù)應(yīng)用開(kāi)發(fā)最重要的因素。
為了實(shí)現(xiàn)修改字符序列的目的, StringBufer 和 StringBuilder 底層都是利用可修改的( char , JDK 9 以后是 byte )數(shù)組,二者都繼承了 AbstractStringBuilder ,里面包含了基本操作,區(qū)別僅在于最終的方法是否加了 synchronized 。
另外,這個(gè)內(nèi)部數(shù)組應(yīng)該創(chuàng)建成多大的呢?如果太小,拼接的時(shí)候可能要重新創(chuàng)建足夠大的數(shù)組;如果太大,又會(huì)浪費(fèi)空間。目前的實(shí)現(xiàn)是,構(gòu)建時(shí)初始字符串長(zhǎng)度加 16 (這意味著,如果沒(méi)有構(gòu)建對(duì)象時(shí)輸入最初的字符串,那么初始值就是 16 )。我們?nèi)绻_定拼接會(huì)發(fā)生非常多次,而且大概是可預(yù)計(jì)的,那么就可以指定合適的大小,避免很多次擴(kuò)容的開(kāi)銷(xiāo)。擴(kuò)容會(huì)產(chǎn)生多重開(kāi)銷(xiāo),因?yàn)橐獟仐壴袛?shù)組,創(chuàng)建新的(可以簡(jiǎn)單認(rèn)為是倍數(shù))數(shù)組,還要進(jìn)行 arraycopy 。
前面我講的這些內(nèi)容,在具體的代碼書(shū)寫(xiě)中,應(yīng)該如何選擇呢?
在沒(méi)有線(xiàn)程安全問(wèn)題的情況下,全部拼接操作是應(yīng)該都用 StringBuider 實(shí)現(xiàn)嗎?畢竟這樣書(shū)寫(xiě)的代碼,還是要多敲很多字的,可讀性也不理想,下面的對(duì)比非常明顯。

其實(shí),在通常情況下,沒(méi)有必要過(guò)于擔(dān)心,要相信 Java 還是非常智能的。
我們來(lái)做個(gè)實(shí)驗(yàn),把下面一段代碼,利用不同版本的 JDK 編譯,然后再反編譯,例如:

先編譯再反編譯,比如使用 JDK 9 :

JDK 8 的輸出片段是:

而在 JDK 9 中,反編譯的結(jié)果就非常簡(jiǎn)單了,片段是:

你可以看到,在 JDK 8 中,字符串拼接操作會(huì)自動(dòng)被 javac 轉(zhuǎn)換為 StringBuilder 操作,而在 JDK 9 里面則是因?yàn)?Java 9 為了更加統(tǒng)一字符串操作優(yōu)化,提供了 StringConcatFactory ,作為一個(gè)統(tǒng)一的入口。 javac 自動(dòng)生成的代碼,雖然未必是最優(yōu)化的,但普通場(chǎng)景也足夠了,你可以酌情選擇。
2. 字符串緩存
我們粗略統(tǒng)計(jì)過(guò),把常見(jiàn)應(yīng)用進(jìn)行堆轉(zhuǎn)儲(chǔ)( Dump Heap ),然后分析對(duì)象組成,會(huì)發(fā)現(xiàn)平均 25% 的對(duì)象是字符串,并且其中約半數(shù)是重復(fù)的。如果能避免創(chuàng)建重復(fù)字符串,可以有效降低內(nèi)存消耗和對(duì)象創(chuàng)建開(kāi)銷(xiāo)。
String 在 Java 6 以后提供了 intern() 方法,目的是提示 JVM 把相應(yīng)字符串緩存起來(lái),以備重復(fù)使用。在我們創(chuàng)建字符串對(duì)象并調(diào)用 intern() 方法的時(shí)候,如果已經(jīng)有緩存的字符串,就會(huì)返回緩存里的實(shí)例,否則將其緩存起來(lái)。一般來(lái)說(shuō), JVM 會(huì)將所有的類(lèi)似 “abc” 這樣的文本字符串,或者字符串常量之類(lèi)緩存起來(lái)。
看起來(lái)很不錯(cuò)是吧?但實(shí)際情況估計(jì)會(huì)讓你大跌眼鏡。一般使用 Java 6 這種歷史版本,并不推薦大量使用 intern ,為什么呢?魔鬼存在于細(xì)節(jié)中,被緩存的字符串是存在所謂 PermGen 里的,也就是臭名昭著的 “ 永久代 ” ,這個(gè)空間是很有限的,也基本不會(huì)被 FullGC 之外的垃圾收集照顧到。所以,如果使用不當(dāng), OOM 就會(huì)光顧。
在后續(xù)版本中,這個(gè)緩存被放置在堆中,這樣就極大避免了永久代占滿(mǎn)的問(wèn)題,甚至永久代在 JDK 8 中被 MetaSpace (元數(shù)據(jù)區(qū))替代了。而且,默認(rèn)緩存大小也在不斷地?cái)U(kuò)大中,從最初的 1009 ,到 7u40 以后被修改為 60013 。你可以使用下面的參數(shù)直接打印具體數(shù)字,可以拿自己的 JDK 立刻試驗(yàn)一下。

你也可以使用下面的 JVM 參數(shù)手動(dòng)調(diào)整大小,但是絕大部分情況下并不需要調(diào)整,除非你確定它的大小已經(jīng)影響了操作效率。

Intern是一種 顯式地排重機(jī)制 ,但是它也有一定的副作用,因?yàn)樾枰_(kāi)發(fā)者寫(xiě)代碼時(shí)明確調(diào)用,一是不方便,每一個(gè)都顯式調(diào)用是非常麻煩的;另外就是我們很難保證效率,應(yīng)用開(kāi)發(fā)階段很難清楚地預(yù)計(jì)字符串的重復(fù)情況,有人認(rèn)為這是一種污染代碼的實(shí)踐。
幸好在 Oracle JDK 8u20 之后,推出了一個(gè)新的特性,也就是 G1 GC 下的字符串排重。它是通過(guò)將相同數(shù)據(jù)的字符串指向同一份數(shù)據(jù)來(lái)做到的,是 JVM 底層的改變,并不需要 Java 類(lèi)庫(kù)做什么修改。
注意這個(gè)功能目前是默認(rèn)關(guān)閉的,你需要使用下面參數(shù)開(kāi)啟,并且記得指定使用 G1 GC :

前面說(shuō)到的幾個(gè)方面,只是 Java 底層對(duì)字符串各種優(yōu)化的一角,在運(yùn)行時(shí),字符串的一些基礎(chǔ)操作會(huì)直接利用 JVM 內(nèi)部的 Intrinsic 機(jī)制,往往運(yùn)行的就是特殊優(yōu)化的本地代碼,而根本就不是 Java 代碼生成的字節(jié)碼。 Intrinsic 可以簡(jiǎn)單理解為,是一種利用 native 方式 hard-coded 的邏輯,算是一種特別的內(nèi)聯(lián),很多優(yōu)化還是需要直接使用特定的 CPU 指令,具體可以看相關(guān) 源碼 ,搜索“string”以查找相關(guān)Intrinsic定義。當(dāng)然,你也可以在啟動(dòng)實(shí)驗(yàn)應(yīng)用時(shí),使用下面參數(shù),了解intrinsic發(fā)生的狀態(tài)。

可以看出,僅僅是字符串一個(gè)實(shí)現(xiàn),就需要 Java 平臺(tái)工程師和科學(xué)家付出如此大且默默無(wú)聞的努力,我們得到的很多便利都是來(lái)源于此。
我會(huì)在專(zhuān)欄后面的 JVM 和性能等主題,詳細(xì)介紹 JVM 內(nèi)部?jī)?yōu)化的一些方法,如果你有興趣可以再深入學(xué)習(xí)。即使你不做 JVM 開(kāi)發(fā)或者暫時(shí)還沒(méi)有使用到特別的性能優(yōu)化,這些知識(shí)也能幫助你增加技術(shù)深度。
3.String 自身的演化
如果你仔細(xì)觀(guān)察過(guò) Java 的字符串,在歷史版本中,它是使用 char 數(shù)組來(lái)存數(shù)據(jù)的,這樣非常直接。但是 Java 中的 char 是兩個(gè) bytes 大小,拉丁語(yǔ)系語(yǔ)言的字符,根本就不需要太寬的 char ,這樣無(wú)區(qū)別的實(shí)現(xiàn)就造成了一定的浪費(fèi)。密度是編程語(yǔ)言平臺(tái)永恒的話(huà)題,因?yàn)闅w根結(jié)底絕大部分任務(wù)是要來(lái)操作數(shù)據(jù)的。
其實(shí)在 Java 6 的時(shí)候, Oracle JDK 就提供了壓縮字符串的特性,但是這個(gè)特性的實(shí)現(xiàn)并不是開(kāi)源的,而且在實(shí)踐中也暴露出了一些問(wèn)題,所以在最新的 JDK 版本中已經(jīng)將它移除了。
在 Java 9 中,我們引入了 Compact Strings 的設(shè)計(jì),對(duì)字符串進(jìn)行了大刀闊斧的改進(jìn)。將數(shù)據(jù)存儲(chǔ)方式從 char 數(shù)組,改變?yōu)橐粋€(gè) byte 數(shù)組加上一個(gè)標(biāo)識(shí)編碼的所謂 coder ,并且將相關(guān)字符串操作類(lèi)都進(jìn)行了修改。另外,所有相關(guān)的 Intrinsic 之類(lèi)也都進(jìn)行了重寫(xiě),以保證沒(méi)有任何性能損失。
雖然底層實(shí)現(xiàn)發(fā)生了這么大的改變,但是 Java 字符串的行為并沒(méi)有任何大的變化,所以這個(gè)特性對(duì)于絕大部分應(yīng)用來(lái)說(shuō)是透明的,絕大部分情況不需要修改已有代碼。
當(dāng)然,在極端情況下,字符串也出現(xiàn)了一些能力退化,比如最大字符串的大小。你可以思考下,原來(lái) char 數(shù)組的實(shí)現(xiàn),字符串的最大長(zhǎng)度就是數(shù)組本身的長(zhǎng)度限制,但是替換成 byte 數(shù)組,同樣數(shù)組長(zhǎng)度下,存儲(chǔ)能力是退化了一倍的!還好這是存在于理論中的極限,還沒(méi)有發(fā)現(xiàn)現(xiàn)實(shí)應(yīng)用受此影響。
在通用的性能測(cè)試和產(chǎn)品實(shí)驗(yàn)中,我們能非常明顯地看到緊湊字符串帶來(lái)的優(yōu)勢(shì), 即更小的內(nèi)存占用、更快的操作速度 。
今天我從 String 、 StringBufer 和 StringBuilder 的主要設(shè)計(jì)和實(shí)現(xiàn)特點(diǎn)開(kāi)始,分析了字符串緩存的 intern 機(jī)制、非代碼侵入性的虛擬機(jī)層面排重、 Java 9 中緊湊字符的改進(jìn),并且初步接觸了 JVM 的底層優(yōu)化機(jī)制 intrinsic 。從實(shí)踐的角度,不管是 Compact Strings 還是底層 intrinsic 優(yōu)化,都說(shuō)明了使用 Java 基礎(chǔ)類(lèi)庫(kù)的優(yōu)勢(shì),它們往往能夠得到最大程度、最高質(zhì)量的優(yōu)化,而且只要升級(jí) JDK 版本,就能零成本地享受這些益處。