面試官Q1:請問爲何String用"+"拼接字符串效率低下,最好能從JVM角度談談嗎?java
對於這個問題,咱們先來看看以下代碼:面試
public class StringTest { public static void main(String[] args) { String a = "abc"; String b = "def"; String c = a + b; String d = "abc" + "def"; System.out.Println(c); System.out.Println(d); } }
打印結果:app
abcdef
abcdef
從上面代碼示例中,咱們看到兩種方式拼接的字符串打印的結果是同樣的。但這只是表面上的,實際內部運行不同。dom
二者究竟有什麼不同?性能
爲了看到二者的不一樣,對代碼作以下調整:ui
public class StringTest { public static void main(String[] args) { String a = "abc"; String b = "def"; String c = a + b; System.out.Println(c); } }
咱們看看編譯完成後它是什麼樣子:spa
C:\Users\GRACE\Documents>javac StringTest.java 2C:\Users\GRACE\Documents>javap -verbose StringTest 3Classfile /C:/Users/GRACE/Documents/StringTest.class 4 Last modified 2018-7-21; size 607 bytes 5 MD5 checksum a2729f11e22d7e1153a209e5ac968b98 6 Compiled from "StringTest.java" 7public class StringTest 8 minor version: 0 9 major version: 52 10 flags: ACC_PUBLIC, ACC_SUPER 11Constant pool: 12 #1 = Methodref #11.#20 // java/lang/Object."<init>":()V 13 #2 = String #21 // abc 14 #3 = String #22 // def 15 #4 = Class #23 // java/lang/StringBuilder 16 #5 = Methodref #4.#20 // java/lang/StringBuilder."<init>":()V 17 #6 = Methodref #4.#24 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 18 #7 = Methodref #4.#25 // java/lang/StringBuilder.toString:()Ljava/lang/String; 19 #8 = Fieldref #26.#27 // java/lang/System.out:Ljava/io/PrintStream; 20 #9 = Methodref #28.#29 // java/io/PrintStream.println:(Ljava/lang/String;)V 21 #10 = Class #30 // StringTest 22 #11 = Class #31 // java/lang/Object 23 #12 = Utf8 <init> 24 #13 = Utf8 ()V 25 #14 = Utf8 Code 26 #15 = Utf8 LineNumberTable 27 #16 = Utf8 main 28 #17 = Utf8 ([Ljava/lang/String;)V 29 #18 = Utf8 SourceFile 30 #19 = Utf8 StringTest.java 31 #20 = NameAndType #12:#13 // "<init>":()V 32 #21 = Utf8 abc 33 #22 = Utf8 def 34 #23 = Utf8 java/lang/StringBuilder 35 #24 = NameAndType #32:#33 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 36 #25 = NameAndType #34:#35 // toString:()Ljava/lang/String; 37 #26 = Class #36 // java/lang/System 38 #27 = NameAndType #37:#38 // out:Ljava/io/PrintStream; 39 #28 = Class #39 // java/io/PrintStream 40 #29 = NameAndType #40:#41 // println:(Ljava/lang/String;)V 41 #30 = Utf8 StringTest 42 #31 = Utf8 java/lang/Object 43 #32 = Utf8 append 44 #33 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; 45 #34 = Utf8 toString 46 #35 = Utf8 ()Ljava/lang/String; 47 #36 = Utf8 java/lang/System 48 #37 = Utf8 out 49 #38 = Utf8 Ljava/io/PrintStream; 50 #39 = Utf8 java/io/PrintStream 51 #40 = Utf8 println 52 #41 = Utf8 (Ljava/lang/String;)V 53{ 54 public StringTest(); 55 descriptor: ()V 56 flags: ACC_PUBLIC 57 Code: 58 stack=1, locals=1, args_size=1 59 0: aload_0 60 1: invokespecial #1 // Method java/lang/Object."<init>":()V 61 4: return 62 LineNumberTable: 63 line 1: 0 64 65 public static void main(java.lang.String[]); 66 descriptor: ([Ljava/lang/String;)V 67 flags: ACC_PUBLIC, ACC_STATIC 68 Code: 69 stack=2, locals=4, args_size=1 70 0: ldc #2 // String abc 71 2: astore_1 72 3: ldc #3 // String def 73 5: astore_2 74 6: new #4 // class java/lang/StringBuilder 75 9: dup 76 10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V 77 13: aload_1 78 14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 79 17: aload_2 80 18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 81 21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 82 24: astore_3 83 25: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream; 84 28: aload_3 85 29: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 86 32: return 87 LineNumberTable: 88 line 3: 0 89 line 4: 3 90 line 5: 6 91 line 6: 25 92 line 7: 32 93} 94SourceFile: "StringTest.java"
首先看到使用了一個指針指向一個常量池中的對象內容爲「abc」,而另外一個指針指向「def」,此時經過new申請了一個StringBuilder,而後調用這個StringBuilder的初始化方法;而後分別作了兩次append操做,而後最後作一個toString()操做;可見String的+在編譯後會被編譯爲StringBuilder來運行,咱們知道這裏作了一個new StringBuilder的操做,而且作了一個toString的操做,若是你對JVM有所瞭解,凡是new出來的對象絕對不會放在常量池中,toString會發生一次內容拷貝,可是也不會在常量池中,因此在這裏常量池String+常量池String放在了堆中。指針
咱們再來看看另一種狀況,用一樣的方式來看看結果是什麼:code
代碼以下:對象
public class StringTest { public static void main(String[] args) { String c = "abc" + "def"; System.out.println(c); } }
咱們也來看看它編譯完成後是什麼樣子:
C:\Users\GRACE\Documents>javac StringTest.java 2 3C:\Users\GRACE\Documents>javap -verbose StringTest 4Classfile /C:/Users/GRACE/Documents/StringTest.class 5 Last modified 2018-7-21; size 426 bytes 6 MD5 checksum c659d48ff8aeb45a3338dea5d129f593 7 Compiled from "StringTest.java" 8public class StringTest 9 minor version: 0 10 major version: 52 11 flags: ACC_PUBLIC, ACC_SUPER 12Constant pool: 13 #1 = Methodref #6.#15 // java/lang/Object."<init>":()V 14 #2 = String #16 // abcdef 15 #3 = Fieldref #17.#18 // java/lang/System.out:Ljava/io/PrintStream; 16 #4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V 17 #5 = Class #21 // StringTest 18 #6 = Class #22 // java/lang/Object 19 #7 = Utf8 <init> 20 #8 = Utf8 ()V 21 #9 = Utf8 Code 22 #10 = Utf8 LineNumberTable 23 #11 = Utf8 main 24 #12 = Utf8 ([Ljava/lang/String;)V 25 #13 = Utf8 SourceFile 26 #14 = Utf8 StringTest.java 27 #15 = NameAndType #7:#8 // "<init>":()V 28 #16 = Utf8 abcdef 29 #17 = Class #23 // java/lang/System 30 #18 = NameAndType #24:#25 // out:Ljava/io/PrintStream; 31 #19 = Class #26 // java/io/PrintStream 32 #20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V 33 #21 = Utf8 StringTest 34 #22 = Utf8 java/lang/Object 35 #23 = Utf8 java/lang/System 36 #24 = Utf8 out 37 #25 = Utf8 Ljava/io/PrintStream; 38 #26 = Utf8 java/io/PrintStream 39 #27 = Utf8 println 40 #28 = Utf8 (Ljava/lang/String;)V 41{ 42 public StringTest(); 43 descriptor: ()V 44 flags: ACC_PUBLIC 45 Code: 46 stack=1, locals=1, args_size=1 47 0: aload_0 48 1: invokespecial #1 // Method java/lang/Object."<init>":()V 49 4: return 50 LineNumberTable: 51 line 1: 0 52 53 public static void main(java.lang.String[]); 54 descriptor: ([Ljava/lang/String;)V 55 flags: ACC_PUBLIC, ACC_STATIC 56 Code: 57 stack=2, locals=2, args_size=1 58 0: ldc #2 // String abcdef 59 2: astore_1 60 3: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 61 6: aload_1 62 7: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 63 10: return 64 LineNumberTable: 65 line 3: 0 66 line 4: 3 67 line 5: 10 68} 69SourceFile: "StringTest.java"
這一次編譯完後的代碼比前面少了不少,並且,仔細看,你會發現14行處,編譯的過程當中直接變成了"abcdef",這是爲何呢?由於當發生「abc」 + 「def」在同一行發生時,JVM在編譯時就認爲這個加號是沒有用處的,編譯的時候就直接變成
String d = "abcdef";
同理若是出現:String a =「a」 + 1,編譯時候就會變成:String a = 「a1″;
再補充一個例子:
final String a = "a"; final String b = "ab"; String c = a + b;
在編譯時候,c部分會被編譯爲:String c = 「aab」;可是若是a或b有任意一個不是final的,都會new一個新的對象出來;其次再補充下,若是a和b,是某個方法返回回來的,不論方法中是final類型的仍是常量什麼的,都不會被在編譯時將數據編譯到常量池,由於編譯器並不會跟蹤到方法體裏面去看你作了什麼,其次只要是變量就是可變的,即便你認爲你看到的代碼是不可變的,可是運行時是能夠被切入的。
那麼效率問題從何提及?
那說了這麼多,也沒看到有說效率方面的問題呀?
其實上面兩個例子,鏈接字符串行表達式很簡單,那麼"+"和StringBuilder基本是同樣的,但若是結構比較複雜,如使用循環來鏈接字符串,那麼產生的Java Byte Code就會有很大的區別。咱們再來看看下面一段代碼:
import java.util.*; public class StringTest { public static void main(String[] args){ String s = ""; Random rand = new Random(); for (int i = 0; i < 10; i++){ s = s + rand.nextInt(1000) + " "; } System.out.println(s); } }
上面代碼反編譯後的結果以下:
C:\Java\jdk1.8.0_171\bin>javap -c E:\StringTest.class Picked up _JAVA_OPTIONS: -Xmx512M Compiled from "StringTest.java" public class StringTest { public StringTest(); Code: 0: aload_0 1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: //String s = ""; 0: ldc #16 // String 2: astore_1 //Random rand = new Random(); 3: new #18 // class java/util/Random 6: dup 7: invokespecial #20 // Method java/util/Random."<init>":()V 10: astore_2 //StringBuilder result = new StringBuilder(); 11: iconst_0 12: istore_3 13: goto 49 //s = (new StringBuilder(String.valueOf(s))).append(rand.nextInt(1000)).append(" ").toString(); 16: new #21 // class java/lang/StringBuilder 19: dup 20: aload_1 21: invokestatic #23 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String; 24: invokespecial #29 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V 27: aload_2 28: sipush 1000 31: invokevirtual #32 // Method java/util/Random.nextInt:(I)I 34: invokevirtual #36 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 37: ldc #40 // String 39: invokevirtual #42 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 42: invokevirtual #45 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 45: astore_1 46: iinc 3, 1 49: iload_3 50: bipush 10 52: if_icmplt 16 //System.out.println(s); 55: getstatic #49 // Field java/lang/System.out:Ljava/io/PrintStream; 58: aload_1 59: invokevirtual #55 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 62: return }
咱們能夠看到,雖然編譯器將"+"轉換成了StringBuilder,但建立StringBuilder對象的位置卻在for語句內部。這就意味着每執行一次循環,就會建立一個StringBuilder對象(對於本例來講,是建立了10個StringBuilder對象),雖然Java有垃圾回收器,但這個回收器的工做時間是不定的。若是不斷產生這樣的垃圾,那麼仍然會佔用大量的資源。解決這個問題的方法就是在程序中直接使用StringBuilder來鏈接字符串,代碼以下:
import java.util.Random; public class StringTest { public static void main(String[] args) { Random rand = new Random(); StringBuilder result = new StringBuilder(); for (int i = 0; i < 10; i++) { result.append(rand.nextInt(1000)); result.append(" "); } System.out.println(result.toString()); } }
上面代碼反編譯後的結果以下:
C:\Java\jdk1.8.0_171\bin>javap -c E:\Dubbo\Demo\bin\StringTest.class Picked up _JAVA_OPTIONS: -Xmx512M Compiled from "StringTest.java" public class StringTest { public StringTest(); Code: 0: aload_0 1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: //Random rand = new Random(); 0: new #16 // class java/util/Random 3: dup 4: invokespecial #18 // Method java/util/Random."<init>":()V 7: astore_1 //StringBuilder result = new StringBuilder(); 8: new #19 // class java/lang/StringBuilder 11: dup 12: invokespecial #21 // Method java/lang/StringBuilder."<init>":()V 15: astore_2 //for(int i = 0; i < 10; i++) 16: iconst_0 17: istore_3 18: goto 43 //result.append(rand.nextInt(1000)); 21: aload_2 22: aload_1 23: sipush 1000 26: invokevirtual #22 // Method java/util/Random.nextInt:(I)I 29: invokevirtual #26 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 32: pop //result.append(" "); 33: aload_2 34: ldc #30 // String 36: invokevirtual #32 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 39: pop 40: iinc 3, 1 43: iload_3 44: bipush 10 46: if_icmplt 21 //System.out.println(result.toString()); 49: getstatic #35 // Field java/lang/System.out:Ljava/io/PrintStream; 52: aload_2 53: invokevirtual #41 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 56: invokevirtual #45 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 59: return }
從上面的反編譯結果能夠看出,建立StringBuilder的代碼被放在了for語句外。雖然這樣處理在源程序中看起來複雜,但卻換來了更高的效率,同時消耗的資源也更少了。
因此,從上述幾個例子中咱們得出的結論是:String採用鏈接運算符(+)效率低下,都是上述循環、大批量數據狀況形成的,每作一次"+"就產生個StringBuilder對象,而後append後就扔掉。下次循環再到達時從新產生個StringBuilder對象,而後append字符串,如此循環直至結束。若是咱們直接採用StringBuilder對象進行append的話,咱們能夠節省建立和銷燬對象的時間。若是隻是簡單的字面量拼接或者不多的字符串拼接,性能都是差很少的。
C:\Users\GRACE\Documents>javac StringTest.java
2C:\Users\GRACE\Documents>javap -verbose StringTest
3Classfile /C:/Users/GRACE/Documents/StringTest.class
4 Last modified 2018-7-21; size 607 bytes
5 MD5 checksum a2729f11e22d7e1153a209e5ac968b98
6 Compiled from "StringTest.java"
7public class StringTest
8 minor version: 0
9 major version: 52
10 flags: ACC_PUBLIC, ACC_SUPER
11Constant pool:
12 #1 = Methodref #11.#20 // java/lang/Object."<init>":()V
13 #2 = String #21 // abc
14 #3 = String #22 // def
15 #4 = Class #23 // java/lang/StringBuilder
16 #5 = Methodref #4.#20 // java/lang/StringBuilder."<init>":()V
17 #6 = Methodref #4.#24 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18 #7 = Methodref #4.#25 // java/lang/StringBuilder.toString:()Ljava/lang/String;
19 #8 = Fieldref #26.#27 // java/lang/System.out:Ljava/io/PrintStream;
20 #9 = Methodref #28.#29 // java/io/PrintStream.println:(Ljava/lang/String;)V
21 #10 = Class #30 // StringTest
22 #11 = Class #31 // java/lang/Object
23 #12 = Utf8 <init>
24 #13 = Utf8 ()V
25 #14 = Utf8 Code
26 #15 = Utf8 LineNumberTable
27 #16 = Utf8 main
28 #17 = Utf8 ([Ljava/lang/String;)V
29 #18 = Utf8 SourceFile
30 #19 = Utf8 StringTest.java
31 #20 = NameAndType #12:#13 // "<init>":()V
32 #21 = Utf8 abc
33 #22 = Utf8 def
34 #23 = Utf8 java/lang/StringBuilder
35 #24 = NameAndType #32:#33 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
36 #25 = NameAndType #34:#35 // toString:()Ljava/lang/String;
37 #26 = Class #36 // java/lang/System
38 #27 = NameAndType #37:#38 // out:Ljava/io/PrintStream;
39 #28 = Class #39 // java/io/PrintStream
40 #29 = NameAndType #40:#41 // println:(Ljava/lang/String;)V
41 #30 = Utf8 StringTest
42 #31 = Utf8 java/lang/Object
43 #32 = Utf8 append
44 #33 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
45 #34 = Utf8 toString
46 #35 = Utf8 ()Ljava/lang/String;
47 #36 = Utf8 java/lang/System
48 #37 = Utf8 out
49 #38 = Utf8 Ljava/io/PrintStream;
50 #39 = Utf8 java/io/PrintStream
51 #40 = Utf8 println
52 #41 = Utf8 (Ljava/lang/String;)V
53{
54 public StringTest();
55 descriptor: ()V
56 flags: ACC_PUBLIC
57 Code:
58 stack=1, locals=1, args_size=1
59 0: aload_0
60 1: invokespecial #1 // Method java/lang/Object."<init>":()V
61 4: return
62 LineNumberTable:
63 line 1: 0
64
65 public static void main(java.lang.String[]);
66 descriptor: ([Ljava/lang/String;)V
67 flags: ACC_PUBLIC, ACC_STATIC
68 Code:
69 stack=2, locals=4, args_size=1
70 0: ldc #2 // String abc
71 2: astore_1
72 3: ldc #3 // String def
73 5: astore_2
74 6: new #4 // class java/lang/StringBuilder
75 9: dup
76 10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
77 13: aload_1
78 14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
79 17: aload_2
80 18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
81 21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
82 24: astore_3
83 25: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
84 28: aload_3
85 29: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
86 32: return
87 LineNumberTable:
88 line 3: 0
89 line 4: 3
90 line 5: 6
91 line 6: 25
92 line 7: 32
93}
94SourceFile: "StringTest.java"