JVM - 深入剖析字符串常量池
文章目錄
- 字符串常量池
- 位置的變更
- JVM對字符串常量池的優化
- 字符串的常見創建方式 (1.7+)
- 直接賦值字符串
- new String()
- intern
- 經典面試題
- 下列代碼創建幾個對象
- 案例
- 案例
- 案例
- 案例
- 案例
- 案例
- 案例
- 案例
字符串常量池
位置的變更
- Jdk1.6及之前: JVM存在永久代, 運行時常量池在永久代,運行時常量池包含字符串常量池
- Jdk1.7:有永久代,但已經逐步“去永久代”,字符串常量池從永久代里的運行時常量池分離到堆里
- Jdk1.8及之后: 無永久代,變成了元空間,運行時常量池在元空間,字符串常量池里依然在堆里
看 1.8 , 瘋狂的intern, 拋出了 heap oom ,由此可以推斷出 1.8中的字符串常量池 是在堆中。
如果是1.6 ,會拋出 OutOfMemoryError: PermGen space ,因為1.6中 字符串常量池在永久代中。
JVM對字符串常量池的優化
字符串的分配,和其他對象的分配一樣,同樣也需要耗費時間和空間,作為最基礎的數據類型,大量頻繁的常講字符串,極大程度上影響了程序的性能。
JVM為了提高性能和減少內存開銷,在實例化字符串常量池的時候進行了一些優化,主要有一下幾點
1. 為字符串開辟一個字符串常量池,用于緩存相同的字符串,類似于環村區域
2. 創建字符串常量時,首先要查詢字符串常量池中是否存在該字符串
3. 如若存在該字符串,返回引用實例。 不存在時,實例化該字符串并放入緩存池中。
字符串的常見創建方式 (1.7+)
注意JDK的版本 ,這里討論的是1.7+
直接賦值字符串
String s = "artisan"; // s指向常量池中的引用這種方式創建的字符串對象,只會在常量池中 。
因為有"artisan"這個字面量,創建對象s的時候,JVM會先去常量池中通過 equals(key) 方法,判斷是否有相同的對象
- 若有,直接返回該對象在常量池中的引用
- 若沒有,則會在常量池中創建一個新對象,再返回引用
new String()
String s1 = new String("artisan"); // s1指向內存中的對象引用這種方式會保證字符串常量池和堆中都有這個對象,沒有就創建,最后返回堆內存中的對象引用。
步驟大致如下
因為有"artisan"這個字面量,所以會先檢查字符串常量池中是否存在字符串"artisan"
- 不存在,先在字符串常量池里創建一個字符串對象;再去內存中創建一個字符串對象"artisan";
- 存在的話,就直接去堆內存中創建一個字符串對象"zhuge";
- 最后,將內存中的引用返回
intern
String s1 = new String("artisan");String s2 = s1.intern();System.out.println(s1 == s2); //falseString中的intern方法是一個 native 的方法,當調用 intern方法時,如果池已經包含一個等于此String對象的字符串(用equals(oject)方法確定),則返回池中的字符串, 否則,將intern返回的引用指向當前字符串 s1 。
jdk1.6版本需要將 s1 復制到字符串常量池里
經典面試題
下列代碼創建幾個對象
public static void main(String[] args) {String s1 = new String("he") + new String("llo");String s2 = s1.intern();System.out.println(s1 == s2);}對于JDK1.6 , 字符串常量池位于永久代 。
new String("he") + new String("llo")毫無疑問,堆內存中有兩個,但是JDK
JDK 1.6運行結果 false
案例
String s0="artisan";String s1="artisan";String s2="arti" + "san";System.out.println( s0==s1 ); //trueSystem.out.println( s0==s2 ); //trues0和s1中的”artisan”都是字符串常量,它們在編譯期就被確定了,所以s0==s1為true;
而”arti”和”san”也都是字符串常量,當一個字 符串由多個字符串常量連接而成時,它自己肯定也是字符串常量,所以s2也同樣在編譯期就被優化為一個字符串常量"artisan",所以s2也是常量池中” artisan”的一個引用。
所以我們得出s0==s1==s2
案例
再看看這個
String s = "a" + "b" + "c"; //就等價于String s = "abc"; String a = "a"; String b = "b"; String c = "c"; String s1 = a + b + c;s1 這個就不一樣了,可以通過觀察其JVM指令碼發現s1的"+"操作會變成如下操作:
StringBuilder temp = new StringBuilder(); temp.append(a).append(b).append(c); String s = temp.toString();看看StringBuilder#toString ,其實就是new String , 堆中分配內存地址
案例
String s0="artisan";String s1=new String("artisan");String s2="arti" + new String("san");System.out.println( s0==s1 );// falseSystem.out.println( s0==s2 );// falseSystem.out.println( s1==s2 );// false用new String() 創建的字符串不是常量,不能在編譯期就確定,所以new String() 創建的字符串不放入常量池中,它們有自己的地址空間。
s0還是常量池 中"artisan”的引用,s1因為無法在編譯期確定,所以是運行時創建的新對象”artisan”的引用,s2因為有后半部分 new String(”san”)所以也無法在編譯期確定,所以也是一個新創建對象”artisan”的引用;
明白了這些也就知道為何都是false了
案例
String a = "a1";String b = "a" + 1;System.out.println(a == b); // trueString a = "atrue";String b = "a" + "true";System.out.println(a == b); // trueString a = "a3.4";String b = "a" + 3.4;System.out.println(a == b); // trueJVM對于字符串常量的"+“號連接,將在程序編譯期,JVM就將常量字符串的”+“連接優化為連接后的值,拿"a” + 1來說,經編譯器優化后在class中就已經是a1。
在編譯期其字符串常量的值就確定下來,故上面程序最終的結果都為true。
案例
String a = "ab";String bb = "b";String b = "a" + bb;System.out.println(a == b); // falseJVM對于字符串引用,由于在字符串的"+"連接中,有字符串引用存在,而引用的值在程序編譯期是無法確定的,即"a" + bb無法被編譯器優化,只有在程序運行期來動態分配并將連接后的新地址賦給b。所以上面程序的結果也就為false。
案例
String a = "ab";final String bb = "b";String b = "a" + bb;System.out.println(a == b); // true和示例4中唯一不同的是bb字符串加了final修飾,對于final修飾的變量,它在編譯時被解析為常量值的一個本地拷貝存儲到自己的常量池中或嵌入到它的字節碼流中。所以此時的"a" + bb和"a" + "b"效果是一樣的。故上面程序的結果為true。
案例
String a = "ab";final String bb = getBB();String b = "a" + bb;System.out.println(a == b); // falseprivate static String getBB(){return "b";}JVM對于字符串引用bb,它的值在編譯期無法確定,只有在程序運行期調用方法后,將方法的返回值和"a"來動態連接并分配地址為b,故上面 程序的結果為false。
案例
//字符串常量池:"計算機"和"技術" 堆內存:str1引用的對象"計算機技術" //堆內存中還有個StringBuilder的對象,但是會被gc回收,StringBuilder的toString方法會new String(),這個String才是真正返回的對象引用 String str1 = new StringBuilder("計算機").append("技術").toString(); //沒有出現"計算機技術"字面量,所以不會在常量池里生成"計算機技術"對象System.out.println(str1 == str1.intern()); //true //"計算機技術" 在池中沒有,但是在heap中存在,則intern時,會直接返回該heap中的引用//字符串常量池:"ja"和"va" 堆內存:str1引用的對象"java" //堆內存中還有個StringBuilder的對象,但是會被gc回收,StringBuilder的toString方法會new String(),這個String才是真正返回的對象引用String str1 = new StringBuilder("ja").append("va").toString(); //沒有出現"java"字面量,所以不會在常量池里生成"java"對象System.out.println(str1 == str1.intern()); //false //java是關鍵字,在JVM初始化的相關類里肯定早就放進字符串常量池了String s1 = new String("test");System.out.println(s1 == s1.intern()); //false //"test"作為字面量,放入了池中,而new時s1指向的是heap中新生成的string對象,s1.intern()指向的是"test"字面量之前在池中生成的字符串對象String s2 = new StringBuilder("abc").toString();System.out.println(s2 == s2.intern()); //false //同上總結
以上是生活随笔為你收集整理的JVM - 深入剖析字符串常量池的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java - String源码解析及常见
- 下一篇: JVM - 基本类型的包装类和对象池