天天看點

StingBuffer 和 StringBuilder 的差別

參考文獻:
  1. 極客時間《Java核心技術面試精講》
  2. OpenJDK / JDK

為什麼會有 StringBuffer 類?

String 是一個典型的 Immutable 類,被聲明為 final class,所有的屬性也都是 final 的。具有不可變性,類似拼接、裁剪字元串等動作,都會産生新的 String 對象。由于字元串操作的普遍性,所有相關操作的效率往往對應用性能有明顯的影響。

StringBuffer 是為解決這個問題而提供的一個類,使用 append 或者 add 方法,把字元串竄添加到已有序列的末尾或者指定位置。

StringBuffer 和 StringBuilder 的差別

StringBuffer 本質是一個線程安全的可修改字元序列,它保證了線程安全,也随之帶來了額外的性能開銷,是以除非有線程安全的需要,不然還是推薦使用它的後繼者,也就是 StringBuilder。

StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 沒有本質差別,但是它去掉了線程安全的部分,有效減小了開銷,是絕大部分情況下進行字元串拼接的首選。

StringBuffer 和 StringBuilder 底層都是利用可修改的(char,JDK 9 以後是 byte)數組,二者都繼承了 AbstractStringBuilder,裡面包含了基本操作,差別僅在與最終的方法是否加了 synchronized。

在聲明字元串時,對字元串長度了解和如何設計?

在建立内部數組時,如果太小,拼接的時候可能要重新建立足夠大的數組;如果太大,又會浪費空間。目前的實作是,建構時初始字元串長度加 16(這意味着如果沒有建構對象時輸入最初的字元串,那麼初始值就是 16)。我們如果确定拼接會發生非常多次,而且大概是可預計的,那麼就可以指定合适的大小,避免很多次擴容的開銷。擴容會産生多重開銷,因為要抛棄原有數組,建立新的數組,還要進行 arraycopy。

在沒有線程安全的情況下,全部拼接操作是都應該用 StringBuilder 實作嗎?

非靜态的拼接邏輯在 JDK8 中會自動被 javac 轉換為 StringBuilder 操作;而在 JDK9 裡面,則是展現了思路的變化。 Java9 利用 InvokeDynamic,将字元串拼接的優化與 javac 生成的位元組碼結構,假設未來 JVM 增強相關運作時實作,将不需要依賴 javac 的任何修改。

在日常變成過程中,保證程式的可讀性、可維護性,往往比所謂的最優性能跟重要,你可以根據實際需求酌情選擇具體的編碼方式。

字元串緩存的優化

把常見應用進行堆轉儲(Dump Heap),然後分析對象組成,會發現平均 25% 的對象是字元串,并且其中約半數是重複的。如果能避免建立重複字元串,可以有效降低記憶體消耗和對象開銷。

String 在 Java6 以後提供了 intern() 方法,目的是提示 JVM 把響應字元串緩存起來,以備重複使用。在我們建立字元串并調用 intern() 方法的時候,如果已經有緩存的字元串,就會傳回緩存裡的執行個體,否則将其緩存起來。一般來說,JVM 會将所有類似“abc”這樣的文本字元串,或者字元串常量之類緩存起來。

然而上面的處理方法并不理想,被緩存的字元串是存在所謂的 PermGen 裡的,這個空間是很有限的,也基本不會被 FullGC 之外的垃圾收集器照顧到,是以如果使用不當,會經常出現 OOM。

在後續版本中,這個緩存被放置在堆中,這樣就極大避免了永久代占滿的問題,甚至永久代在 JDK8 中被 MetaSpace(中繼資料區)替代了。而且預設緩存大小也在不斷地擴大中,從最初的 1009,到 7u40 以後被修改為 60013。以使用下面的參數直接列印具體數字,可以拿自己的 JDK 立刻試驗一下。

-XX:+PrintStringTableStatistics      

你也可以使用下面的 JVM 參數手動調整大小,但是絕大部分情況下并不需要調整,除非你确定它的大小已經影響了操作效率。

-XX:StringTableSize=N      

Intern 是一種顯示地排重機制,但是它也有一定的副作用,因為需要開發者寫代碼時明确調用,一是不友善,每一個都顯示調用是非常麻煩的;另外就是我們很難保證效率,應用開發階段很難清楚地預計字元串的重複情況,有人認為這是一種污染代碼的實踐。

幸好在 Oracle JDK 8u20 之後,推出了一個新特性,也就是 G1 GC 下的字元串排重。它是通過将相同資料的字元串指向同一份資料來做到的,是 JVM 底層的改變,并不需要 Java 類庫做什麼修改。這個功能是預設關閉的,需要使用下面的參數開啟,并且記得指定使用 G1 GC:

-XX:+UseStringDeduplication      

前面說到的幾個方面,隻是 Java 底層對字元串各種優化的一角,在運作時,字元串的一些基礎操作會直接利用 JVM 内部的 Intrinsic 機制,往往運作的就是特殊優化的本地代碼,而根本就不是 Java 代碼生成的位元組碼。Intrinsic 可以簡單了解為,是一種利用 native 方式 hard-coded 的邏輯,算是一種特别的内聯,很多優化是需要直接使用特定的 CPU 指令,具體可以看相關的

源碼

String 自身的演化

在曆史版本中,字元串使用 char 數組來存資料,這樣非常直接。但是 Java 中的 char 是兩個 bytes 大小,拉丁語系語言的字元,根本就不需要太寬的 char,這樣無差別的實作就造成了一定的浪費。密度是程式設計語言平台永恒的話題,因為歸根接地絕大部分任務是要來操作資料的。

其實在 Java9 中,我們引入了 Compact Stings 的設計,對字元串進行了大刀闊斧的改進。将資料存儲方式從 char 數組,改變為一個 byte 數組加上一個辨別編碼的所謂 coder,并且将相關字元串操作類都進行了修改。另外,所有相關的 Instinct 之類也都進行了重寫,以保證沒有任何性能損失。

雖然底層發生了這麼大的改變,但是 Java 字元串的行為并沒有任何打的變化,是以這個特性對絕大部分應用來說是透明的,絕大部分情況不需要修改已有代碼。

當然,在極端情況下,字元串也出現一些能力退化,比如最大字元串的大小。原來 char 數組的實作,字元串的最大長度就是數組本身的長度限制,但是替換成 byte 數組,同樣數組長度下,存儲能力是退化了一倍的。還好這是存在于理論中的極限,還沒有發現現實應用受此影響。

在通用的性能測試和産品實驗中,我們能非常明顯地看到緊湊字元串帶來的優勢,即更小的記憶體占用、更快的操作速度。