故事起源于書籍《深入了解Java虛拟機》,案例如下:
public class RunTimeConstantPoolOOM {
public static void main(String[] args) throws Throwable {
String str1 = new StringBuilder("計算機").append("軟體").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
}
這段代碼在JDK1.6中執行會得到兩個false,在JDK1.7中會得到一個true和一個false。筆者未在JDK1.7執行,選擇的是在JDK1.8中執行,結果是和1.7是一樣的。書中是這樣解釋産生差異的原因:在JDK1.6中,intern()方法會把首次遇到的字元串執行個體複制到永久代,傳回的也是永久代中這個字元串執行個體的引用,而由StringBuilder建立的字元串執行個體在Java堆上,是以必然不是同一個引用,将傳回false。而JDK1.7(以及部分其他虛拟機,例如JRockit)的intern()實作不會再複制執行個體,隻是在常量池中記錄首次出現的執行個體引用,是以intern()傳回的引用和StringBuilder建立的那個字元串執行個體是同一個。對str2比較傳回false是因為“java”這個字元串在執行StringBuilder.toString()之前已經出現過,字元串常量池中已經有它的引用了,不符合“首次出現”的原則,而“計算機軟體”這個字元串則是首次出現的,是以傳回true。
筆者不知道各位其他讀者在閱讀此書時,對這段話是不是一下子就明白了,反正筆者是有些不太明白的,尤其是JDK1.7下str2傳回false這段。是以筆者覺得對這部分内容需要學習一下。
在學習之前,先了解下如下基礎知識:String.intern()方法作用、虛拟機記憶體劃分、永久代和元空間。
String.intern()方法作用
String.intern()是一個native方法,它的作用是:如果字元串常量池中已經包含一個等于此String對象的字元串,則傳回代表池中這個字元串的String對象;否則,将此String對象包含的字元串添加到常量池中,并且傳回此String對象的引用。查閱openjdk6、openjdk8的intern方法源碼(hotspot\src\share\vm\classfile\symbolTable.cpp)實作可以證明上述這句話。
虛拟機記憶體劃分

圖一 虛拟機記憶體區域
圖二 JDK1.6之前虛拟機記憶體區域細化
圖三 JDK1.6之前虛拟機記憶體區域繼續細化
程式計數器:程式計數器是一塊較小的記憶體,它可以看做是目前線程執行的位元組碼行号訓示器。在虛拟機概念模型中,位元組碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令。由于JVM的多線程就是通過輪流切換并配置設定處理器的執行時間的方式來實作的,在任何一個确定的時刻,一個處理器都隻會執行一條線程的指令。是以為了線程切換後能恢複到正确的執行位置,每條線程都有自己的程式計數器。
虛拟機棧:虛拟機棧和程式計數器一樣,也是線程私有的。虛拟機棧描述的是Java方法執行的記憶體模型,即每個方法在執行的時候都會建立一個棧幀用于存儲局部變量表、操作數棧、動态連結、方法出口等資訊。其中局部變量表存放的是編譯器可知的各種基本資料類型和對象引用。
本地方法棧:本地方法棧與虛拟機棧所發揮的作用非常類似,差別不過是虛拟機棧為虛拟機執行Java方法服務,而本地方法棧則為虛拟機使用到的Native方法服務。
堆:Java堆是Java虛拟機所管理的記憶體最大的一塊,Java堆被所有的線程共享。所有的對象執行個體以及數組都要在堆上配置設定。從記憶體回收的角度看,堆可以細分為新生代和老年代(見圖二、圖三堆空間的介紹)。再細化點,新生代又可以分為Eden、From Survivor、To Survivor(見圖二、圖三堆空間的介紹)。
方法區:方法區和堆一樣,是各個線程共享的記憶體區域,用于存儲被虛拟機加載的類資訊、常量、靜态變量、即時編譯後的代碼等資料,對于習慣在HotSpot虛拟機上開發的開發者來說,方法區被習慣性的稱為“永久代”(見圖三的方法區),JDK1.8後已經移除永久代,取而代之的是元空間。
運作時常量池:運作時常量池是方法區的一部分。用于存放編譯器生成的各種字面量和符号引用。
永久代和元空間
以HotSpot虛拟機為例,在1.6、1.7和1.8版本中,對于堆的實作沒有太大的差異,主要分為年輕代和年老代。但是對于方法區(即永久代)的實作存在着差異,移除永久代的工作從1.7開始,但是并未完全移除,永久代仍然存在1.7中,但是其中的符号引用、字面量和類的靜态變量都轉移到了堆,直到1.8才完全移除永久代,取而代之的是元空間。以運作時常量池為例,調用方法String.intern(),在各個版本指定JVM參數,執行的結果略有差異,案例代碼如下:
public static void main(String[] args) throws Throwable {
List<String> list = new ArrayList<String>();
String base = "string";
for (int i=0;i< Integer.MAX_VALUE;i++){
String str = base + base;
base = str;
list.add(str.intern());
}
}
在1.6指定參數:-XX:PermSize=10m -XX:MaxPermSize=10m,執行結果如下:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at com.lingjiango.oom.RunTimeConstantPoolOOM.main(RunTimeConstantPoolOOM.java:29)
在1.7指定參數:-XX:PermSize=10m -XX:MaxPermSize=10m,執行結果如下:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2367)
at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:130)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:114)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:415)
at java.lang.StringBuilder.append(StringBuilder.java:132)
at com.lingjiango.oom.RunTimeConstantPoolOOM.main(RunTimeConstantPoolOOM.java:27)
在1.8指定參數:-XX:PermSize=10m -XX:MaxPermSize=10m,執行結果如下:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Unknown Source)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(Unknown Source)
at java.lang.AbstractStringBuilder.append(Unknown Source)
at java.lang.StringBuilder.append(Unknown Source)
at com.lingjiango.oom.RunTimeConstantPoolOOM.main(RunTimeConstantPoolOOM.java:28)
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=10m; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=10m; support was removed in 8.0
從上述結果可以看出,1.6下,會出現“PermGen Space”的記憶體溢出,而在 1.7和 1.8 中,會出現堆記憶體溢出,并且1.8中提示PermSize 和 MaxPermSize已經不再支援。是以,可以大緻驗證 1.7和1.8 将字面量由永久代轉移到堆中,并且 1.8 中已經不存在永久代的結論。
有了以上概念,再畫圖了解如上第二段話。
圖四 JDK1.6示意圖
圖五 JDK1.8示意圖
從圖四可以看出s1和s1.intern不是一個對象,s2和s2.intern不是一個對象,是以結論是false,而圖五中s1和s1.intern是一個對象,s2和s2.intern不是一個對象,是以前者結論為true,後者為false。但是筆者在了解這段話的時候,還有一個點沒了解到的就是為什麼“計算機軟體”是第一次出現,而“java”卻不是第一次出現呢?如果隻是從這段代碼中是無法了解這句話的,必須從全局來看,虛拟機在啟動加載的時候,自動調用System類,System類會調用sun.misc.Version.init(),而在Version方法中,就有字元串常量“java”,是以在這段代碼中,“java”不是第一次出現。
private static final String launcher_name = "java";
private static final String java_version = "1.8.0_181";
private static final String java_runtime_name = "Java(TM) SE Runtime Environment";
private static final String java_profile_name = "";
private static final String java_runtime_version = "1.8.0_181-b13";
參考資料:
《深入了解Java虛拟機》
http://www.importnew.com/14142.html
https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf
https://docs.oracle.com/javase/specs/jvms/se6/html/VMSpecTOC.doc.html
https://www.cnblogs.com/snowwhite/p/9532311.html
https://www.cnblogs.com/paddix/p/5309550.html