JDK9為何要將String的底層實現由char[]改成了byte[]

語言: CN / TW / HK

關注公眾號:IT老哥,每天閱讀一篇乾貨技術文章,一年後你會發現一個不一樣的自己。

如果你不是 Java8 的釘子戶,你應該早就發現了:String 類的原始碼已經由 char[] 優化為了 byte[] 來儲存字串內容,為什麼要這樣做呢?

開門見山地說,從 char[] 到 byte[],最主要的目的是為了節省字串佔用的記憶體。記憶體佔用減少帶來的另外一個好處,就是 GC 次數也會減少。

一、為什麼要優化 String 節省記憶體空間

我們使用 jmap -histo:live pid | head -n 10 命令就可以檢視到堆內物件示例的統計資訊、檢視 ClassLoader 的資訊以及 finalizer 佇列。

以我正在執行著的程式設計喵喵專案例項(基於 Java 8)來說,結果是這樣的。

圖片

其中 String 物件有 17638 個,佔用了 423312 個位元組的記憶體,排在第三位。

由於 Java 8 的 String 內部實現仍然是 char[],所以我們可以看到記憶體佔用排在第 1 位的就是 char 陣列。

char[] 物件有 17673 個,佔用了 1621352 個位元組的記憶體,排在第一位。

那也就是說優化 String 節省記憶體空間是非常有必要的,如果是去優化一個使用頻率沒有 String 這麼高的類庫,就顯得非常的雞肋。

二、byte[] 為什麼就能節省記憶體空間呢?

眾所周知,char 型別的資料在 JVM 中是佔用兩個位元組的,並且使用的是 UTF-8 編碼,其值範圍在 '\u0000'(0)和 '\uffff'(65,535)(包含)之間。

也就是說,使用 char[] 來表示 String 就導致了即使 String 中的字元只用一個位元組就能表示,也得佔用兩個位元組。

而實際開發中,單位元組的字元使用頻率仍然要高於雙位元組的。

當然了,僅僅將 char[] 優化為 byte[] 是不夠的,還要配合 Latin-1 的編碼方式,該編碼方式是用單個位元組來表示字元的,這樣就比 UTF-8 編碼節省了更多的空間。

換句話說,對於:

``` String name = "jack";  

```

這樣的,使用 Latin-1 編碼,佔用 4 個位元組就夠了。

但對於:

``` String name = "小二";  

```

這種,木的辦法,只能使用 UTF16 來編碼。

針對 JDK 9 的 String 原始碼裡,為了區別編碼方式,追加了一個 coder 欄位來區分。

``` /*    * The identifier of the encoding used to encode the bytes in    * {@code value}. The supported values in this implementation are    *    * LATIN1    * UTF16    *    * @implNote This field is trusted by the VM, and is a subject to    * constant folding if String instance is constant. Overwriting this    * field after construction will cause problems.    /   private final byte coder;  

```

Java 會根據字串的內容自動設定為相應的編碼,要麼 Latin-1 要麼 UTF16。

也就是說,從 char[] 到 byte[]中文是兩個位元組,純英文是一個位元組,在此之前呢,中文是兩個位元組,英文也是兩個位元組

三、為什麼用UTF-16而不用UTF-8呢?

在 UTF-8 中,0-127 號的字元用 1 個位元組來表示,使用和 ASCII 相同的編碼。只有 128 號及以上的字元才用 2 個、3 個或者 4 個位元組來表示。

  • 如果只有一個位元組,那麼最高的位元位為 0;

  • 如果有多個位元組,那麼第一個位元組從最高位開始,連續有幾個位元位的值為 1,就使用幾個位元組編碼,剩下的位元組均以 10 開頭。

具體的表現形式為:

  • 0xxxxxxx:一個位元組;

  • 110xxxxx 10xxxxxx:兩個位元組編碼形式(開始兩個 1);- 1110xxxx 10xxxxxx 10xxxxxx:三位元組編碼形式(開始三個 1);

  • 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx:四位元組編碼形式(開始四個 1)。

也就是說,UTF-8 是變長的,那對於 String 這種有隨機訪問方法的類來說,就很不方便。所謂的隨機訪問,就是charAt、subString這種方法,隨便指定一個數字,String要能給出結果。如果字串中的每個字元佔用的記憶體是不定長的,那麼進行隨機訪問的時候,就需要從頭開始數每個字元的長度,才能找到你想要的字元。

那有小夥伴可能會問,UTF-16也是變長的呢?一個字元還可能佔用 4 個位元組呢?

的確,UTF-16 使用 2 個或者 4 個位元組來儲存字元。

  • 對於 Unicode 編號範圍在 0 ~ FFFF 之間的字元,UTF-16 使用兩個位元組儲存。

  • 對於 Unicode 編號範圍在 10000 ~ 10FFFF 之間的字元,UTF-16 使用四個位元組儲存,具體來說就是:將字元編號的所有位元位分成兩部分,較高的一些位元位用一個值介於 D800~DBFF 之間的雙位元組儲存,較低的一些位元位(剩下的位元位)用一個值介於 DC00~DFFF 之間的雙位元組儲存。

但是在 Java 中,一個字元(char)就是 2 個位元組,佔 4 個位元組的字元,在 Java 裡也是用兩個 char 來儲存的,而String的各種操作,都是以Java的字元(char)為單位的,charAt是取得第幾個char,subString取的也是第幾個到第幾個char組成的子串,甚至length返回的都是char的個數。

所以UTF-16在Java的世界裡,就可以視為一個定長的編碼。

關注公眾號:IT老哥,每天閱讀一篇乾貨技術文章,一年後你會發現一個不一樣的自己。