Java-String.intern的深入研究


When---什么時候需要了解String的intern方法:

面試的時候(蜜汁尷尬)!雖然不想承認,不過面試的時候經常碰到這種高逼格的問題來考察我們是否真正理解了String的不可變性、String常量池的設計以及String.intern方法所做的事情。但其實,我們在實際的編程中也可能碰到可以利用String.intern方法來提高程序效率或者減少內存占用的情況,這個我們等下會細說。

 

What---String.intern方法究竟做了什么:

Returns a canonical representation for the string object. A pool of strings, initially empty, is maintained privately by the class String. When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned. It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true. All literal strings and string-valued constant expressions are interned. String literals are defined in section 3.10.5 of the The Java? Language Specification.

上面是jdk源碼中對intern方法的詳細解釋。簡單來說就是intern用來返回常量池中的某字符串,如果常量池中已經存在該字符串,則直接返回常量池中該對象的引用。否則,在常量池中加入該對象,然后 返回引用。下面的一個例子詳細的解釋了intern的作用過程:

Now lets understand how Java handles these strings. When you create two string literals:

String name1 = "Ram"; 

String name2 = "Ram";

In this case, JVM searches String constant pool for value "Ram", and if it does not find it there then it allocates a new memory space and store value "Ram" and return its reference to name1. Similarly, for name2 it checks String constant pool for value "Ram" but this time it find "Ram" there so it does nothing simply return the reference to name2 variable. The way how java handles only one copy of distinct string is called String interning.

 

How---String.intern方法在jdk1.7之前和之后的區別:

簡單的說其實就一個:在jdk1.7之前,字符串常量存儲在方法區的PermGen Space。在jdk1.7之后,字符串常量重新被移到了堆中。

 

Back---重回String設計的初衷:

Java中的String被設計成不可變的,出於以下幾點考慮:

1. 字符串常量池的需要。字符串常量池的誕生是為了提升效率和減少內存分配。可以說我們編程有百分之八十的時間在處理字符串,而處理的字符串中有很大概率會出現重復的情況。正因為String的不可變性,常量池很容易被管理和優化。

2. 安全性考慮。正因為使用字符串的場景如此之多,所以設計成不可變可以有效的防止字符串被有意或者無意的篡改。從java源碼中String的設計中我們不難發現,該類被final修飾,同時所有的屬性都被final修飾,在源碼中也未暴露任何成員變量的修改方法。(當然如果我們想,通過反射或者Unsafe直接操作內存的手段也可以實現對所謂不可變String的修改)。

3. 作為HashMap、HashTable等hash型數據key的必要。因為不可變的設計,jvm底層很容易在緩存String對象的時候緩存其hashcode,這樣在執行效率上會大大提升。

 

 

Deeper---直接來看例子:

首先來試試下面程序的運行結果是否與預想的一致:

 1 String s1 = new String("aaa");
 2 String s2 = "aaa";
 3 System.out.println(s1 == s2);    // false
 4 
 5 s1 = new String("bbb").intern();
 6 s2 = "bbb";
 7 System.out.println(s1 == s2);    // true
 8 
 9 s1 = "ccc";
10 s2 = "ccc";
11 System.out.println(s1 == s2);    // true
12 
13 s1 = new String("ddd").intern();
14 s2 = new String("ddd").intern();
15 System.out.println(s1 == s2);    // true
16 
17 s1 = "ab" + "cd";
18 s2 = "abcd";    
19 System.out.println(s1 == s2);    // true
20 
21 String temp = "hh";
22 s1 = "a" + temp;
23 // 如果調用s1.intern 則最終返回true
24 s2 = "ahh";
25 System.out.println(s1 == s2);    // false
26 
27 temp = "hh".intern();
28 s1 = "a" + temp;
29 s2 = "ahh";
30 System.out.println(s1 == s2);    // false
31 
32 temp = "hh".intern();
33 s1 = ("a" + temp).intern();
34 s2 = "ahh";
35 System.out.println(s1 == s2);    // true
36 
37 s1 = new String("1");    // 同時會生成堆中的對象 以及常量池中1的對象,但是此時s1是指向堆中的對象的
38 s1.intern();            // 常量池中的已經存在
39 s2 = "1";
40 System.out.println(s1 == s2);    // false
41 
42 String s3 = new String("1") + new String("1");    // 此時生成了四個對象 常量池中的"1" + 2個堆中的"1" + s3指向的堆中的對象(注此時常量池不會生成"11")
43 s3.intern();    // jdk1.7之后,常量池不僅僅可以存儲對象,還可以存儲對象的引用,會直接將s3的地址存儲在常量池
44 String s4 = "11";    // jdk1.7之后,常量池中的地址其實就是s3的地址
45 System.out.println(s3 == s4); // jdk1.7之前false, jdk1.7之后true
46 
47 s3 = new String("2") + new String("2");
48 s4 = "22";        // 常量池中不存在22,所以會新開辟一個存儲22對象的常量池地址
49 s3.intern();    // 常量池22的地址和s3的地址不同
50 System.out.println(s3 == s4); // false

// 對於什么時候會在常量池存儲字符串對象,我想我們可以基本得出結論: 1. 顯示調用String的intern方法的時候; 2. 直接聲明字符串字面常量的時候,例如: String a = "aaa";
// 3. 字符串直接常量相加的時候,例如: String c = "aa" + "bb"; 其中的aa/bb只要有任何一個不是字符串字面常量形式,都不會在常量池生成"aabb". 且此時jvm做了優化,不// 會同時生成"aa"和"bb"在字符串常量池中

如果有出入的話,再來看看具體的字節碼分析:

 1 /**
 2  * 字節碼為:
 3  *   0:   ldc     #16; //String 11   --- 從常量池加載字符串常量11
 4      2:   astore_1                   --- 將11的引用存到本地變量1,其實就是將s指向常量池中11的位置
 5  */
 6 String s = "11";    
 7 
 8 /**
 9  * 0:   new     #16; //class java/lang/String    --- 新開辟了一個地址,存儲new出來的對象
10    3:   dup                                      --- 將new出來的對象復制了一份到棧頂(也就是s1最終指向的是堆中的另一個存儲字符串11的地址)
11    4:   ldc     #18; //String 11          
12    6:   invokespecial   #20; //Method java/lang/String."<init>":(Ljava/lang/String;)V
13    9:   astore_1
14  */
15 String s1 = new String("11");
16 
17 /**
18  * 0:   new     #16; //class java/lang/StringBuilder                       --- 可以看到jdk對字符串拼接做了優化,先是建了一個StringBuilder對象
19    3:   dup
20    4:   new     #18; //class java/lang/String                              --- 創建String對象
21    7:   dup
22    8:   ldc     #20; //String 1                                            --- 從常量池加載了1(此時常量池和堆中都會存字符串對象)
23    10:  invokespecial   #22; //Method java/lang/String."<init>":(Ljava/lang/String;)V                    --- 初始化String("1")對象
24    13:  invokestatic    #25; //Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
25    16:  invokespecial   #29; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V             --- 初始化StringBuilder對象
26    19:  new     #18; //class java/lang/String
27    22:  dup
28    23:  ldc     #20; //String 1
29    25:  invokespecial   #22; //Method java/lang/String."<init>":(Ljava/lang/String;)V
30    28:  invokevirtual   #30; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31    31:  invokevirtual   #34; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
32    34:  astore_1                                                                                          ---從上可以看到實際上常量池目前只存了1
34   36:  invokevirtual   #38; //Method java/lang/String.intern:()Ljava/lang/String;  --- 調用String.intern中,jdk1.7以后,常量池也是堆中的一部分且常量池可以存引用,這里直接存的是s2的引用
35   39:  pop                                                                                                --- 這里直接返回的是棧頂的元素
36  */
37 String s2 = new String("1") + new String("1");
38 s2.intern();
39 
40 /**
41  * 0:   ldc     #16; //String abc        --- 可以看到此時常量池直接存儲的是:abc, 而不會a、b、c各存一份
42    2:   astore_1
43  */
44 String s3 = "a" + "b" + "c";
45 
46 /**    
47 0:   new     #16; //class java/lang/StringBuilder
48 3:   dup
49 4:   ldc     #18; //String why                --- 常量池的why
50 6:   invokespecial   #20; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
51 9:   ldc     #23; //String true                --- 常量池的true
52 11:  invokevirtual   #25; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
53 14:  invokevirtual   #29; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
54 17:  astore_1
55 */
56 String s1 = new StringBuilder("why").append("true").toString();
57 System.out.println(s1 == s1.intern());                            // jdk1.7之前為false,之后為true

下面我們延伸一下來講講字符串拼接的優化問題: 

 1 String a = "1"; 
2 for (int i=0; i<10; i++) {
3   a += i;
4 }
6 0: ldc #16; //String 1
 7 2: astore_1  8 3: iconst_0  9 4: istore_2                    --- 循環開始 10 5: goto 30 11 8: new #18; //class java/lang/StringBuilder --- 每個循環都建了一個StringBuilder對象,對性能有損耗 12 11: dup 13 12: aload_1 14 13: invokestatic #20; //Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String; 15 16: invokespecial #26; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V 16 19: iload_2 17 20: invokevirtual #29; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 18 23: invokevirtual #33; //Method java/lang/StringBuilder.toString:()Ljava/lang/String; 19 26: astore_1 20 27: iinc 2, 1 ---- 計數加1 21 30: iload_2 22 31: bipush 10 23 33: if_icmplt 8 24 25 String a = "1"; 26 for (int i=0; i<10; i++) { 27 a += "1"; 28 } 29 的字節碼為: 30 0: ldc #16; //String 1 31 2: astore_1 32 3: iconst_0 33 4: istore_2 34 5: goto 31 35 8: new #18; //class java/lang/StringBuilder ---還是會每次建立一個StringBuilder對象 36 11: dup 37 12: aload_1 38 13: invokestatic #20; //Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String; 39 16: invokespecial #26; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V 40 19: ldc #16; //String 1 ---和上一個循環的區別也僅僅在於這里是從常量池加載1, 41 21: invokevirtual #29; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 42 24: invokevirtual #33; //Method java/lang/StringBuilder.toString:()Ljava/lang/String; 43 27: astore_1 44 28: iinc 2, 1 45 31: iload_2 46 32: bipush 10 47 34: if_icmplt 8 
可知,真正的性能瓶頸在於每次循環都建了一個StringBuilder對象
所以我們優化一下 :
50 StringBuilder sb = new StringBuilder("1"); 51 for (int i=0; i<10; i++) { 52 sb.append("1"); 53 }
對應的字節碼為:
55 0: new #16; //class java/lang/StringBuilder -- 在循環直接初始化了StringBuilder對象 56 3: dup 57 4: ldc #18; //String 1 58 6: invokespecial #20; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V 59 9: astore_1 60 10: iconst_0 61 11: istore_2 62 12: goto 25 63 15: aload_1 64 16: ldc #18; //String 1 65 18: invokevirtual #23; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 66 21: pop 67 22: iinc 2, 1 68 25: iload_2 69 26: bipush 10 70 28: if_icmplt 15

 

Where---String.intern的使用:

我們直接看一個例子來結束String.intern之旅吧:

 1 Integer[] DB_DATA = new Integer[10];
 2 Random random = new Random(10 * 10000);
 3 for (int i = 0; i < DB_DATA.length; i++) {
 4     DB_DATA[i] = random.nextInt();
 5 }
 6 long t = System.currentTimeMillis();
 7 for (int i = 0; i < MAX; i++) {
 8     arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length]));                // --- 每次都要new一個對象
 9     // arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();    --- 其實雖然這么多字符串,但是類型最多為10個,大部分重復的字符串,大大減少內存
10 }
11 
12 System.out.println((System.currentTimeMillis() - t) + "ms");
13 System.gc();

 

參考鏈接:

http://www.360doc.com/content/14/0721/16/1073512_396062351.shtml

https://www.cnblogs.com/SaraMoring/p/5713732.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM