揭開 String.intern() 那神秘的面紗

緣起

開始介紹 intern()方法前,先看一個簡單的 Java程序吧!下面是一段 Java代碼,代碼內容比較簡單,簡而言之,就是比較幾個字符串是否相等并輸出比較結果。然而,看似簡單的字符串比較操作,卻暗含玄機,聰明的你,能一字不差的說出最后的輸出結果么?如果你知道答案并理解原因的話,那么你就可以選擇跳過此篇博文去干更有意義的事了。若是不能的話,要不就跟隨小編一起探明究竟吧!

public class Intern {
    // 測試 String.intern()的使用
    public static void main(String[] args) {
        String str1 = "abc";
        String str2 = "abc";
        String str3 = "a";
        String str4 = "bc";
        String str5 = str3 + str4;
        String str6 = new String(str1);

        print("------no intern------");
        printnb("str1 == str2 ? ");
        print( str1 == str2);
        printnb("str1 == str5 ? ");
        print(str1 == str5);
        printnb("str1 == str6 ? ");
        print(str1 == str6);
        print();

        print("------intern------");
        printnb("str1.intern() == str2.intern() ? ");
        print(str1.intern() == str2.intern());
        printnb("str1.intern() == str5.intern() ? ");
        print(str1.intern() == str5.intern());
        printnb("str1.intern() == str6.intern() ? ");
        print(str1.intern() == str6.intern());
        printnb("str1 == str6.intern() ? ");
        print(str1 == str6.intern());
    }
}

Duang, the true answer is over here:

------no intern------
str1 == str2 ? true
str1 == str5 ? false
str1 == str6 ? false

------intern------
str1.intern() == str2.intern() ? true
str1.intern() == str5.intern() ? true
str1.intern() == str6.intern() ? true
str1 == str6.intern() ? true

** 初步解析 **
------no intern------
Java語言會使用 常量池 保存那些在編譯器就已確定的已編譯的class文件中的一份數據,主要有類、接口、方法中的常量,以及一些以文本形式出現的符號引用,如:類和接口的全限定名;字段的名稱和描述符;方法和名稱和描述符等。因此在編譯完Intern類后,生成的class文件中會在常量池中保存“abc”、“a”和“bc”三個String常量。

  • 變量str1和str2均保存的是常量池中“abc”的引用,所以str1==str2成立;
  • 在執行 str5 = str3 + str4這句時,JVM會先創建一個StringBuilder對象,通過StringBuilder.append()方法將str3與str4的值拼接,然后通過StringBuilder.toString()返回一個String對象,賦值給str5,因此str1和str5指向的不是同一個String對象,str1 == str5不成立;
  • String str6 = new String(str1)一句顯式創建了一個新的String對象,因此str1 == str6不成立便是顯而易見的事了。

------intern------
上面沒有使用intern()方法的字符串比較相對比較好理解,然而下面這部分使用了intern()方法的字符串比較操作才是本文的重點。看到答案的你有沒有一臉懵逼?

String.intern()使用原理

查看 Java String類源碼,可以看到 intern()方法的定義如下:

public native String intern();

String.intern()是一個Native方法,底層調用C++的 StringTable::intern方法實現。

當通過語句str.intern()調用intern()方法后,JVM 就會在當前類的常量池中查找是否存在與str等值的String,若存在則直接返回常量池中相應Strnig的引用;若不存在,則會在常量池中創建一個等值的String,然后返回這個String在常量池中的引用。因此,只要是等值的String對象,使用intern()方法返回的都是常量池中同一個String引用,所以,這些等值的String對象通過intern()后使用==是可以匹配的。

由此就可以理解上面代碼中------intern------部分的結果了。因為str1、str5和str6是三個等值的String,所以通過intern()方法,他們均會指向常量池中的同一個String引用,因此str1.intern() == str5.intern() == str6.intern()均為true。

String.intern() in Java 6

Java 6中常量池位于PermGen(永久代)中,PermGen是一塊主要用于存放已加載的類信息和字符串池的大小固定的區域。執行intern()方法時,若常量池中不存在等值的字符串,JVM就會在常量池中*** 創建一個等值的字符串***,然后返回該字符串的引用。除此以外,JVM 會自動在常量池中保存一份之前已使用過的字符串集合。

** Java 6中使用intern()方法的主要問題就在于常量池被保存在PermGen中 **

  • 首先,PermGen是一塊大小固定的區域,一般,不同的平臺PermGen的默認大小也不相同,大致在32M到96M之間。所以不能對不受控制的運行時字符串(如用戶輸入信息等)使用intern()方法,否則很有可能會引發PermGen內存溢出;

  • 其次,String對象保存在 Java堆區,Java堆區與PermGen是物理隔離的,因此,如果對多個不等值的字符串對象執行intern操作,則會導致內存中存在許多重復的字符串,會造成性能損失。

String.intern() in Java 7

Java 7將常量池從PermGen區移到了Java堆區,執行intern操作時,如果常量池已經存在該字符串,則直接返回字符串引用,否則*** 復制該字符串對象的引用*** 到常量池中并返回。

堆區的大小一般不受限,所以將常量池從PremGen區移到堆區使得常量池的使用不再受限于固定大小。除此之外,位于堆區的常量池中的對象可以被垃圾回收。當常量池中的字符串不再存在指向它的引用時,JVM就會回收該字符串。

可以使用 -XX:StringTableSize 虛擬機參數設置字符串池的map大小。字符串池內部實現為一個HashMap,所以當能夠確定程序中需要intern的字符串數目時,可以將該map的size設置為所需數目*2(減少hash沖突),這樣就可以使得String.intern()每次都只需要常量時間和相當小的內存就能夠將一個String存入字符串池中。

-XX:StringTableSize的默認值:Java 7u40以前為:1009,Java 7u40以后:60013

intern()適用場景

Java 6中常量池位于PermGen區,大小受限,所以不建議適用intern()方法,當需要字符串池時,需要自己使用HashMap實現。

Java7、8中,常量池由PermGen區移到了堆區,還可以通過-XX:StringTableSize參數設置StringTable的大小,常量池的使用不再受限,由此可以重新考慮使用intern()方法。

intern()方法優點:

  • 執行速度非常快,直接使用==進行比較要比使用equals()方法快很多;
  • 內存占用少。

雖然intern()方法的優點看上去很誘人,但若不是在恰當的場合中使用該方法的話,便非但不能獲得如此好處,反而還可能會有性能損失。

下面程序對比了使用intern()方法和未使用intern()方法存儲100萬個String時的性能,從輸出結果可以看出,若是單純使用intern()方法進行數據存儲的話,程序運行時間要遠高于未使用intern()方法時:

public class Intern2 {

    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;
    }
}
//Output:
noIntern: 48    // 未使用intern方法時,存儲100萬個String所需時間
intern: 99      // 使用intern方法時,存儲100萬個String所需時間

由于intern()操作每次都需要與常量池中的數據進行比較以查看常量池中是否存在等值數據,同時JVM需要確保常量池中的數據的唯一性,這就涉及到加鎖機制,這些操作都是有需要占用CPU時間的,所以如果進行intern操作的是大量不會被重復利用的String的話,則有點得不償失。由此可見,String.intern()主要 適用于只有有限值,并且這些有限值會被重復利用的場景,如:數據庫表中的列名、人的姓氏、編碼類型等。

總結:

  • String.intern()方法是一種手動將字符串加入常量池中的方法,原理如下:如果在常量池中存在與調用intern()方法的字符串等值的字符串,就直接返回常量池中相應字符串的引用,否則在常量池中復制一份該字符串,并將其引用返回(Java7中會直接在常量池中保存當前字符串的引用);
  • Java 6 中常量池位于PremGen區,大小受限,不建議使用String.intern()方法,不過Java 7 將常量池移到了Java堆區,大小可控,可以重新考慮使用String.intern()方法,但是由對比測試可知,使用該方法的耗時不容忽視,所以需要慎重考慮該方法的使用;
  • String.intern()方法主要適用于程序中需要保存有限個會被反復使用的值的場景,這樣可以減少內存消耗,同時在進行比較操作時減少時耗,提高程序性能。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容