JVM 中的StringTable

萌新J發表於2021-03-04

是什麼

字串常量池是 JVM 中的一個重要結構,用於儲存JVM執行時產生的字串。在JDK7之前在方法區中,儲存的是字串常量。而字串常量池在 JDK7 開始移入堆中,隨之而來的是除了儲存字串常量外,還可以儲存字串引用(因為在堆中,引用堆中的字串常量很方便,所以可以儲存引用)。這使得很多字串的操作在 JDK7 中和在之前的版本中執行是不同的結果。這也是為什麼字串相關的問題是如此具有迷惑性的原因之一。

 

底層

String:在 JDK9 之前,String 底層是使用 char 陣列來儲存字串資料的,而在 JDK9 開始,使用 byte 陣列+編碼來代替 char 陣列,這是為了節省空間,因為不同編碼的資料佔空間不一樣,很多單位資料只需要一個 byte(8位元組) 就可以儲存,而使用 char(16位元組)就會浪費多餘的空間。

字串常量池:底層使用 HashTable 來儲存字串,在 JDK6 HashTable 的陣列長度是1006,JDK7 開始變成了 60013,這是為了避免儲存字串過多導致連結串列長度過長從而查詢效率降低。可以使用引數 -XX:StringTableSize= 來設定 StringTable 陣列的長度。

 

常見問題

字串相加

1、對於字串常量相加,編譯器會優化成直接相加。

如 String ss = "a" + "b",在編譯器的優化下,實際上只會建立一個 "ab" 字串。

而 final String s1 = "a"; String s2 = s1+"b",除了建立字串 "a" 外,只會建立 "ab"。

操作相關字串如下:

可以看到只對字串 "a"、"ab" 進行了入池操作(ldc)

2、對於包含字串變數的相加,不會在字串常量池中建立對應的字串。
如 String s1 = "a"; String s2 = s1 + "b",執行完後字串常量池中只會包含 "a"、"b" 字串。

對於 s1 + "b",下面是其位元組碼操作

可以看到,相加操作實際上是呼叫 StringBuilder 的append 方法進行字串拼接,然後呼叫它的 toString 方法獲取返回值儲存輸出,期間並沒有入池操作(ldc)。

 

由此得出的優化建議:因為每次執行一次包含非常量的字串相加時,都進行了一次 StringBuilder 物件的建立,所以如果需要多次連線,可以直接建立 StringBuilder 物件,使用一個 StringBuilder 物件進行字串拼接,避免建立多個物件降低效率。


物件建立數量

物件,包括 new 的物件以及字串物件。

1、對於String ss = new String ("ab"),這個過程首先會在會在字串常量池中建立一個 "ab" 字串常量,然後再在堆上建立一個 new String() 的物件,在這個物件中會儲存常量池中 "ab" 的地址資訊,最後在棧上建立一個區域性變數 ss ,儲存堆中建立的物件地址。所以全程建立了堆中的一個物件和字串常量池中的一個物件。

2、new String("a") + new String("b")。嚴格來看,建立了六個物件。

首先new String("a") 和 new String("b") ,分為建立了兩個物件。兩者相加時,會建立一個 StringBuilder 物件,而在 StringBuilder.toString()方法中,也會建立一個 String 物件

 3、String s1 = "a", String s2 = "b",  String s3 = "a" + "b" + s1 + "c" + s2; 對應的位元組碼如下:

字串常量池中會有四個字串物件,分別是 "a"、"b"、"ab"、"c"。在開始因為 s1、s2 的賦值,會將 "a"、"b" 分別加入字串常量池,然後執行第三步,執行順序是從左到右,首先執行 "a" + "b" ,因為兩個都是常量,所以會因為編譯器的優化直接返回 "ab",並且因為計算的兩個引數都是常量,所以直接加入字串常量池,隨後因為與變數 s1 相加,所以呼叫 StringBuilder的append 方法,得到的結果儲存到區域性變數表中,所以引入常量 "c",因為是常量,所以還是會引入字串常量池,然後與前面拼接得到的結果再次拼接,最後再與變數 s2 相加,因為不是常量所以還是不會將結果加入字串常量池。

 

除此之外,還需要注意,上面三種情況是在初始情況下,也就是字串常量池中沒有要加入的字串時的場景,如果字串常量池中預先就包含要加入的字串,那麼就會直接將常量池中的對應的字串地址返回給呼叫方。比如 String s1 = "a",在常量池中沒有 "a" 時,建立的物件是 1個,而如果常量池中已經存在,那麼就會將其地址直接返回賦給 s1。那麼建立的物件就是 0個了。

 

intern() 與字串相等判斷

intern() 方法是 String 類的一個native方法,作用是嘗試將呼叫這個方法的字串物件加入字串常量池中,然後返回常量池中儲存的值。在開頭說過,在 JDK7 開始字串常量池可以儲存字串引用,導致字串操作的過程可能會之前不一樣,從而得到不同的結果。

intern() 方法的執行:

1.6 及之前:嘗試將當前字串常量加入常量池,如果常量池存在就返回地址值;如果不存在就先加入常量池,然後再返回加入位置的地址值。

1.7開始:嘗試將當前字串常量加入常量池,如果存在就將返回地址值;如果不存在就存入當前 String 字串的地址值。

下面以一個例子來解釋一下,在JDK7和JDK7之前下面程式碼執行分別是什麼結果。

 1     @Test
 2     public void test1(){
 3         String s = new String("1");
 4         s.intern();
 5         String s2 = "1";
 6         System.out.println(s == s2);
 7 
 8 
 9         String s3 = new String("1") + new String("1");
10         s3.intern();
11         String s4 = "11";
12         System.out.println(s3 == s4);
13     }    

先說結論:

JDK7 之前: false、false。

JDK7 及之後:false、true。

原因:

1、首先先看上面 3------6 行的,首先,第三行會在字串常量池中新增 "1" ,然後在堆中建立一個物件,儲存 "1" 在常量池中的地址,再在區域性變數表中新增一個 s 儲存堆中物件的地址。隨後執行第四行,此時 s 指向的字串已經在常量池中了,所以這一步無效,第五行因為常量池已經存在 "1" ,所以 JDK7或之前執行的邏輯是一樣的,直接將 "1" 在常量池中的地址返回給 s2。然後判斷,s 指向的是堆中的物件,而 s2 指向的是常量池中的字串常量,所以無論是 JDK7 還是之前的都是 false。

2、然後再看下面 9-----12 行。因為前面已經在常量池中新增 "1",所以第9行會直接返回地址,然後執行新增操作,建立字串 "11",此時並沒有新增到常量池,然後執行第10行,因為常量池不存在 "11",所以 JDK7 之前直接加入常量池,JDK7 及以後則直接將 "11" 的地址存入常量池,而 s3 則不變,還是儲存的是常量池外的那個 "11" 的地址值。然後執行 11 行,因為常量池已存在 "11",所以 s4 就是返回 "11" 的地址值,不同的是在 JDK7 之前因為常量池儲存的是 "11" 常量,所以返回的是常量池中的地址值;而 JDK7 及以後常量池儲存的是常量池外的 "11" 的地址值,所以返回的是池外的地址值。所以最後判斷在 JDK7 之前是 false,而在 JDK7 開始是 true。

相關文章