緣起
開始介紹 intern()方法前,先看一個(gè)簡(jiǎn)單的 Java程序吧!下面是一段 Java代碼,代碼內(nèi)容比較簡(jiǎn)單,簡(jiǎn)而言之,就是比較幾個(gè)字符串是否相等并輸出比較結(jié)果。然而,看似簡(jiǎn)單的字符串比較操作,卻暗含玄機(jī),聰明的你,能一字不差的說出最后的輸出結(jié)果么?如果你知道答案并理解原因的話,那么你就可以選擇跳過此篇博文去干更有意義的事了。若是不能的話,要不就跟隨小編一起探明究竟吧!
public class Intern {
// 測(cè)試 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語言會(huì)使用 常量池 保存那些在編譯器就已確定的已編譯的class文件中的一份數(shù)據(jù),主要有類、接口、方法中的常量,以及一些以文本形式出現(xiàn)的符號(hào)引用,如:類和接口的全限定名;字段的名稱和描述符;方法和名稱和描述符等。因此在編譯完Intern類后,生成的class文件中會(huì)在常量池中保存“abc”、“a”和“bc”三個(gè)String常量。
- 變量str1和str2均保存的是常量池中“abc”的引用,所以str1==str2成立;
- 在執(zhí)行 str5 = str3 + str4這句時(shí),JVM會(huì)先創(chuàng)建一個(gè)StringBuilder對(duì)象,通過StringBuilder.append()方法將str3與str4的值拼接,然后通過StringBuilder.toString()返回一個(gè)String對(duì)象,賦值給str5,因此str1和str5指向的不是同一個(gè)String對(duì)象,str1 == str5不成立;
- String str6 = new String(str1)一句顯式創(chuàng)建了一個(gè)新的String對(duì)象,因此str1 == str6不成立便是顯而易見的事了。
------intern------
上面沒有使用intern()方法的字符串比較相對(duì)比較好理解,然而下面這部分使用了intern()方法的字符串比較操作才是本文的重點(diǎn)。看到答案的你有沒有一臉懵逼?
String.intern()使用原理
查看 Java String類源碼,可以看到 intern()方法的定義如下:
public native String intern();
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對(duì)象,使用intern()方法返回的都是常量池中同一個(gè)String引用,所以,這些等值的String對(duì)象通過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ū)域,一般,不同的平臺(tái)PermGen的默認(rèn)大小也不相同,大致在32M到96M之間。所以不能對(duì)不受控制的運(yùn)行時(shí)字符串(如用戶輸入信息等)使用intern()方法,否則很有可能會(huì)引發(fā)PermGen內(nèi)存溢出;
其次,String對(duì)象保存在 Java堆區(qū),Java堆區(qū)與PermGen是物理隔離的,因此,如果對(duì)多個(gè)不等值的字符串對(duì)象執(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ù)制該字符串對(duì)象的引用*** 到常量池中并返回。
堆區(qū)的大小一般不受限,所以將常量池從PremGen區(qū)移到堆區(qū)使得常量池的使用不再受限于固定大小。除此之外,位于堆區(qū)的常量池中的對(duì)象可以被垃圾回收。當(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()適用場(chǎng)景
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ù)膱?chǎng)合中使用該方法的話,便非但不能獲得如此好處,反而還可能會(huì)有性能損失。
下面程序?qū)Ρ攘耸褂胕ntern()方法和未使用intern()方法存儲(chǔ)100萬個(gè)String時(shí)的性能,從輸出結(jié)果可以看出,若是單純使用intern()方法進(jìn)行數(shù)據(jù)存儲(chǔ)的話,程序運(yùn)行時(shí)間要遠(yuǎn)高于未使用intern()方法時(shí):
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方法時(shí),存儲(chǔ)100萬個(gè)String所需時(shí)間
intern: 99 // 使用intern方法時(shí),存儲(chǔ)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ù)利用的場(chǎng)景,如:數(shù)據(jù)庫(kù)表中的列名、人的姓氏、編碼類型等。
總結(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()方法,但是由對(duì)比測(cè)試可知,使用該方法的耗時(shí)不容忽視,所以需要慎重考慮該方法的使用;
- String.intern()方法主要適用于程序中需要保存有限個(gè)會(huì)被反復(fù)使用的值的場(chǎng)景,這樣可以減少內(nèi)存消耗,同時(shí)在進(jìn)行比較操作時(shí)減少時(shí)耗,提高程序性能。