1.序
我醒來,在一個陌生的世界。
周圍沒有一個熟悉的人,這里一望無盡,身前身后都是空寂。
走著,碰見一群可愛的小家伙們, 有個大家伙似乎控制著它們,他們完全聽從他的指揮。
我上前去詢問,你們是什么?
“常量!”
常量代表程序運行過程中不能改變的值,一般使用final修飾。
在Java中常量可以先聲明,然后進行賦值,但是只能賦值一次。
我恍然,
“那這里是JVM的常量池吧?”
常量池是為了避免頻繁的創建和銷毀對象而影響系統性能而建立的存放常量的空間,其實現了對象的共享。比如字符串常量池(下面所用的),整形常量池
“對呀,這里是運行時常量池!”。
所謂運行時常量池,則是jvm虛擬機在完成類裝載操作后,將class文件中的常量池載入到內存中,并保存在方法區中,我們常說的常量池,就是指方法區中的運行時常量池(java6),運行時常量池不是一成不變的,比如使用intern()就可以把新的內容放到常量池。
我順著光走過去,遇到了一個檢察員,他正在忙碌的尋找著,找到了就帶著小家伙的標記出去,找不到就做一個小家伙放在池子里。
“初次見面,請多關照!”,我走上前去打招呼
“你叫什么?”
“我……";
突然,小家伙們吵起來,又有新的伙伴進來了,可是小小的池子里已經塞滿了小常量,根本就塞不下啦。
隨著一聲巨響,夢境破碎了。
我醒來。
我打開IDEA。
2.碼
寫下來這樣一段代碼
String str0 = "a";
String str1 = "a";
String str2 = "b";
String str3 = "ab";
String str6 = "a"+"b";
String str4 = str1 + str2;
String str5 = new String("ab");
/**
* 這個返回true,因為字符串被寫死,在編譯期間直接放入常量池從而實現復用,
* 那么這樣看來,s0和s1指向同一個內存地址,所以相等
* */
System.out.println(str0 == str1); //true
/**
* s6是使用+符號動態拼接出來的字符串,但是參與拼接的也是很顯然的字面量(寫出內容的),所以
* 在編譯期間,編譯器會直接拼接,從而實現復用,那么在常量池中,達到的效果和上面的效果一樣
* */
System.out.println(str3 == str6); //true
/**
* s4的拼接在人為看來同s6沒有什么兩樣,最后達到的效果也是一樣的,那么這樣就可以解釋
* s4.equal(s3)了,那么s4 為什么 == s3 ,這個是要看JVM的操作了,
* JVM會先創建一個StringBuilder對象,通過StringBuilder.append()
* 方法將s1與s2的值拼接,然后通過StringBuilder.toString()
* 返回一個String對象,賦值給str4,因此str1和str5指向的不是同一個
* String對象,str4 == str3不成立;
* */
System.out.println(str4 == str3); //false
System.out.println(str4.equals(str3)); //ture
/**
* 很顯然,內容一致equal一定成立,但是s5是新建對象,那么一定不等于s3
* */
System.out.println(str5.equals(str3)); //true
System.out.println(str5 == str3); //false
首先寫下的是一些很常見的比較,具體為什么有這樣的執行結果大家都已經很明了,不懂的可以參考注釋。
恍過神,記起來那個檢查員。
查閱資料。
尋找,尋找。
我想它應該是intern()吧。
3.String.intern()
/**
* Returns a canonical representation for the string object.
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java? Language Specification</cite>.
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
Q1:什么是intern()?
正如上面官方給的解釋, 如果一個string調用了intern(),如果字符串常量池中已經包含等于此String的字符串,則從常量池將字符串引用返回,否則將此字符串對象添加到常量池,并且返回String對象的引用,當且僅當s.equals(t)的時候,s.intern() = t.intern()返回true。
String.intern()是一個Native方法,底層調用C++的 StringTable::intern方法實現。
那么我們來試一試;
String a = "a";
String b = "a";
System.out.println(a.intern() == b.intern()); //true
返回為ture,這也是上面所說的當且僅當s.equals(t)的時候,s.intern() = t.intern()返回true。
Q2:intern()的前生今世
Java6/HotSpot1.6:
注:下文所提到的永久代與方法區不等價,但在Hotspot開發中,大家習慣把方法區稱為永久代
本版本中常量池存在于永久代(PermGen)中,永久代和Java堆的內存是物理隔離的,Java6中intern()方法會把首次遇到的字符串實例復制到PermGen,執行intern方法時,如果常量池不存在該字符串,虛擬機會在常量池中復制該字符串,并返回引用,如果存在則直接返回引用,但是正是因為常量池在永久代里,導致出現了幾個問題
問題1.
永久代是一塊大小固定的區域,不同平臺的永久代默認大小也不同,大致在32M與96M之間,所以當對象內容不可控時(比如人為輸入,系統生成等),很有可能發生PermGenOOM
問題2.
String對象保存在Java堆,而此時的Java堆和永久代是屬于虛擬機中兩部分的,因此如果對不等值的字符串對象進行intern操作,那么永久代就會出現內存浪費 。
Java7/HotSpot1.7:
常量池在java堆上分配內存,執行intern方法時,如果常量池已經存在相等的字符串,則直接返回字符串引用,否則復制該字符串引用到常量池中并返回;intern()會進行一個引用保存的操作而不是對象復制,將在下面提到,相比與永久代,堆區的好處如下:
1.堆區大小不受限制,那么常量池也隨之不受內存限制,同時GC可以一并回收常量池的垃圾。
2.相比于永久代,堆中常量池對于字符對象的利用更充分,不會因為調用了intern()就產生很多版本復制。
正是遷移常量池的這個做法,解決了Java6的缺點。
那么下面的這個小例子可以說明這個問題。
源碼參考:http://www.lxweimin.com/p/c14364f72b7e,同時這也是《深入理解Java虛擬機》一書給的例子,在原來基礎上稍作修改
String s0 = "java";
String s1 = new StringBuilder("String").append("Test").toString();
System.out.println(s1.intern() == s1);
String s2 = new StringBuilder("ja").append("va").toString();
System.out.println(s2.intern() == s2);
Java6的輸出結果是false false
如圖所示,
- 代碼第一句,首先寫了一個字面上的量(就是直接寫了”xxxx“字符串),這樣寫JVM會直接將其放入常量池,此時常量池有“java”。
- 代碼第二句生成了堆中的StringTest,并且s1是堆中對象的引用,或者說s1指向的是堆中對象,
- 第三句輸出,s1.intern()會去永久代里的常量池檢查,沒有StringTest之后,將StringTest加入其中,并且返回其引用,我們可以這樣理解,s1.intern()指向了常量池,按照我們所說的,由于永久代和堆區的物理隔離,這兩個地址顯然不同,這樣顯然是s1.inrern() != s1的。
- 同理第四句,雖然常量池在這之前包括“java”,但是也只是讓intern()找到了它,返回的引用也是在永久代中,不可能會與堆中地址相等。
我們可以看到,這樣子的永久代會產生很多對象副本,如果超過了永久代儲存能力就會發生OOM,很不安全。
Java7的結果是true false
如圖所示
- 同樣,代碼第一句,JVM會直接將其放入常量池,此時常量池有“java”。
- 代碼第二句生成了StringTest對象,s1指向它。
- 第三句,s1.intern()去常量池尋找這個字符串,在常量池中沒有找到StringTest,但是不生成一個對象復制,而只是在常量池中記錄下首次出現的實例地址,也就是堆區中StringTest所在的位置,此時s1.intern()與s1同時指向堆中StringTest對象,那必定返回true
- 第四句,堆中有了一個內容為Java的字符串對象,并且s2指向它,但是s2.intern()找到了一個常量池存在的“Java”,所以指向了它,那么我們可以通過圖片看到,s2.intern()與s2不相等。
堆中放置常量池提供了很大的空間,并且大大減少了對象的重復生成。
同樣,我們做另一個小實驗
String s1 = new StringBuilder("String").append("Test").toString();
System.out.println(s1.intern() == s1);
String s2 = new StringBuilder("ja").append("va").toString();
System.out.println(s2.intern() == s2);
這樣子就會返回true ture了。因為圖中的“java”部分不存在了。
String s1 = new StringBuilder("String").append("Test").toString();
System.out.println(s1.intern() == s1);
String s2 = new StringBuilder().append("StringTest").toString();
System.out.println(s2.intern() == s2);
這樣子由于s1建立時常量區沒有StringTest,當s2建立的時候常量區已經有了,并且是s1那時候建立的,必定不能和新建立的這個地址一樣,所以上述代碼返回true false
String s1 = new StringBuilder("123").append("45").toString();
System.out.println(s1.intern() == s1);
String s2 = new StringBuilder("12").append("3").toString();
System.out.println(s2.intern() == s2);
String s3 = new StringBuilder("45").toString();
System.out.println(s3.intern() == s3);
這段代碼輸出:true false false ,大家可以思考一下為什么。
Q3:intern()用來干什么?
我們已經明白了intern()的大致用法,那么intern()能給我們帶來什么好處?
首先我們要知道intern()其實是可以代替 == 或者 equal的,效率比較快,內存占用較少。
public static void main(String[] args) {
print("noIntern: " + noIntern());
print("intern: " + intern());
}
private static long noIntern(){
long start = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
int j = i % 100;
String str = String.valueOf(j);
}
return System.currentTimeMillis() - start;
}
private static long intern(){
long start = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
int j = i % 100;
String str = String.valueOf(j).intern();
}
return System.currentTimeMillis() - start;
}
作者:LilacZiyun
鏈接:http://www.lxweimin.com/p/95f516cb75ef
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
//Output:
noIntern: 48 // 未使用intern方法時,存儲100萬個String所需時間
intern: 99 // 使用intern方法時,存儲100萬個String所需時間
這段代碼來自于LilacZiyun.
那么我們可以明白intern其實不是用來代替equal或者==,也不是隨手就用的,主要是用于存在大量相同數據或者反復使用的情況。
4.終
2017-11-12 00:03:17,該睡覺了。
去夢里,見見那些孩子們吧。
如能有幸,我帶更多那個世界的秘密來,好么?
晚安,孤獨的創作者。