緣起
開始介紹 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()方法主要適用于程序中需要保存有限個會被反復使用的值的場景,這樣可以減少內存消耗,同時在進行比較操作時減少時耗,提高程序性能。