字符串常量池理解

在JVM中,爲了減小字符串對象的重複建立,維護了一塊特殊的內存空間,這塊內存就被稱爲字符串常量池。面試

在JDK1.6及以前,字符串常量池存放在方法區中。到JDK1.7以後,就從方法區中移除了,而存放在堆中。如下是《深刻理解Java虛擬機》第二版原文:segmentfault

對於HotSpot虛擬機,根據官方發佈的路線圖信息,如今也有放棄永久代並逐步改成採用Native Memory來實現方法區的規劃了,在目前已經發布的JDK1.7 的HotSpot中,已經把本來放在永久代的字符串常量池移出。

咱們知道字符串常量通常有兩種建立方式:app

  1. 使用字符串字面量定義
String s = "aa";
  1. 經過new建立字符串對象
String s = new String("aa");

那這兩種方式有什麼區別呢?優化

第一種方式經過字面量定義一個字符串時,JVM會先去字符串常量池中檢查是否存在「aa」這個對象。若是不存在,則在字符串常量池中建立「aa」對象,並將引用返回給s,這樣s的引用就指向字符串常量池中的「aa」對象。若是存在,則不建立任何對象,直接把常量池中「aa」對象的地址返回,賦值給s。ui

第二種方式經過new關鍵字建立一個字符串時,咱們須要知道建立了幾個對象,這也是面試中常常問到的。首先,會在字符串常量池中建立一個"aa"對象。而後執行new String時會在堆中建立一個「aa」的對象,而後把s的引用指向堆中的這個「aa」對象。spa

思考如下代碼的打印結果:code

public class StringTest {
    public static void main(String[] args) {
        //建立了兩個對象,一份存在字符串常量池中,一份存在堆中
        String s = new String("aa");
        //檢查常量池中是否存在字符串aa,此處存在則直接返回
        String s1 = s.intern();
        String s2 = "aa";

        System.out.println(s == s2);  //①
        System.out.println(s1 == s2); //②

        String s3 = new String("b") + new String("b");
        //常量池中沒有bb,在jdk1.7以後會把堆中的引用放到常量池中,故引用地址相等
        String s4 = s3.intern();
        String s5 = "bb";

        System.out.println(s3 == s5 ); //③
        System.out.println(s4 == s5);  //④

    }
}

以上的①②③④四個地方應該輸出true仍是false呢?彆着急,先看下,代碼中用到了intern方法。這個方法的做用是,在運行期間能夠把新的常量放入到字符串常量池中。對象

看下String源碼中對intern方法的解釋:blog

file

字面意思就是,當調用這個方法時,會去檢查字符串常量池中是否已經存在這個字符串,若是存在的話,就直接返回,若是不存在的話,就把這個字符串常量加入到字符串常量池中,而後再返回其引用。內存

可是,其實在JDK1.6和 JDK1.7的處理方式是有一些不一樣的。

在JDK1.6中,若是字符串常量池中已經存在該字符串對象,則直接返回池中此字符串對象的引用。不然,將此字符串的對象添加到字符串常量池中,而後返回該字符串對象的引用。

在JDK1.7中,若是字符串常量池中已經存在該字符串對象,則返回池中此字符串對象的引用。不然,若是堆中已經有這個字符串對象了,則把此字符串對象的引用添加到字符串常量池中並返回該引用,若是堆中沒有此字符串對象,則先在堆中建立字符串對象,再返回其引用。(這也說明,此時字符串常量池中存儲的是對象的引用,而對象自己存儲於堆中)

因而代碼中,String s = new String("aa");建立了兩個「aa」對象,一個存在字符串常量池中,一個存在堆中。

String s1 = s.intern(); 因爲字符串常量池中已經存在「aa」對象,因而直接返回其引用,故s1指向字符串常量池中的對象。

String s2 = "aa"; 此時字符串常量池中已經存在「aa」對象,因此也直接返回,故 s2和 s1的地址相同。②返回true。

System.out.println(s == s2); 因爲s的引用指向的是堆中的「aa」對象,s2指向的是常量池中的對象。故不相等,①返回false。

String s3 = new String("b") + new String("b"); 先說明一下,這種形式的字符串拼接,等同於使用StringBuilder的append方法把兩個「b」拼接,而後調用toString方法,new出「bb」對象,所以「bb」對象是在堆中生成的。因此,這段代碼最終生成了兩個對象,一個是「b」對象存在於字符串常量池中,一個是 「bb」對象,存在於堆中,可是此時字符串常量池中是沒有「bb」對象的。 s3指向的是堆中的「bb」對象。

String s4 = s3.intern(); 調用了intern方法以後,在JDK1.6中,因爲字符串常量池中沒有「bb」對象,故建立一個「bb」對象,而後返回其引用。因此 s4 這個引用指向的是字符串常量池中新建立的「bb」對象。在JDK1.7中,則把堆中「bb」對象的引用添加到字符串常量池中,故s4和s3所指向的對象是同一個,都指向堆中的「bb」對象。

String s5 = "bb"; 在JDK1.6中,指向字符串常量池中的「bb」對象的引用,在JDK1.7中指向的是堆中「bb」對象的引用。

System.out.println(s3 == s5 ); 參照以上分析便可知道,在JDK1.6中③返回false(由於s3指向的是堆中的「bb」對象,s5指向的是字符串常量池中的「bb」對象),在JDK1.7中,③返回true(由於s3和s5指向的都是堆中的「bb」對象)。

System.out.println(s4 == s5); 在JDK1.6中,s4和s5指向的都是字符串常量池中建立的「bb」對象,在JDK1.7中,s4和s5指向的都是堆中的「bb」對象。故不管JDK版本如何,④都返回true。

綜上,在JDK1.6中,返回的結果爲:

false
true
false
true

在JDK1.7中,返回結果爲:

false
true
true
true

以上,能夠在JDK1.7和JDK1.6中分別驗證。注意一下,最好搞兩個項目而後分別設置不一樣的JDK,由於若是在一個項目中直接更改JDK版本,有可能高版本編譯以後,低版本編譯不經過。

原理搞懂了,咱們再思考一下如下代碼的結果:

public class InternTest {
    public static void main(String[] args) {
        String str1 = "xy";
        String str2 = "z";
        String str3 = "xyz";
        String str4 = str1 + str2;
        String str5 = str4.intern();
        String str6 = "xy" + "z";

        System.out.println(str3 == str4); //⑤
        System.out.println(str3 == str5); //⑥
        System.out.println(str3 == str6); //⑦
    }
}

咱們分析一下。

str一、str2和str3都是簡單的定義字符串,全部它們都是在字符串常量池中建立對象,而後引用指向字符串常量池中的對象。

String str4 = str1 + str2; 這段代碼和以前的 String s3 = new String("b") + new String("b"); 原理相同,所以在堆中建立了一個「xyz」對象,而後str4指向堆中的這個對象。故⑤處返回false。(str3指向的是字符串常量池中的「xyz」對象)

String str5 = str4.intern(); 因爲字符串常量池中已經存在「xyz」對象,所以不管是JDK1.6仍是JDK1.7,此處返回的都是字符串常量池中對象的引用。因此str5指向字符串常量池中的對象,故 ⑥返回true。

String str6 = "xy" + "z"; 這段代碼須要說明一下,它不一樣於兩個字符串的引用拼接(如str1 + str2)。JVM會對其優化處理,也就是在編譯階段會把「xy」和「z」進行拼接成爲「xyz」,存放在字符串常量池。所以,str6指向的是字符串常量池的對象,故⑦返回true。

相關文章
相關標籤/搜索