運行時常量池是方法區的一部分,方法區用於存放Class的相關信息,如類名、訪問修飾符、常量池、字段描述、方法描述等。
String.intern()是一個native方法,它的作用是:如果字符串常量池中已經包含了一個等於此String對象的字符串,則返回代表池中這個字符串的String對象;否則,將此String對象包含的字符串添加到常量池中,並返回此String對象的引用。在JDK1.6及之前版本中,由於常量池分配在永久代中(即方法區),我們可以通過-XX:PermSize和-XX:MaxPermSize限制方法區大小,從而間接限制其中常量池的容量,注意,JDK1.7開始逐步開始“去永久代”。代碼如下所示:
package jvm; import java.util.ArrayList; import java.util.List; /* * VM Args: -XX:PermSize=10m -XX:MaxPermSize=10m */ public class RuntimeConstantPoolOOM { public static void main(String[] args) { // 使用List保持著常量池引用,避免Full GC回收常量池行為 List<String> list = new ArrayList<String>(); int i = 0; while (true) { list.add(String.valueOf(i++).intern()); } } }
注意,VM Args為配置VM的參數,在下圖所示中配置:
運行結果:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space at java.lang.String.intern(Native Method) at jvm.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:16)
從運行結果中可以看到,運行時常量池溢出,在OutOfMemoryError後面跟隨的提示信息是“PermGen space”,說明運行時常量池屬於方法區(HotSpot虛擬機中的永久代)的一部分。但是使用JDK1.7運行這段程序不會得到相同的結果,而是出現以下的提示信息,這是因為這兩個參數已經不在JDK1.7中使用了。
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
如果在JDK1.7中運行RuntimeConstantPoolOOM.java程序,while循環將一直運行下去,但是,while循環並不是始終運行下去,直到系統中堆內存用完為止,一般需要過好長時間才會出現,不過筆者並沒有在本地測試。因為在JDK1.7中常量池存儲的不再是對象,而是對象引用,真正的對象是存儲在堆中的。把RuntimeConstantPoolOOM.java運行時的VM參數改為如下所示:
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
運行程序後結果:
出現異常提示信息:java.lang.OutOfMemoryError: GC overhead limit exceeded,這裡沒有提示說堆還是持久代有問題,虛擬機只是告訴你你的程序花在垃圾回收上的時間太多了,卻沒有什麼見效。默認的話,如果你98%的時間都花在GC上並且回收了才不到2%的空間的話,虛擬機才會拋這個異常。這是一個快速失敗的安全保障的很好的實踐。從運行結果中可以看出, 我們限定了堆的大小後,程序很快就運行異常了,異常信息和之前設想的一樣,也就是常量池存儲的不再是對象,而是對象引用,真正的對象是存儲在堆中的。關於JDK1.7字符串常量池的實現問題,這裡還可以引申一個更有意義的影響,如以下代碼所示:
package jvm; public class Hello { public static void main(String[] args) { 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.6中,intern()方法會把首次遇到的字符串復制到永久代中,返回的也是永久代中這個字符串的引用,而由StringBuilder創建的字符串實例在Java堆中,所以必然不是同一個引用,將返回false。而JDK1.7(以及部分其他虛擬機,例如JRockit)的intern()實現不會再復制實例,而是在常量池中記錄首次出現的實例引用,因此intern()返回的引用和由StringBuilder創建的那個字符串是同一個。對str2比較返回false是因為"java"字符串在執行StringBuilder()之前就已經出現過,字符串常量池中已經有它的引用了,不符合“首次出現”原則,而“計算機軟件”這個字符串則是首次出現的,因此返回true。如果在Hello.java中添加如下代碼的話,返回的結果也是false,證明"main"字符串之前也出現過了。
String str3 = new StringBuilder("ma").append("in").toString(); System.out.println(str3.intern() == str3);