字符串常量池是 JVM 中的一個重要結構,用於存儲JVM運行時產生的字符串。在JDK7以前在方法區中,存儲的是字符串常量。而字符串常量池在 JDK7 開始移入堆中,隨之而來的是除了存儲字符串常量外,還能夠存儲字符串引用(由於在堆中,引用堆中的字符串常量很方便,因此能夠存儲引用)。這使得不少字符串的操做在 JDK7 中和在以前的版本中執行是不一樣的結果。這也是爲何字符串相關的問題是如此具備迷惑性的緣由之一。數組
String:在 JDK9 以前,String 底層是使用 char 數組來存儲字符串數據的,而在 JDK9 開始,使用 byte 數組+編碼來代替 char 數組,這是爲了節省空間,由於不一樣編碼的數據佔空間不同,不少單位數據只須要一個 byte(8字節) 就能夠存儲,而使用 char(16字節)就會浪費多餘的空間。app
字符串常量池:底層使用 HashTable 來存儲字符串,在 JDK6 HashTable 的數組長度是1006,JDK7 開始變成了 60013,這是爲了不存儲字符串過多致使鏈表長度過長從而查詢效率下降。可使用參數 -XX:StringTableSize= 來設置 StringTable 數組的長度。優化
一、對於字符串常量相加,編譯器會優化成直接相加。ui
如 String ss = "a" + "b",在編譯器的優化下,實際上只會建立一個 "ab" 字符串。編碼
而 final String s1 = "a"; String s2 = s1+"b",除了建立字符串 "a" 外,只會建立 "ab"。spa
操做相關字符串以下:3d
能夠看到只對字符串 "a"、"ab" 進行了入池操做(ldc)code
二、對於包含字符串變量的相加,不會在字符串常量池中建立對應的字符串。
如 String s1 = "a"; String s2 = s1 + "b",執行完後字符串常量池中只會包含 "a"、"b" 字符串。對象
對於 s1 + "b",下面是其字節碼操做blog
能夠看到,相加操做其實是調用 StringBuilder 的append 方法進行字符串拼接,而後調用它的 toString 方法獲取返回值保存輸出,期間並無入池操做(ldc)。
由此得出的優化建議:由於每次執行一次包含很是量的字符串相加時,都進行了一次 StringBuilder 對象的建立,因此若是須要屢次鏈接,能夠直接建立 StringBuilder 對象,使用一個 StringBuilder 對象進行字符串拼接,避免建立多個對象下降效率。
對象,包括 new 的對象以及字符串對象。
一、對於String ss = new String ("ab"),這個過程首先會在會在字符串常量池中建立一個 "ab" 字符串常量,而後再在堆上建立一個 new String() 的對象,在這個對象中會保存常量池中 "ab" 的地址信息,最後在棧上建立一個局部變量 ss ,保存堆中建立的對象地址。因此全程建立了堆中的一個對象和字符串常量池中的一個對象。
二、new String("a") + new String("b")。嚴格來看,建立了六個對象。
首先new String("a") 和 new String("b") ,分爲建立了兩個對象。二者相加時,會建立一個 StringBuilder 對象,而在 StringBuilder.toString()方法中,也會建立一個 String 對象
三、String s1 = "a", String s2 = "b", String s3 = "a" + "b" + s1 + "c" + s2; 對應的字節碼以下:
字符串常量池中會有四個字符串對象,分別是 "a"、"b"、"ab"、"c"。在開始由於 s一、s2 的賦值,會將 "a"、"b" 分別加入字符串常量池,而後執行第三步,運行順序是從左到右,首先執行 "a" + "b" ,由於兩個都是常量,因此會由於編譯器的優化直接返回 "ab",而且由於計算的兩個參數都是常量,因此直接加入字符串常量池,隨後由於與變量 s1 相加,因此調用 StringBuilder的append 方法,獲得的結果保存到局部變量表中,因此引入常量 "c",由於是常量,因此仍是會引入字符串常量池,而後與前面拼接獲得的結果再次拼接,最後再與變量 s2 相加,由於不是常量因此仍是不會將結果加入字符串常量池。
除此以外,還須要注意,上面三種狀況是在初始狀況下,也就是字符串常量池中沒有要加入的字符串時的場景,若是字符串常量池中預先就包含要加入的字符串,那麼就會直接將常量池中的對應的字符串地址返回給調用方。好比 String s1 = "a",在常量池中沒有 "a" 時,建立的對象是 1個,而若是常量池中已經存在,那麼就會將其地址直接返回賦給 s1。那麼建立的對象就是 0個了。
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。
緣由:
一、首先先看上面 3------6 行的,首先,第三行會在字符串常量池中添加 "1" ,而後在堆中建立一個對象,保存 "1" 在常量池中的地址,再在局部變量表中添加一個 s 保存堆中對象的地址。隨後執行第四行,此時 s 指向的字符串已經在常量池中了,因此這一步無效,第五行由於常量池已經存在 "1" ,因此 JDK7或以前執行的邏輯是同樣的,直接將 "1" 在常量池中的地址返回給 s2。而後判斷,s 指向的是堆中的對象,而 s2 指向的是常量池中的字符串常量,因此不管是 JDK7 仍是以前的都是 false。
二、而後再看下面 9-----12 行。由於前面已經在常量池中添加 "1",因此第9行會直接返回地址,而後執行添加操做,建立字符串 "11",此時並無添加到常量池,而後執行第10行,由於常量池不存在 "11",因此 JDK7 以前直接加入常量池,JDK7 及之後則直接將 "11" 的地址存入常量池,而 s3 則不變,仍是保存的是常量池外的那個 "11" 的地址值。而後執行 11 行,由於常量池已存在 "11",因此 s4 就是返回 "11" 的地址值,不一樣的是在 JDK7 以前由於常量池保存的是 "11" 常量,因此返回的是常量池中的地址值;而 JDK7 及之後常量池保存的是常量池外的 "11" 的地址值,因此返回的是池外的地址值。因此最後判斷在 JDK7 以前是 false,而在 JDK7 開始是 true。