String 既然能做效能調優,我直呼內行

碼哥位元組發表於2022-01-07
碼哥,String 還能優化啥?你是不是框我?

莫慌,今天給大家見識一下不一樣的 String,從根上拿捏直達 G 點。

並且碼哥分享一個例子:通過效能調優我們能實現百兆記憶體輕鬆儲存幾十 G 資料。

String物件是我們每天都「摸」的物件型別,但是她的效能問題我們卻總是忽略。

愛她,不能只會簡單一起玩耍,要深入瞭解String 的內心深處,做一個「心有猛虎,細嗅薔薇」的暖男。

通過以下幾點分析,我們一步步揭開她的衣裳,直達內心深處,提升一個 Level,讓 String 直接起飛:

  1. 字串物件的特性;
  2. String 的不可變性;
  3. 大字串構建技巧;
  4. String.intern 節省記憶體;
  5. 字串分割技巧;

String 身體解密

想要深入瞭解,就先從基本組成開始……

「String 締造者」對 String 物件做了大量優化來節省記憶體,從而提升 String 的效能:

Java 6 及之前

資料儲存在 char[]陣列中,String通過 offsetcount兩個屬性定位 char[] 資料獲取字串。

這樣可以高效快速的定位並共享陣列物件,並且節省記憶體,但是有可能導致記憶體洩漏。

共享 char 陣列為啥可能會導致記憶體洩漏呢?
String(int offset, int count, char value[]) {
    this.value = value;
    this.offset = offset;
    this.count = count;
}

public String substring(int beginIndex, int endIndex) {
    //check boundary
    return  new String(offset + beginIndex, endIndex - beginIndex, value);
}

呼叫 substring() 的時候雖然建立了新的字串,但字串的值 value 仍然指向的是記憶體中的同一個陣列,如下圖所示:

如果我們僅僅是用 substring 獲取一小段字元,而原始 string字串非常大的情況下,substring 的物件如果一直被引用。

此時 String 字串也無法回收,從而導致記憶體洩露。

如果有大量這種通過 substring 獲取超大字串中一小段字串的操作,會因為記憶體洩露而導致記憶體溢位。

JDK7、8

去掉了 offsetcount兩個變數,減少了 String 物件佔用的記憶體。

substring 原始碼:

public String(char value[], int offset, int count) {
    this.value = Arrays.copyOfRange(value, offset, offset + count);
}

public String substring(int beginIndex, int endIndex) {
    int subLen = endIndex - beginIndex;
    return new String(value, beginIndex, subLen);
}

substring() 通過 new String() 返回了一個新的字串物件,在建立新的物件時通過 Arrays.copyOfRange() 深度拷貝了一個新的字元陣列。

如下圖所示:

String.substring 方法不再共享 char[]陣列的資料,解決了可能記憶體洩漏的問題。

Java 9

char[]欄位改為 byte[],新增 coder屬性。

碼哥,為什麼這麼改呢?

一個 char 字元佔 2 個位元組,16 位。儲存單位元組編碼內的字元(佔一個位元組的字元)就顯得非常浪費。

為了節約記憶體空間,於是使用了 1 個位元組佔 8 位的 byte 陣列來存放字串。

勤儉節約的女神,誰不愛……

新屬性 coder 的作用是:在計算字串長度或者使用 indexOf()方法時,我們需要根據編碼型別來計算字串長度。

coder 的值分別表示不同編碼型別:

  • 0:表示使用 Latin-1 (單位元組編碼);
  • 1:使用UTF-16

String 的不可變性

瞭解了String 的基本組成之後,發現 String 還有一個比外在更性感的特性,她被 final關鍵字修飾,char 陣列也是。

我們知道類被 final 修飾代表該類不可繼承,而 char[]final+private 修飾,代表了 String 物件不可被更改。

String 物件一旦建立成功,就不能再對它進行改變

final 修飾的好處

安全性

當你在呼叫其他方法時,比如呼叫一些系統級操作指令之前,可能會有一系列校驗。

如果是可變類的話,可能在你校驗過後,它的內部的值又被改變了,這樣有可能會引起嚴重的系統崩潰問題。

高效能快取

String不可變之後就能保證 hash值得唯一性,使得類似 HashMap容器才能實現相應的 key-value 快取功能。

實現字串常量池

由於不可變,才得以實現字串常量池。

字串常量池指的是在建立字串的時候,先去「常量池」查詢是否建立過該「字串」;

如果有,則不會開闢新空間建立字串,而是直接把常量池中該字串的引用返回給此物件。

建立字串的兩種方式:

  • String str1 = “碼哥位元組”;
  • String str2 = new String(“碼哥位元組”);

當程式碼中使用第一種方式建立字串物件時,JVM 首先會檢查該物件是否在字串常量池中,如果在,就返回該物件引用。

否則新的字串將在常量池中被建立,並返回該引用。

這樣可以減少同一個值的字串物件的重複建立,節約記憶體

第二種方式建立,在編譯類檔案時,"碼哥位元組" 字串將會放入到常量結構中,在類載入時,“碼哥位元組" 將會在常量池中建立;

在呼叫 new 時,JVM 命令將會呼叫 String 的建構函式,在堆記憶體中建立一個 String 物件,同時該物件指向「常量池」中的“碼哥位元組”字串,str 指向剛剛在堆上建立的 String 物件;

如下圖:

什麼是物件和物件引用呀?

str 屬於方法棧的字面量,它指向堆中的 String 物件,並不是物件本。

物件在記憶體中是一塊記憶體地址,str 則是指向這個記憶體地址的引用。

也就是說 str 並不是物件,而只是一個物件引用。

碼哥,字串的不可變到底指的是什麼呀?
String str = "Java";
str = "Java,yyds"

第一次賦值 「Java」,第二次賦值「Java,yyds」,str 值確實改變了,為什麼我還說 String 物件不可變呢?

這是因為 str 只是 String 物件的引用,並不是物件本身。

真正的物件依然還在記憶體中,沒有被改變。

優化實戰

瞭解了 String 的物件實現原理和特性,是時候要深入女神內心,結合實際場景,如何更上一層樓優化 String 物件的使用。

大字串如何構建

既然 String 物件是不可變,所以我們在頻繁拼接字串的時候是否意味著建立多個物件呢?

String str = "癩蛤蟆撩青蛙" + "長的醜" + "玩的花";

是不是以為先生成「癩蛤蟆撩青蛙」物件,再生成「癩蛤蟆撩青蛙長的醜」物件,最後生成「癩蛤蟆撩青蛙長得醜玩的花」物件。

實際執行中,只有一個物件生成。

這是為什麼呢?

雖然程式碼寫的醜陋,但是編譯器自動優化了程式碼。

再看下面例子:

String str = "小青蛙";

for(int i=0; i<1000; i++) {
     str += i;
}

上面的程式碼編譯後,你可以看到編譯器同樣對這段程式碼進行了優化。

Java 在進行字串的拼接時,偏向使用 StringBuilder,這樣可以提高程式的效率。

String str = "小青蛙";

for(int i=0; i<1000; i++) {
            str = (new StringBuilder(String.valueOf(str))).append(i).toString();
}

即使如此,還是迴圈內重複建立 StringBuilder物件。

敲黑板

所以做字串拼接的時候,我建議你還是要顯示地使用 String Builder 來提升系統效能。

如果在多執行緒程式設計中,String 物件的拼接涉及到執行緒安全,你可以使用 StringBuffer。

運用 intern 節省記憶體

直接看intern() 方法的定義與原始碼:

intern() 是一個本地方法,它的定義中說的是,當呼叫 intern 方法時,如果字串常量池中已經包含此字串,則直接返回此字串的引用。

否則將此字串新增到常量池中,並返回字串的引用。

如果不包含此字串,先將字串新增到常量池中,再返回此物件的引用。

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

Twitter 工程師曾分享過一個 String.intern() 的使用示例,Twitter 每次釋出訊息狀態的時候,都會產生一個地址資訊,以當時 Twitter 使用者的規模預估,伺服器需要 20G 的記憶體來儲存地址資訊。

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

考慮到其中有很多使用者在地址資訊上是有重合的,比如,國家、省份、城市等,這時就可以將這部分資訊單獨列出一個類,以減少重複,程式碼如下:

public class SharedLocation {

  private String city;
  private String region;
  private String countryCode;
}

public class Location {

  private SharedLocation sharedLocation;
  double longitude;
  double latitude;
}

通過優化,資料儲存大小減到了 20G 左右。

但對於記憶體儲存這個資料來說,依然很大,怎麼辦呢?

Twitter 工程師使用 String.intern() 使重複性非常高的地址資訊儲存大小從 20G 降到幾百兆,從而優化了 String 物件的儲存。

核心程式碼如下:

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

弄個簡單例子方便理解:

String a =new String("abc").intern();
String b = new String("abc").intern();

System.out.print(a==b);

輸出結果:true

在載入類的時候會在常量池中建立一個字串物件,內容是「abc」。

建立區域性 a 變數時,呼叫 new Sting() 會在堆記憶體中建立一個 String 物件,String 物件中的 char 陣列將會引用常量池中字串。

在呼叫 intern 方法之後,會去常量池中查詢是否有等於該字串物件的引用,有就返回引用。

建立 b 變數時,呼叫 new Sting() 會在堆記憶體中建立一個 String 物件,String 物件中的 char 陣列將會引用常量池中字串。

在呼叫 intern 方法之後,會去常量池中查詢是否有等於該字串物件的引用,有就返回引用給區域性變數。

而剛在堆記憶體中的兩個物件,由於沒有引用指向它,將會被垃圾回收。

所以 a 和 b 引用的是同一個物件。

字串分割有妙招

Split() 方法使用了正規表示式實現了其強大的分割功能,而正規表示式的效能是非常不穩定的。

使用不恰當會引起回溯問題,很可能導致 CPU 居高不下。

Java 正規表示式使用的引擎實現是 NFA(Non deterministic Finite Automaton,確定型有窮自動機)自動機,這種正規表示式引擎在進行字元匹配時會發生回溯(backtracking),而一旦發生回溯,那其消耗的時間就會變得很長,有可能是幾分鐘,也有可能是幾個小時,時間長短取決於回溯的次數和複雜度。

所以我們應該慎重使用 Split() 方法,我們可以用String.indexOf()方法代替 Split() 方法完成字串的分割。

總結與思考

我們從 String 進化歷程掌握了她的組成,不斷的改變成員變數節約記憶體。

她的不可變性從而實現了字串常量池,減少同一個字串的重複建立,節約記憶體。

但也是因為這個特性,我們在做長字串拼接時,需要顯示使用 StringBuilder,以提高字串的拼接效能。

最後,在優化方面,我們還可以使用 intern 方法,讓變數字串物件重複使用常量池中相同值的物件,進而節約記憶體。

最後,出一個問題給大家,歡迎在評論區留言,點贊對多的將獲取碼哥贈送的書籍。

通過三種不同的方式建立了三個物件,再依次兩兩匹配,每組被匹配的兩個物件是否相等?程式碼如下:

String str1 = "abc";
String str2 = new String("abc");
String str3 = str2.intern();
assertSame(str1 == str2);
assertSame(str2 == str3);
assertSame(str1 == str3)

公zhong號後臺回覆:「String」獲取答案。

相關文章