? ? 開始介紹 intern()方法前,先看一個(gè)簡單的 Java程序吧!
publicclassIntern{
? ? // 測試 String.intern()的使用
? ? ?publicstaticvoid main(String[] args) {? ? ? ??
? ? ? ? ? ? ? ? ?String str1 ="abc";? ? ??
? ? ? ? ? ? ? ? ?String str2 ="abc";? ? ? ?
? ? ? ? ? ? ? ? ? String str3 ="a";? ? ??
????????????????? String str4 ="bc";? ? ? ?
?????????????????String str5 = str3 + str4;? ? ? ??
? ? ? ? ? ? ? ? ?String str6 =newString(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());? ? }}
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語言會(huì)使用常量池保存那些在編譯器就已確定的已編譯的class文件中的一份數(shù)據(jù),主要有類留夜、接口撬碟、方法中的常量,以及一些以文本形式出現(xiàn)的符號引用底靠,如:類和接口的全限定名害晦;字段的名稱和描述符;方法和名稱和描述符等暑中。因此在編譯完Intern類后壹瘟,生成的class文件中會(huì)在常量池中保存“abc”、“a”和“bc”三個(gè)String常量鳄逾。
變量str1和str2均保存的是常量池中“abc”的引用稻轨,所以str1==str2成立;
在執(zhí)行 str5 = str3 + str4這句時(shí)雕凹,JVM會(huì)先創(chuàng)建一個(gè)StringBuilder對象殴俱,通過StringBuilder.append()方法將str3與str4的值拼接政冻,然后通過StringBuilder.toString()返回一個(gè)String對象,賦值給str5线欲,因此str1和str5指向的不是同一個(gè)String對象明场,str1 == str5不成立;
String str6 = new String(str1)一句顯式創(chuàng)建了一個(gè)新的String對象李丰,因此str1 == str6不成立便是顯而易見的事了苦锨。
------intern------
上面沒有使用intern()方法的字符串比較相對比較好理解,然而下面這部分使用了intern()方法的字符串比較操作才是本文的重點(diǎn)嫌套∧媛牛看到答案的你有沒有一臉懵逼?
String.intern()使用原理
查看 Java String類源碼踱讨,可以看到 intern()方法的定義如下:
publicnativeStringintern();
String.intern()是一個(gè)Native方法魏蔗,底層調(diào)用C++的 StringTable::intern方法實(shí)現(xiàn)。
當(dāng)通過語句str.intern()調(diào)用intern()方法后痹筛,JVM 就會(huì)在當(dāng)前類的常量池中查找是否存在與str等值的String莺治,若存在則直接返回常量池中相應(yīng)Strnig的引用;若不存在帚稠,則會(huì)在常量池中創(chuàng)建一個(gè)等值的String谣旁,然后返回這個(gè)String在常量池中的引用。因此滋早,只要是等值的String對象榄审,使用intern()方法返回的都是常量池中同一個(gè)String引用,所以杆麸,這些等值的String對象通過intern()后使用==是可以匹配的搁进。
由此就可以理解上面代碼中------intern------部分的結(jié)果了。因?yàn)閟tr1昔头、str5和str6是三個(gè)等值的String饼问,所以通過intern()方法,他們均會(huì)指向常量池中的同一個(gè)String引用揭斧,因此str1.intern() == str5.intern() == str6.intern()均為true莱革。
String.intern() in Java 6
Java 6中常量池位于PermGen(永久代)中,PermGen是一塊主要用于存放已加載的類信息和字符串池的大小固定的區(qū)域讹开。執(zhí)行intern()方法時(shí)盅视,若常量池中不存在等值的字符串,JVM就會(huì)在常量池中*** 創(chuàng)建一個(gè)等值的字符串***萧吠,然后返回該字符串的引用左冬。除此以外,JVM 會(huì)自動(dòng)在常量池中保存一份之前已使用過的字符串集合纸型。
** Java 6中使用intern()方法的主要問題就在于常量池被保存在PermGen中 **
首先,PermGen是一塊大小固定的區(qū)域,一般狰腌,不同的平臺PermGen的默認(rèn)大小也不相同除破,大致在32M到96M之間。所以不能對不受控制的運(yùn)行時(shí)字符串(如用戶輸入信息等)使用intern()方法琼腔,否則很有可能會(huì)引發(fā)PermGen內(nèi)存溢出瑰枫;
其次,String對象保存在 Java堆區(qū)丹莲,Java堆區(qū)與PermGen是物理隔離的光坝,因此,如果對多個(gè)不等值的字符串對象執(zhí)行intern操作甥材,則會(huì)導(dǎo)致內(nèi)存中存在許多重復(fù)的字符串盯另,會(huì)造成性能損失。
String.intern() in Java 7
Java 7將常量池從PermGen區(qū)移到了Java堆區(qū)洲赵,執(zhí)行intern操作時(shí)鸳惯,如果常量池已經(jīng)存在該字符串,則直接返回字符串引用叠萍,否則*** 復(fù)制該字符串對象的引用*** 到常量池中并返回芝发。
堆區(qū)的大小一般不受限,所以將常量池從PremGen區(qū)移到堆區(qū)使得常量池的使用不再受限于固定大小苛谷。除此之外辅鲸,位于堆區(qū)的常量池中的對象可以被垃圾回收。當(dāng)常量池中的字符串不再存在指向它的引用時(shí)腹殿,JVM就會(huì)回收該字符串独悴。
可以使用 -XX:StringTableSize 虛擬機(jī)參數(shù)設(shè)置字符串池的map大小。字符串池內(nèi)部實(shí)現(xiàn)為一個(gè)HashMap赫蛇,所以當(dāng)能夠確定程序中需要intern的字符串?dāng)?shù)目時(shí)绵患,可以將該map的size設(shè)置為所需數(shù)目*2(減少hash沖突),這樣就可以使得String.intern()每次都只需要常量時(shí)間和相當(dāng)小的內(nèi)存就能夠?qū)⒁粋€(gè)String存入字符串池中悟耘。
-XX:StringTableSize的默認(rèn)值:Java 7u40以前為:1009落蝙,Java 7u40以后:60013
intern()適用場景
Java 6中常量池位于PermGen區(qū),大小受限暂幼,所以不建議適用intern()方法筏勒,當(dāng)需要字符串池時(shí),需要自己使用HashMap實(shí)現(xiàn)旺嬉。
Java7管行、8中,常量池由PermGen區(qū)移到了堆區(qū)邪媳,還可以通過-XX:StringTableSize參數(shù)設(shè)置StringTable的大小捐顷,常量池的使用不再受限荡陷,由此可以重新考慮使用intern()方法。
intern()方法優(yōu)點(diǎn):
執(zhí)行速度非逞镐蹋快废赞,直接使用==進(jìn)行比較要比使用equals()方法快很多;
內(nèi)存占用少叮姑。
雖然intern()方法的優(yōu)點(diǎn)看上去很誘人唉地,但若不是在恰當(dāng)?shù)膱龊现惺褂迷摲椒ǖ脑挘惴堑荒塬@得如此好處传透,反而還可能會(huì)有性能損失耘沼。
下面程序?qū)Ρ攘耸褂胕ntern()方法和未使用intern()方法存儲100萬個(gè)String時(shí)的性能,從輸出結(jié)果可以看出朱盐,若是單純使用intern()方法進(jìn)行數(shù)據(jù)存儲的話群嗤,程序運(yùn)行時(shí)間要遠(yuǎn)高于未使用intern()方法時(shí):
publicclassIntern2{
????????publicstaticvoidmain(String[] args){? ? ? ??
????????????????print("noIntern: "+ noIntern());? ? ? ??
????????????????print("intern: "+ intern());? ??
????????}
? ? ? ? ?privatestaticlongnoIntern(){
????????????????longstart = System.currentTimeMillis();
????????????????for(inti =0; i <1000000; i++) {
????????????????????????intj = i %100;? ? ? ? ? ??
????????????????????????String str = String.valueOf(j);? ? ? ??
? ? ? ? ? ? ? ? ?}
? ? ? ? ? ? ? ? ?returnSystem.currentTimeMillis() - start;??
? ? ? ? ? ?}
? ? ? ? ? ?privatestaticlongintern(){
????????????????????longstart = System.currentTimeMillis();
? ? ? ? ? ? ? ? ? ? ?for(inti =0; i <1000000; i++) {intj = i %100;? ? ? ? ? ??
? ? ? ? ? ? ? ? ? ? ? ? ? ?String str = String.valueOf(j).intern();? ? ? ?
? ? ? ? ? ? ? ? ? ? }
????????????????????returnSystem.currentTimeMillis() - start;? ?
? ? ? ? ? ? ?}
}
//Output:noIntern:48// 未使用intern方法時(shí),存儲100萬個(gè)String所需時(shí)間intern:99
// 使用intern方法時(shí)托享,存儲100萬個(gè)String所需時(shí)間
由于intern()操作每次都需要與常量池中的數(shù)據(jù)進(jìn)行比較以查看常量池中是否存在等值數(shù)據(jù)骚烧,同時(shí)JVM需要確保常量池中的數(shù)據(jù)的唯一性,這就涉及到加鎖機(jī)制闰围,這些操作都是有需要占用CPU時(shí)間的赃绊,所以如果進(jìn)行intern操作的是大量不會(huì)被重復(fù)利用的String的話,則有點(diǎn)得不償失羡榴。由此可見碧查,String.intern()主要適用于只有有限值,并且這些有限值會(huì)被重復(fù)利用的場景校仑,如:數(shù)據(jù)庫表中的列名忠售、人的姓氏、編碼類型等迄沫。
使用 String.intern() 可以保證所有相同內(nèi)容的字符串變量引用相同的內(nèi)存對象稻扬。
intern 用法 intern 適合用在需要讀取數(shù)據(jù)并將這些對象或者字符串納入一個(gè)更大范圍作用域的情況。需要注意的是羊瘩,硬編碼在代碼中的字符串(例如常量等等)都會(huì)被編譯器自動(dòng)的執(zhí)行 intern 操作泰佳。
看一個(gè)例子:
?String city = resultSet.getString(1);
String region = resultSet.getString(2);
String countryCode = resultSet.getString(3);
double city = resultSet.getDouble(4);
double city = resultSet.getDouble(5);
Location location = new Location(city.intern(), region.intern(), countryCode.intern(), long, lat); allLocations.add(location);
所有新創(chuàng)建的地點(diǎn)對象都會(huì)使用 intern 得到的字符串。而從數(shù)據(jù)庫讀取到的臨時(shí)字符串則會(huì)被垃圾回收尘吗。
總結(jié):
String.intern()方法是一種手動(dòng)將字符串加入常量池中的方法逝她,原理如下:如果在常量池中存在與調(diào)用intern()方法的字符串等值的字符串,就直接返回常量池中相應(yīng)字符串的引用睬捶,否則在常量池中復(fù)制一份該字符串黔宛,并將其引用返回(Java7中會(huì)直接在常量池中保存當(dāng)前字符串的引用);
Java 6 中常量池位于PremGen區(qū)擒贸,大小受限臀晃,不建議使用String.intern()方法觉渴,不過Java 7 將常量池移到了Java堆區(qū),大小可控积仗,可以重新考慮使用String.intern()方法疆拘,但是由對比測試可知蜕猫,使用該方法的耗時(shí)不容忽視寂曹,所以需要慎重考慮該方法的使用;
String.intern()方法主要適用于程序中需要保存有限個(gè)會(huì)被反復(fù)使用的值的場景回右,這樣可以減少內(nèi)存消耗隆圆,同時(shí)在進(jìn)行比較操作時(shí)減少時(shí)耗,提高程序性能翔烁。
如何確定 intern 的效率
最好的方法是對整個(gè)堆執(zhí)行一次堆轉(zhuǎn)儲渺氧。堆轉(zhuǎn)儲也會(huì)在發(fā)生 OutOfMemoryError 時(shí)執(zhí)行。
在 MAT (內(nèi)存分析工具蹬屹,譯者注)中打開轉(zhuǎn)儲文件侣背,然后選擇?java.lang.String,依次點(diǎn)擊“Java Basics”慨默、“Group By Value”贩耐。
根據(jù)堆的大小,上面的操作可能耗費(fèi)比較長的時(shí)間厦取。最后可以看到類型這樣的結(jié)果潮太。按 “Retained Heap” 或者是 “Objects” 列進(jìn)行排序,可以發(fā)現(xiàn)一些有趣的東西:
從這快照中我們可以看到虾攻,空的字符串占用了大量的內(nèi)存铡买!兩百萬個(gè)空字符串對象占用了總共 130 MB 的空間。另外可以看到一部分被加載的 JavaScript 腳本霎箍,一些作為鍵的字符串奇钞,它們被用于定位。另外漂坏,還有一些與業(yè)務(wù)邏輯相關(guān)的字符串景埃。
這些與業(yè)務(wù)邏輯相關(guān)的字符串是最容易進(jìn)行 intern 操作的,因?yàn)槲覀兦宄刂浪鼈兪窃谑裁吹胤奖患虞d進(jìn)內(nèi)存的樊拓。對于其他字符串纠亚,可以通過 “Merge shortest Path to GC Root” 選項(xiàng)來找到它們被存儲的位置,這個(gè)信息也許能夠幫助我們找到該使用 intern 的地方筋夏。