1. 程式人生 > >Knowledge Point 20180309 字符串常量池與String,intern()

Knowledge Point 20180309 字符串常量池與String,intern()

blog tint pre 將在 image 會有 一個 即使 不可變

引言

  什麽都先不說,先看下面這個引入的例子:

public static void test4(){
        String str1 = new String("SEU") + new String("Calvin");
        System.out.println(str1.intern() == str1);
        System.out.println(str1 == "SEUCalvin");
    }

技術分享圖片

  再將上面的例子加上一行代碼:

public static void test5(){
        String str2 
= "SEUCalvin"; String str1 = new String("SEU") + new String("Calvin"); System.out.println(str1.intern() == str1); System.out.println(str1 == "SEUCalvin"); }

技術分享圖片

  是不是感覺莫名其妙,新定義的str2好像和str1沒有半毛錢的關系,怎麽會影響到有關str1的輸出結果呢?其實這都是intern()方法搞的鬼!看完這篇文章,你就會明白。

  這中間的原因和Hotspot是有很大關系的,在JVM運行時數據區中的方法區有一個常量池,但是發現在JDK1.6以後常量池被放置在了堆空間,因此常量池位置的不同影響到了String的intern()方法的表現。

1.為什麽要介紹intern()方法

  intern()方法設計的初衷,就是重(chong)用String對象,以節省內存消耗。這麽說可能有點抽象,那麽就用例子來證明。

package cn.stringPractise.create;
static final int MAX = 100000;  
    static final String[] arr = new String[MAX];  
      
    public static void main(String[] args) throws Exception {  
        //為長度為10的Integer數組隨機賦值  
Integer[] sample = new Integer[10]; Random random = new Random(1000); for (int i = 0; i < sample.length; i++) { sample[i] = random.nextInt(); } //記錄程序開始時間 long t = System.currentTimeMillis(); //使用/不使用intern方法為10萬個String賦值,值來自於Integer數組的10個數 for (int i = 0; i < MAX; i++) { arr[i] = new String(String.valueOf(sample[i % sample.length])); //arr[i] = new String(String.valueOf(sample[i % sample.length])).intern(); 通過intern重復利用已經存在的字符串對象,這樣垃圾回收器能夠回收更多沒有指向的對象 } System.out.println((System.currentTimeMillis() - t) + "ms"); System.gc(); }

  這個例子也比較簡單,就是為了證明使用intern()比不使用intern()消耗的內存更少。

  先定義一個長度為10的Integer數組,並隨機為其賦值,在通過for循環為長度為10萬的String對象依次賦值,這些值都來自於Integer數組。兩種情況分別運行,可通過Window ---> Preferences --> Java --> Installed JREs設置JVM啟動參數為-agentlib:hprof=heap=dump,format=b,將程序運行完後的hprof置於工程目錄下。再通過MAT插件查看該hprof文件。
兩次實驗結果如下:

技術分享圖片

技術分享圖片

  從運行結果來看,不使用intern()的情況下,程序生成了101924個String對象,而使用了intern()方法時,程序僅生成了1934個String對象(多次測試結果可能不同)。自然也證明了intern()節省內存的結論。細心的同學會發現使用了intern()方法後程序運行時間有所增加。這是因為程序中每次都是用了new String後又進行intern()操作的耗時時間,但是不使用intern()占用內存空間導致GC的時間是要遠遠大於這點時間的。

2. 深入認識intern()方法

  查看API對於intern()的解釋如下:

  String intern() 返回字符串對象的規範表示。【具體是什麽意思,我們將在一系列的例子後總結說明,有助於理解】

  JDK1.7後,常量池被放入到堆空間中,這導致intern()函數的功能不同,具體怎麽個不同法,且看看下面代碼,這個例子是網上流傳較廣的一個例子,分析圖也是直接粘貼過來的,這裏我會用自己的理解去解釋這個例子:

String s = new String("1");  
    s.intern();  
    String s2 = "1";  
    System.out.println(s == s2);  
    String s3 = new String("1") + new String("1");  
    s3.intern();  
    String s4 = "11";  
    System.out.println(s3 == s4);  
  1. JDK1.6以及以下:false false
  2. JDK1.7以及以上:false true

再分別調整上面代碼2.3行、7.8行的順序:

1.String s = new String("1");  
2.String s2 = "1";  
3.s.intern();  
4.System.out.println(s == s2);  
5.  
6.String s3 = new String("1") + new String("1");  
7.String s4 = "11";  
8.s3.intern();  
9.System.out.println(s3 == s4);  

輸出結果為:

  1.JDK1.6以及以下:false false

  2.JDK1.7以及以上:false false

下面依據上面代碼對intern()方法進行分析:

1.2.1 JDK1.6

技術分享圖片

  在JDK1.6中所有的輸出結果都是 false,因為JDK1.6以及以前版本中,常量池是放在 Perm 區(方法區,也叫永久代)中的,熟悉JVM的話應該知道這是和堆區完全分開的。使用引號聲明的字符串都是會直接在字符串常量池中生成的,而 new 出來的 String 對象是放在堆空間中的。所以兩者的內存地址肯定是不相同的,即使調用了intern()方法也是不影響的。intern()方法在JDK1.6中的作用是:比如String s = new String("SEU_Calvin"),再調用s.intern(),此時返回值還是字符串"SEU_Calvin",表面上看起來好像這個方法沒什麽用處。但實際上,在JDK1.6中它做了個小動作:檢查字符串池裏是否存在"SEU_Calvin"這麽一個字符串,如果存在,就返回池裏的字符串;如果不存在,該方法會把"SEU_Calvin"添加到字符串池中,然後再返回它的引用。然而在JDK1.7中卻不是這樣的,後面會討論。

1.2.2 JDK1.7

  針對JDK1.7以及以上的版本,我們將上面兩段代碼分開討論。先看第一段代碼的情況:

技術分享圖片

再把第一段代碼貼一下便於查看:

    String s = new String("1");  
    s.intern();  
    String s2 = "1";  
    System.out.println(s == s2);  
    String s3 = new String("1") + new String("1");  
    s3.intern();  
    String s4 = "11";  
    System.out.println(s3 == s4);  
    1. JDK1.6以及以下:false false
    2. JDK1.7以及以上:false true
  1. String s = newString("1"),生成了常量池中的“1” 和堆空間中的字符串對象。
  2. s.intern(),這一行的作用是s對象去常量池中尋找後發現"1"已經存在於常量池中了。
  3. String s2 = "1",這行代碼是生成一個s2的引用指向常量池中的“1”對象。
  4. 結果就是 s 和 s2 的引用地址明顯不同。因此返回了false。
  5. String s3 = new String("1") + newString("1"),這行代碼在字符串常量池中生成“1”,並在堆空間中生成s3引用指向的對象(內容為"11")。註意此時常量池中是沒有 “11”對象的。
  6. s3.intern(),這一行代碼,是將 s3中的“11”字符串放入 String 常量池中,此時常量池中不存在“11”字符串,JDK1.6的做法是直接在常量池中生成一個 "11" 的對象。
  7. 但是在JDK1.7中,常量池中不需要再存儲一份對象了,可以直接存儲堆中的引用。這份引用直接指向 s3 引用的對象,也就是說s3.intern() ==s3會返回true。
  8. String s4 = "11", 這一行代碼會直接去常量池中創建,但是發現已經有這個對象了,此時也就是指向 s3 引用對象的一個引用。因此s3 == s4

技術分享圖片

再把第二段代碼貼一下便於查看:

String s = new String("1");  

String s2 = "1";  

s.intern();  

System.out.println(s == s2);  

String s3 = new String("1") + new String("1");  

String s4 = "11";  

s3.intern();  

System.out.println(s3 == s4); 

  1.JDK1.6以及以下:false false

  2.JDK1.7以及以上:false false

  1. String s = newString("1"),生成了常量池中的“1” 和堆空間中的字符串對象。
  2. String s2 = "1",這行代碼是生成一個s2的引用指向常量池中的“1”對象,但是發現已經存在了,那麽就直接指向了它。
  3. s.intern(),這一行在這裏就沒什麽實際作用了。因為"1"已經存在了。
  4. 結果就是 s 和 s2 的引用地址明顯不同。因此返回了false。
  5. String s3 = new String("1") + newString("1"),這行代碼在字符串常量池中生成“1” ,並在堆空間中生成s3引用指向的對象(內容為"11",實際上s3指向的是一個字符串對象,該對象中是兩個個字符串變量連接運算)。註意此時常量池中是沒有 “11”對象的。
  6. String s4 = "11", 這一行代碼會直接去生成常量池中的"11"。
  7. s3.intern(),這一行在這裏就沒什麽實際作用了。因為"11"已經存在了。
  8. 結果就是 s3 和 s4 的引用地址明顯不同。因此返回了false。

  本文轉載,原文鏈接為:SEU_Calvin的博客

1.3 總結

終於要做Ending了。現在再來看一下開篇給的引入例子,是不是就很清晰了呢。

    String str1 = new String("SEU") + new String("Calvin");        
    System.out.println(str1.intern() == str1);     //true
    System.out.println(str1 == "SEUCalvin");    //true

  str1.intern() == str1就是上面例子中的情況,str1.intern()發現常量池中不存在“SEUCalvin”,因此指向了str1。 "SEUCalvin"在常量池中創建時,也就直接指向了str1了。兩個都返回true就理所當然啦。

那麽第二段代碼呢:

    String str2 = "SEUCalvin";//新加的一行代碼,其余不變  
    String str1 = new String("SEU")+ new String("Calvin");      
    System.out.println(str1.intern() == str1);   //false
    System.out.println(str1 == "SEUCalvin");   //false

  也很簡單啦,str2先在常量池中創建了“SEUCalvin”,那麽str1.intern()當然就直接指向了str2,你可以去驗證它們兩個是返回的true。後面的"SEUCalvin"也一樣指向str2。所以誰都不搭理在堆空間中的str1了,所以都返回了false。

  小結:

  intern()方法在JDK1.6中的作用是:比如String s = new String("SEU_Calvin"),再調用s.intern(),此時返回值還是字符串"SEU_Calvin",表面上看起來好像這個方法沒什麽用處。但實際上,在JDK1.6中它做了個小動作:檢查字符串池裏是否存在"SEU_Calvin"這麽一個字符串,如果存在,就返回池裏的字符串;如果不存在,該方法會把"SEU_Calvin"添加到字符串池中,然後再返回它的引用。

  JDK1.7中,string.Intern()會先檢查在字符串常量池中有沒有這個字符串,如果沒有,那麽在字符串常量池中村存入一個指針指向堆中的String對象,如果字符串常量池中有這個字符串,那麽會返回字符串常量池中這個對象地址.

  1. String str1 = new String("SEU") + new String("Calvin");

  語句在內存中是什麽樣的呢?因為String是不可改變的,所以Str1相當於創建了5個對象,兩個new,兩個在字符串常量池,一個是str1的指向,這個指向的內容並不是SEUCalvin,而是一個計算new String("SEU") + new String("Calvin")的計算,也就是會有這兩個new對象的地址,還有運算法則,所以System.out.println(str1.intern() == str1);為true,如果是下面情況:

  String str1 = new String("SEUCalvin");

  System.out.println(str1.intern() == str1);

那麽結果就是false,因為第一句會在字符串常量池中創建SEUCalvin字符串對象,str1.intern()後地址就變成了字符串常量池中的地址,兩個地址這時變不一樣了.

  使用String.intern()能夠重復使用在內存中已經存在的相同的字符串對象,這樣避免了相同對象分配內存造成的內存的浪費,又因為String是不可變的,所以在兩個索引指向同一個String對象時,這時如果另一個對象改變String,實際上是重新創建了一個String對象,也不會因為一個索引的操作修改而影響另一個索引指向的String發生改變;

Knowledge Point 20180309 字符串常量池與String,intern()