深入Java原始碼剖析之字串常量
字串在Java生產開發中的使用頻率是非常高的,可見,字串對於我們而言非常關鍵。那麼從C語言過來的同學會發現,在C中是沒有String型別的,那麼C語言要想實現字串就必須使用char陣列,通過一個個的字元來組拼成字串。
Java中是如何實現字串的
那其實在Java中,關於字串的實現,其實用的也是char陣列,這可以從原始碼中得到體現。
/**
* Initializes a newly created {@code String} object so that it represents
* the same sequence of characters as the argument; in other words,the
* newly created string is a copy of the argument string. Unless an
* explicit copy of {@code original} is needed,use of this constructor is
* unnecessary since Strings are immutable.
*
* @param original
* A {@code String}
*/
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}複製程式碼
這是String類的構造方法,而這個value實際上就是char陣列。
/** The value is used for character storage. */
private final char value[];複製程式碼
字串在記憶體中的儲存方式
我們都知道如何去建立一個字串,那麼, 字串在記憶體中的儲存方式是怎樣的呢?在記憶體中有一個區域叫做常量池,而當我們以這樣的方式去建立字串:
String s1 = "abc";
String s2 = "abc";複製程式碼
這個字串就一定會被儲存到常量池中。而Java虛擬機器器如果發現常量池中已經存在需要建立的字串中,它就不會重複建立,而是指向那個字串即可。
String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2);複製程式碼
所以上述程式碼段的執行結果一定是true。但是如果使用new關鍵字區建立字串,過程就不太一樣了。比如下面的宣告:
String s3 = new String("abc");
String s4 = new String("abc");複製程式碼
過程是這樣的:首先將abc儲存在常量池中,此時並沒有引用,然後new關鍵字會去建立一個字串物件,就會在堆記憶體中建立abc,然後s3變數指向abc。當執行第二句宣告時,因為常量池中已經存在abc,所以不會重複建立,而new關鍵字又會去堆記憶體開闢空間存放abc,然後s4變數指向abc。
String s3 = new String("abc");
String s4 = new String("abc");
System.out.println(s3 == s4);複製程式碼
所以上述程式碼段的執行結果一定是false。
字串駐留
當相同的字串常量被多次建立時,注意是使用雙引號(" ")顯式宣告時,字串常量物件會被儲存在常量池中,且只會建立一個物件,這就是字串駐留,這個名詞的產生就是為了提升效能。簡單提一下,字串中有一個方法叫做intern();那麼這個方法有什麼作用呢? 該方法會去常量池中尋找當前呼叫該方法的字串常量,若找到,則直接返回該字串物件,若沒有,則將當前字串放入常量池並返回,總之該方法一定會返回字串。
String s3 = new String("abc");
String s4 = new String("abc");
System.out.println(s3.intern() == s4.intern());複製程式碼
所以上述程式碼段的執行結果一定是true,因為字串駐留只允許常量池中一個相同字串的存在。
JVM記憶體結構
剛才一直在說常量池,那麼常量池具體在哪呢?這就要來研究一下JVM的記憶體結構。JVM分為堆、棧、方法區,棧又分為本地方法棧和Java棧。在Java7之前常量池就放在方法區裡,而從Java7開始,常量池被移到了堆。這樣說過於抽象,我們可以通過程式碼來感受這一過程。
String s1 = new String("hello") + new String("world");
String s2 = "helloworld";
System.out.println(s1 == s2);複製程式碼
上述程式段的執行結果一定是false。因為s1變數在堆中,而s2變數在常量池中,兩者肯定不相同。那麼看下面這段程式碼,猜猜看結果是什麼?
String s1 = new String("hello") + new String("world");
System.out.println(s1.intern() == s1);複製程式碼
按照剛才的分析,intern()返回的一定是常量池裡的字串,而s1變數在堆中,它們肯定是不一樣的,但執行結果竟然是true。那是不是就能解釋常量池在堆中,所以它們指向的是同一個物件呢?其實還不完全是,我們可以繼續看一段程式碼。
String s1 = new String("hello") + new String("world");
System.out.println(s1.intern() == s1);
String s2 = new String("hello") + new String("world");
System.out.println(s2.intern() == s2);複製程式碼
這段程式碼的執行結果:
true
false複製程式碼
感覺很神奇,讓人猜不透,摸不著。別急,下面我們來一起分析一下。通過這個圖來理解一下,首先第一行程式碼會在常量池中建立hello和world兩個字串,接著在堆中開闢了一個空間存放組合後的字串helloworld,然後變數s1指向它。我們說intern()會返回常量池中的字串,那麼在常量池中沒有helloworld的情況下intern()方法會怎樣處理呢?其實它會將對堆中helloworld的引用放入常量池中,此時s1.intern()和s1都指向的是同一個物件,它們是相等的。但是s2在建立的過程中也會在堆中開闢一個空間存放helloworld,使變數s2指向它,而s2.intern()方法在執行的時候發現,helloworld的引用已經存在,所以直接返回,但此時返回的其實是s1變數的引用,那麼s2.intern()與s2不相等相信大家能夠理解了。
String s1 = new String("hello") + new String("world");
System.out.println(s1.intern() == s1);
String s2 = new String("hello") + new String("world");
System.out.println(s2.intern() == s1);複製程式碼
那麼這段程式的輸出結果你若是能立馬知曉,那麼恭喜你,前面的知識點你已基本掌握。執行結果就是:
true
true複製程式碼
我們還可以通過一個極端的方法來判斷常量池的位置。
List list = new ArrayList();
String str = "boom";
for(int i = 0;i < Integer.MAX_VALUE;i++) {
String temp = str + i;
str = temp;
list.add(temp.intern());
}複製程式碼
通過編寫這一段程式能夠讓JVM去不停地將字串變數存入常量池從而使其記憶體溢位,記憶體溢位後控制檯資訊如下:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOfRange(Arrays.java:2694)
at java.lang.String.<init>(String.java:203)
at java.lang.StringBuilder.toString(StringBuilder.java:405)
at com.itcast.test2.StringTest.main(StringTest.java:25)複製程式碼
可以看到,控制檯資訊提示堆記憶體溢位,這也可以得出常量池的位置是在堆內。這是Java7及其以後版本的輸出資訊,當我們將版本切換為Java7之前的版本,同樣的程式碼,輸出資訊如下:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.util.Arrays.copyOfRange(Arrays.java:2694)
at java.lang.String.<init>(String.java:203)
at java.lang.StringBuilder.toString(StringBuilder.java:405)
at com.itcast.test2.StringTest.main(StringTest.java:25)複製程式碼
PermGen space其實就是方法區, 那麼其實在JVM中的堆,一般分為三大部分:新生代、老年代、永久代:這個PermGen space就是永久代,也就是方法區,叫法不同而已。
其它問題
繼續來探討一下關於字串常量的一些其它問題。
String s1 = "hello" + "world";
String s2 = "helloworld";
System.out.println(s1 == s2);
String temp = "hello";
String s3 = temp + "world";
String s4 = "helloworld";
System.out.println(s3 == s4);複製程式碼
那麼,這兩個輸出的結果是什麼呢?結果是:
true
false複製程式碼
第一個輸出為true不難理解,因為s1和s2指向的都是常量池中的helloworld字串,那麼s3和s4難道就不是嗎?它還真就不是這樣了。s3在建立過程中會將temp儲存在堆記憶體中,所以s3和s4指向的物件不是同一個。我們可以通過反編譯來證實,將這段程式碼的.class檔案進行反編譯,結果如下:
String s1 = "helloworld";
String s2 = "helloworld";
System.out.println(s1 == s2);
String temp = "hello";
String s3 = String.valueOf(temp) + "world";
String s4 = "helloworld";
System.out.println(s3 == s4);複製程式碼
我們可以看到,s1和s2的建立過程其實是一模一樣的,其實,JVM為了優化速度,當它確定是兩個字串常量進行拼接時,它會在編譯器就完成拼接,而並不會去建立物件處理,但是s3的建立要經過temp變數,因為JVM無法在編譯期就推測出temp,所以它要通過String物件來進行處理,將temp放入堆記憶體。所以,並不是說只有出現new關鍵字變數才會放入堆記憶體中。
希望這篇文章能夠使你更加深入地理解字串常量。