幾張圖輕鬆理解String.intern()

BooleanZhang發表於2020-12-17

https://blog.csdn.net/xcy1193068639/article/details/81809515

 

一、new String都是在堆上建立字串物件。當呼叫 intern() 方法時,編譯器會將字串新增到常量池中(stringTable維護),並返回指向該常量的引用。
這裡寫圖片描述

這裡寫圖片描述

二、通過字面量賦值建立字串(如:String str=”twm”)時,會先在常量池中查詢是否存在相同的字串,若存在,則將棧中的引用直接指向該字串;若不存在,則在常量池中生成一個字串,再將棧中的引用指向該字串。
這裡寫圖片描述

三、常量字串的“+”操作,編譯階段直接會合成為一個字串。如string str=”JA”+”VA”,在編譯階段會直接合併成語句String str=”JAVA”,於是會去常量池中查詢是否存在”JAVA”,從而進行建立或引用。

四、對於final欄位,編譯期直接進行了常量替換(而對於非final欄位則是在執行期進行賦值處理的)。
final String str1=”ja”;
final String str2=”va”;
String str3=str1+str2;
在編譯時,直接替換成了String str3=”ja”+”va”,根據第三條規則,再次替換成String str3=”JAVA”

五、常量字串和變數拼接時(如:String str3=baseStr + “01”;)會呼叫stringBuilder.append()在堆上建立新的物件。

六、JDK 1.7後,intern方法還是會先去查詢常量池中是否有已經存在,如果存在,則返回常量池中的引用,這一點與之前沒有區別,區別在於,如果在常量池找不到對應的字串,則不會再將字串拷貝到常量池,而只是在常量池中生成一個對原字串的引用。簡單的說,就是往常量池放的東西變了:原來在常量池中找不到時,複製一個副本放到常量池,1.7後則是將在堆上的地址引用複製到常量池。
這裡寫圖片描述

舉例說明:

String str2 = new String("str")+new String("01");
str2.intern();
String str1 = "str01";
System.out.println(str2==str1);
  •  

在JDK 1.7下,當執行str2.intern();時,因為常量池中沒有“str01”這個字串,所以會在常量池中生成一個對堆中的“str01”的引用(注意這裡是引用 ,就是這個區別於JDK 1.6的地方。在JDK1.6下是生成原字串的拷貝),而在進行String str1 = “str01”;字面量賦值的時候,常量池中已經存在一個引用,所以直接返回了該引用,因此str1和str2都指向堆中的同一個字串,返回true。

String str2 = new String("str")+new String("01");
String str1 = "str01";
str2.intern();
System.out.println(str2==str1);
  •  

將中間兩行調換位置以後,因為在進行字面量賦值(String str1 = “str01″)的時候,常量池中不存在,所以str1指向的常量池中的位置,而str2指向的是堆中的物件,再進行intern方法時,對str1和str2已經沒有影響了,所以返回false。

常見試題解答

有了對以上的知識的瞭解,我們現在再來看常見的面試或筆試題就很簡單了:
Q:下列程式的輸出結果:
String s1 = “abc”;
String s2 = “abc”;
System.out.println(s1 == s2);
A:true,均指向常量池中物件。

Q:下列程式的輸出結果:
String s1 = new String(“abc”);
String s2 = new String(“abc”);
System.out.println(s1 == s2);
A:false,兩個引用指向堆中的不同物件。

Q:下列程式的輸出結果:
String s1 = “abc”;
String s2 = “a”;
String s3 = “bc”;
String s4 = s2 + s3;
System.out.println(s1 == s4);
A:false,因為s2+s3實際上是使用StringBuilder.append來完成,會生成不同的物件。

Q:下列程式的輸出結果:
String s1 = “abc”;
final String s2 = “a”;
final String s3 = “bc”;
String s4 = s2 + s3;
System.out.println(s1 == s4);
A:true,因為final變數在編譯後會直接替換成對應的值,所以實際上等於s4=”a”+”bc”,而這種情況下,編譯器會直接合併為s4=”abc”,所以最終s1==s4。

Q:下列程式的輸出結果:
String s = new String(“abc”);
String s1 = “abc”;
String s2 = new String(“abc”);
System.out.println(s == s1.intern());
System.out.println(s == s2.intern());
System.out.println(s1 == s2.intern());
A:false,false,true。

原文連結:https://blog.csdn.net/soonfly/article/details/70147205

以下是博主補充:

String 在 Java 6 以後提供了 intern() 方法,目的是提示 JVM 把相應字串快取起來,以備重複使用。在我們建立字串物件並呼叫 intern() 方法的時候,如果已經有快取的字串,就會返回快取裡的例項,否則將其快取起來。一般來說,JVM 會將所有的類似“abc”這樣的文字字串,或者字串常量之類快取起來。

看起來很不錯是吧?但實際情況估計會讓你大跌眼鏡。一般使用 Java 6 這種歷史版本,並不推薦大量使用 intern,為什麼呢?魔鬼存在於細節中,被快取的字串是存在所謂 PermGen 裡的,也就是臭名昭著的“永久代”,這個空間是很有限的,也基本不會被 FullGC 之外的垃圾收集照顧到。所以,如果使用不當,OOM 就會光顧。

在後續版本中,這個快取被放置在堆中,這樣就極大避免了永久代佔滿的問題,甚至永久代在 JDK 8 中被 MetaSpace(後設資料區)替代了。而且,預設快取大小也在不斷地擴大中,從最初的 1009,到 7u40 以後被修改為 60013。你可以使用下面的引數直接列印具體數字,可以拿自己的 JDK 立刻試驗一下。

-XX:+PrintStringTableStatistics
  •  

你也可以使用下面的 JVM 引數手動調整大小,但是絕大部分情況下並不需要調整,除非你確定它的大小已經影響了操作效率。

-XX:StringTableSize=N
  •  

Intern 是一種顯式地排重機制,但是它也有一定的副作用,因為需要開發者寫程式碼時明確呼叫,一是不方便,每一個都顯式呼叫是非常麻煩的;另外就是我們很難保證效率,應用開發階段很難清楚地預計字串的重複情況,有人認為這是一種汙染程式碼的實踐。

幸好在 Oracle JDK 8u20 之後,推出了一個新的特性,也就是 G1 GC 下的字串排重。它是通過將相同資料的字串指向同一份資料來做到的,是 JVM 底層的改變,並不需要 Java 類庫做什麼修改。

注意這個功能目前是預設關閉的,你需要使用下面引數開啟,並且記得指定使用 G1 GC:

-XX:+UseStringDeduplication

相關文章