0. Background
在 JAVA 語言中有8中基本類型和一種比較特殊的類型String。這些類型為了使他們在運行過程中速度更快,更節(jié)省內(nèi)存,都提供了一種常量池的概念。常量池就類似一個JAVA系統(tǒng)級別提供的緩存。
8種基本類型的常量池都是系統(tǒng)協(xié)調(diào)的,String類型的常量池比較特殊。
它的主要使用方法有兩種:
直接使用雙引號聲明出來的String對象會直接存儲在常量池中。
如果不是用雙引號聲明的String對象,可以使用String提供的intern方法。intern 方法會從字符串常量池中查詢當(dāng)前字符串是否存在,若不存在就會將當(dāng)前字符串放入常量池中
1. 常量池
1.1 常量池是什么?

JVM常量池主要分為Class文件常量池、運行時常量池,全局字符串常量池,以及基本類型包裝類對象常量池。
1.1.0 方法區(qū)
方法區(qū)的作用是存儲Java類的結(jié)構(gòu)信息,當(dāng)創(chuàng)建對象后,對象的類型信息存儲在方法區(qū)中,實例數(shù)據(jù)存放在堆中。類型信息是定義在Java代碼中的常量、靜態(tài)變量、以及類中聲明的各種方法,方法字段等;實例數(shù)據(jù)則是在Java中創(chuàng)建的對象實例以及他們的值。
該區(qū)域進(jìn)行內(nèi)存回收的主要目的是對常量池的回收和對內(nèi)存數(shù)據(jù)的卸載;一般說這個區(qū)域的內(nèi)存回收率比起Java堆低得多。
1.1.1 Class文件常量池
class文件是一組以字節(jié)為單位的二進(jìn)制數(shù)據(jù)流,在Java代碼的編譯期間,我們編寫的Java文件就被編譯為.class文件格式的二進(jìn)制數(shù)據(jù)存放在磁盤中,其中就包括class文件常量池。
class文件常量池主要存放兩大常量:字面量和符號引用。
文本字符串,也就是我們經(jīng)常申明的:public String s = "abc";中的"abc"
用final修飾的成員變量,包括靜態(tài)變量、實例變量和局部變量:public final static int f = 0x101;,final int temp = 3;
而對于基本類型數(shù)據(jù)(甚至是方法中的局部變量),如int value = 1常量池中只保留了他的的字段描述符int和字段的名稱value,他們的字面量不會存在于常量池。
類和接口的全限定名,也就是java/lang/String;這樣,將類名中原來的".“替換為”/"得到的,主要用于在運行時解析得到類的直接引用
字段的名稱和描述符,字段也就是類或者接口中聲明的變量,包括類級別變量和實例級的變量
1.1.2 運行時常量池
在解析階段,會把符號引用替換為直接引用,解析的過程會去查詢字符串常量池,也就StringTable,以保證運行時常量池所引用的字符串與字符串常量池中是一致的。
1.1.3 字符串常量池
字符串的分配,和其他的對象分配一樣,耗費高昂的時間與空間代價,作為最基礎(chǔ)的數(shù)據(jù)類型,大量頻繁的創(chuàng)建字符串,極大程度地影響程序的性能
JVM為了提高性能和減少內(nèi)存開銷,在實例化字符串常量的時候進(jìn)行了一些優(yōu)化
為字符串開辟一個字符串常量池,類似于緩存區(qū)
創(chuàng)建字符串常量時,首先查看字符串常量池是否存在該字符串
存在該字符串,返回引用實例,不存在,實例化該字符串并放入池中
實現(xiàn)該優(yōu)化的基礎(chǔ)是因為字符串是不可變的,可以不用擔(dān)心數(shù)據(jù)沖突進(jìn)行共享
2. String.intern()與字符串常量池

字符串常量池的位置也是隨著jdk版本的不同而位置不同。在jdk6中,常量池的位置在永久代(方法區(qū))中,此時常量池中存儲的是對象。在jdk7中,常量池的位置在堆中,此時,常量池存儲的就是引用了。
在jdk8中,永久代(方法區(qū))被元空間取代了。這里就引出了一個很常見很經(jīng)典的問題,看下面這段代碼

這段代碼在jdk6中輸出是false false,但是在jdk7中輸出的是false true。我們通過圖來一行行解釋。
JDK1.6

String s = new String("2");創(chuàng)建了兩個對象,一個在堆中的StringObject對象,一個是在常量池中的“2”對象。
s.intern();在常量池中尋找與s變量內(nèi)容相同的對象,發(fā)現(xiàn)已經(jīng)存在內(nèi)容相同對象“2”,返回對象2的地址。
String s2 = "2";使用字面量創(chuàng)建,在常量池尋找是否有相同內(nèi)容的對象,發(fā)現(xiàn)有,返回對象"2"的地址。
System.out.println(s == s2);從上面可以分析出,s變量和s2變量地址指向的是不同的對象,所以返回false
String s3 = new String("3") + new String("3");創(chuàng)建了兩個對象,一個在堆中的StringObject對象,一個是在常量池中的“3”對象。中間還有2個匿名的new String(“3”)我們不去討論它們。
s3.intern();在常量池中尋找與s3變量內(nèi)容相同的對象,沒有發(fā)現(xiàn)“33”對象,在常量池中創(chuàng)建“33”對象,返回“33”對象的地址。
String s4 = "33";使用字面量創(chuàng)建,在常量池尋找是否有相同內(nèi)容的對象,發(fā)現(xiàn)有,返回對象"33"的地址。
System.out.println(s3 == s4);從上面可以分析出,s3變量和s4變量地址指向的是不同的對象,所以返回false
JDK1.7

String s = new String("2");創(chuàng)建了兩個對象,一個在堆中的StringObject對象,一個是在堆中的“2”對象,并在常量池中保存“2”對象的引用地址。
s.intern();在常量池中尋找與s變量內(nèi)容相同的對象,發(fā)現(xiàn)已經(jīng)存在內(nèi)容相同對象“2”,返回對象“2”的引用地址。
String s2 = "2";使用字面量創(chuàng)建,在常量池尋找是否有相同內(nèi)容的對象,發(fā)現(xiàn)有,返回對象“2”的引用地址。
System.out.println(s == s2);從上面可以分析出,s變量和s2變量地址指向的是不同的對象,所以返回false
String s3 = new String("3") + new String("3");創(chuàng)建了兩個對象,一個在堆中的StringObject對象,一個是在堆中的“3”對象,并在常量池中保存“3”對象的引用地址。中間還有2個匿名的new String(“3”)我們不去討論它們。
s3.intern();在常量池中尋找與s3變量內(nèi)容相同的對象,沒有發(fā)現(xiàn)“33”對象,將s3對應(yīng)的StringObject對象的地址保存到常量池中,返回StringObject對象的地址。
String s4 = "33";使用字面量創(chuàng)建,在常量池尋找是否有相同內(nèi)容的對象,發(fā)現(xiàn)有,返回其地址,也就是StringObject對象的引用地址。
System.out.println(s3 == s4);從上面可以分析出,s3變量和s4變量地址指向的是相同的對象,所以返回true。
3. String.intern()的應(yīng)用
在大量字符串讀取賦值的情況下,使用String.intern()會大大的節(jié)省內(nèi)存空間。

不當(dāng)?shù)氖褂茫篺astjson 中對所有的 json 的 key 使用了 intern 方法,緩存到了字符串常量池中,這樣每次讀取的時候就會非???,大大減少時間和空間,而且 json 的 key 通常都是不變的。但是這個地方?jīng)]有考慮到大量的 json key 如果是變化的,那就會給字符串常量池帶來很大的負(fù)擔(dān)。