對於String類型,java官網的文檔是這樣子描述的:
String類代表着字符串。java程序中的所有字符串字面值(如"abc")都作為此類的實例實現。
字符串是常量,他們的值在創建之后不能更改。因為 String 對象是不可變的,所以可以共享。
那么,jvm是怎么共享這些字符串的呢?
為了節省內存,提高資源的復用,jvm引入了常量池這個概念,它屬於方法區的一部分的,作用之一就是存放編譯期間生產的各種字面量和符號引用。而從前面的博文《深入了解JVM—內存區域》我們可以知道,方法區的垃圾回收行為是比較少出現的,該區中的對象基本不會被回收,可以理解成是永久存在的。
因此,緩存在字符串緩沖區中的字符串對象基本是不被回收的,而jvm也正是通過復用這些對象從而達到共享作用。
從上一段話中的概念可以知道,一般情況下,只有編譯期間可以確定下來的的字符串才能存放到緩沖區中。為什么要強調是一般情況下呢?因為String類為我們提供了一個intern()方法,它可以幫我們將不存在於緩存池中的java字符串添加到緩存池中,並返回緩存池中該字符串對象的引用。
具體關於intern()方法,后面我們再給出代碼做簡單說明吧。現在我們將重點放在,什么情況下能夠在編譯期間直接確定字符串變量值並且將它添加到緩沖區中呢?
如果程序的字符串連接表達式中沒有使用變量或者調用方法,那么該字符串變量的值就能夠在編譯期間確定下來,並且將該字符換緩存在緩沖區中,同時讓該變量指向該字符串;否則將無法利用緩沖區,因為使用了變量和調用了方法之后的字符串變量的值只能在運行期間才能確定連接式的值,也就無法在編譯期間確定字符串變量的值,從而無法將字符串變量增加到緩沖區並加以利用。
下面我們來看看如何通過代碼及其編譯過程來驗證上述結論吧。
代碼一(沒有使用變量或者調用方法):
1 package com.xiaoxuetu.string; 2 3 public class Test { 4 5 public static void main(String args[]) { 6 String param1 = "abc"; 7 String param2 = "abc" + "def"; 8 String param3 = "abcdef"; 9 } 10 }
首先我們打開cmd.exe, 通過javac Application.java編譯好該Java文件,然后通過命令javap -c Application來查看java編譯后的ByteCode字節碼,如圖所示:
我們先來解釋下,ldc的含義是:將常量值從常量池中取出來並且壓入棧中。從上圖中,我們可以看到第0行、第3行和第6行中,程序分別從常量池中取出 "abc" 和 "abcdef" 並且壓入棧中,而且,第3行和第6行中的字符串引用是同一個。這說明了,在編譯期間,該字符串變量的值已經確定了下來,並且將該字符串值緩存在緩沖區中,同時讓該變量指向該字符串值,后面如果有使用相同的字符串值,則繼續指向同一個字符串值。
如果有安裝jad的話,我們還可以通過jad -o -a Test.class命令來生成java代碼和對應java編譯后的ByteCode字節碼一起的jad文件,如圖所示:
代碼二(使用了變量或者調用了方法):
1 package com.xiaoxuetu.string; 2 3 public class Application { 4 public static void main(String[] args) { 5 String param = "abc"; 6 String param1 = "3abc"; 7 String param2 = param.length() + "abc"; 8 } 9 }
同樣,我們編譯后用jad命令來生成對應的文件查看比較方便吧。
從上圖中,我們看到了param的值引用是從常量池中取出的字符串"abc", param1的引用也是直接從常量池中取出的"3abc";但是param2的值並沒有根據運算結果引用常量池中的“3abc”,而是返回調用當前StringBuilder對象的toString()后的生成的字符串引用。這點我們可以直接通過 param2 == param1 來判斷,很明顯輸出結果就是false.
除此之外,我們還可以從該圖中的第27行到第37行看出,javas在處理param2 = param.length() + "abc"的時候,是通過StringBuilder實例對象的append()方法來實現的。返回的是StringBuilder對象的引用,所以此時param2的值並沒有引用常量池中緩存的也有的對象。對此,官網文檔是這么解釋的:
Java 語言提供對字符串串聯符號("+")以及將其他對象轉換為字符串的特殊支持。字符串串聯是通過 StringBuilder(或 StringBuffer)類及其 append 方法實現的。字符串轉換是通過 toString 方法實現的,該方法由 Object 類定義,並可被 Java 中的所有類繼承。有關字符串串聯和轉換的更多信息,請參閱 Gosling、Joy 和 Steele 合著的 The Java Language Specification。
或許有人看了以后會有以下疑問:
1> 如果將前面代碼二中的第6 、7行交換,變成如下:
1 String param2 = param.length() + "abc"; 2 String param1 = "3abc";
那么param2變量的值“3abc”會不會緩存,然后被param1直接取出來使用呢?
答案是不會的,因為param2變量的字符串值必須在運行時才能確定下來,而不是概念中編譯期間,真正將"3abc"緩存的反而會是param1這行代碼。
2>如果我們通過String newStr = new String("abc");來創建字符串變量,那么abc會不會被緩存呢?而且會不會直接指向緩沖區中的變量呢?
好吧,我們繼續看看代碼然后通過查看編譯消息進行分析:
代碼三(使用了變量或者調用了方法):
1 package com.xiaoxuetu.string; 2 3 public class Application2 { 4 public static void main(String[] args) { 5 String param = "abc"; 6 String newStr = new String("cde"); 7 String param2 = "cde"; 8 9 } 10 }
接着查看jad命令執行后生成的文件:
我們看到在創建newStr的String類型對象的時候,先從棧中取出字符串"cde",然后調用String的構造方法通過關鍵字new 進行創建對象的創建,將新的引用賦給newStr。因此newStr並沒有指向緩沖區中的字符串“cde”,所以通過這種方法創建的字符串變量開銷往往比較大。
接下來我們講解一下intern()方法吧。關於這個方法,官網是這么描述的:
當調用 intern 方法時,如果池已經包含一個等於此 String 對象的字符串(用 equals(Object) 方法確定),則返回池中的字符串。否則,將此 String 對象添加到池中,並返回此 String 對象的引用。
下面我們只給出一個intern()方法使用的例子,具體大家就自行研究咯。
1 package com.xiaoxuetu.string; 2 3 public class InternTest { 4 5 public static void main(String[] args) { 6 String param = "abc"; 7 String newStr = new String("abc"); 8 String param2 = new String("abc"); 9 newStr.intern(); 10 param2 = param2.intern(); //param2指向intern返回的常量池中的引用 11 System.out.println(param == newStr); //false 12 System.out.println(param == param2); //true 13 } 14 }
文筆表達能力有限,可能寫的比較一般。不知道大家看了之后會不會有其他問題哦,希望大家踴躍提出,共同學習共同進步。謝謝。
最后就總結一下判斷字符串是否被緩存到緩沖區的兩大要素 :
1>編譯期間 : 也就說字符串連接式中沒有使用變量或者調用方法。
2>是否使用了intern()方法 : 使用了該方法的字符串變量的值如果不存在緩沖區中將會被緩存。
