這是個人第 50 篇原創文章。
我想全部 Java 程序員都曾被這個 new String 的問題困擾過,這是一道高頻的 Java 面試題,但惋惜的是網上衆說紛紜,居然找不到標準的答案。有人說建立了 1 個對象,也有人說建立了 2 個對象,還有人說可能建立了 1 個或 2 個對象,但誰都沒有拿出幹掉對方的證據,這就讓咱們這幫吃瓜羣衆們陷入了兩難之中,不知道到底該信誰得。
可是今天,老王就斗膽和你們聊聊這個話題,順便再拿出點證據。
以目前的狀況來看,關於 new String("xxx") 建立對象個數的答案有 3 種:java
字符串的分配和其餘的對象分配同樣,須要耗費高昂的時間和空間爲代價,若是須要大量頻繁的建立字符串,會極大程度地影響程序的性能,所以 JVM 爲了提升性能和減小內存開銷引入了字符串常量池(Constant Pool Table)的概念。
字符串常量池至關於給字符串開闢一個常量池空間相似於緩存區,對於直接賦值的字符串(String s="xxx")來講,在每次建立字符串時優先使用已經存在字符串常量池的字符串,若是字符串常量池沒有相關的字符串,會先在字符串常量池中建立該字符串,而後將引用地址返回變量,以下圖所示:
以上說法能夠經過以下代碼進行證實:git
public class StringExample { public static void main(String[] args) { String s1 = "Java"; String s2 = "Java"; System.out.println(s1 == s2); } }
以上程序的執行結果爲:true,說明變量 s1 和變量 s2 指向的是同一個地址。
在這裏咱們順便說一下字符串常量池的再不一樣 JDK 版本的變化。
常量池的內存佈局程序員
從JDK 1.7 以後把永生代換成的元空間,把字符串常量池從方法區移到了 Java 堆上。
JDK 1.7 內存佈局以下圖所示:
github
JDK 1.8 內存佈局以下圖所示:
面試
JDK 1.8 與 JDK 1.7 最大的區別是 JDK 1.8 將永久代取消,並設立了元空間。官方給的說明是因爲永久代內存常常不夠用或發生內存泄露,會爆出 java.lang.OutOfMemoryError: PermGen 的異常,因此把將永久區廢棄而改用元空間了,改成了使用本地內存空間,官網解釋詳情:http://openjdk.java.net/jeps/122
答案解密緩存
認爲 new 方式建立了 1 個對象的人認爲,new String 只是在堆上建立了一個對象,只有在使用 intern() 時纔去常量池中查找並建立字符串。
認爲 new 方式建立了 2 個對象的人認爲,new String 會在堆上建立一個對象,而且在字符串常量池中也建立一個字符串。
認爲 new 方式有可能建立 1 個或 2 個對象的人認爲,new String 會先去常量池中判斷有沒有此字符串,若是有則只在堆上建立一個字符串而且指向常量池中的字符串,若是常量池中沒有此字符串,則會建立 2 個對象,先在常量池中新建此字符串,而後把此引用返回給堆上的對象,以下圖所示:
性能優化
老王認爲正確的答案:建立 1 個或者 2 個對象。
技術論證
解鈴還須繫鈴人,回到問題的那個爭議點上,new String 到底會不會在常量池中建立字符呢?咱們經過反編譯下面這段代碼就能夠得出正確的結論,代碼以下:ide
public class StringExample { public static void main(String[] args) { String s1 = new String("javaer-wang"); String s2 = "wang-javaer"; String s3 = "wang-javaer"; } }
首先咱們使用 javac StringExample.java 編譯代碼,而後咱們再使用 javap -v StringExample 查看編譯的結果,相關信息以下:佈局
Classfile /Users/admin/github/blog-example/blog-example/src/main/java/com/example/StringExample.class Last modified 2020年4月16日; size 401 bytes SHA-256 checksum 89833a7365ef2930ac1bc3d7b88dcc5162da4b98996eaac397940d8997c94d8e Compiled from "StringExample.java" public class com.example.StringExample minor version: 0 major version: 58 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #16 // com/example/StringExample super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/lang/String #8 = Utf8 java/lang/String #9 = String #10 // javaer-wang #10 = Utf8 javaer-wang #11 = Methodref #7.#12 // java/lang/String."<init>":(Ljava/lang/String;)V #12 = NameAndType #5:#13 // "<init>":(Ljava/lang/String;)V #13 = Utf8 (Ljava/lang/String;)V #14 = String #15 // wang-javaer #15 = Utf8 wang-javaer #16 = Class #17 // com/example/StringExample #17 = Utf8 com/example/StringExample #18 = Utf8 Code #19 = Utf8 LineNumberTable #20 = Utf8 main #21 = Utf8 ([Ljava/lang/String;)V #22 = Utf8 SourceFile #23 = Utf8 StringExample.java { public com.example.StringExample(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=4, args_size=1 0: new #7 // class java/lang/String 3: dup 4: ldc #9 // String javaer-wang 6: invokespecial #11 // Method java/lang/String."<init>":(Ljava/lang/String;)V 9: astore_1 10: ldc #14 // String wang-javaer 12: astore_2 13: ldc #14 // String wang-javaer 15: astore_3 16: return LineNumberTable: line 5: 0 line 6: 10 line 7: 13 line 8: 16 } SourceFile: "StringExample.java"
備註:以上代碼的運行也編譯環境爲 jdk1.8.0_101。
其中 Constant pool 表示字符串常量池,咱們在字符串編譯期的字符串常量池中找到了咱們 String s1 = new String("javaer-wang"); 定義的「javaer-wang」字符,在信息 #10 = Utf8 javaer-wang 能夠看出,也就是在編譯期 new 方式建立的字符串就會被放入到編譯期的字符串常量池中,也就是說 new String 的方式會首先去判斷字符串常量池,若是沒有就會新建字符串那麼就會建立 2 個對象,若是已經存在就只會在堆中建立一個對象指向字符串常量池中的字符串。
那麼問題來了,如下這段代碼的執行結果爲 true 仍是 false?性能
String s1 = new String("javaer-wang"); String s2 = new String("javaer-wang"); System.out.println(s1 == s2);
既然 new String 會在常量池中建立字符串,那麼執行的結果就應該是 true 了。其實並非,這裏對比的變量 s1 和 s2 堆上地址,由於堆上的地址是不一樣的,因此結果必定是 false,以下圖所示:
從圖中能夠看出 s1 和 s2 的引用必定是相同的,而 s3 和 s4 的引用是不一樣的,對應的程序代碼以下:
public static void main(String[] args) { String s1 = "Java"; String s2 = "Java"; String s3 = new String("Java"); String s4 = new String("Java"); System.out.println(s1 == s2); System.out.println(s3 == s4); }
程序執行的結果也符合預期:
true
false
擴展知識
咱們知道 String 是 final 修飾的,也就是說必定被賦值就不能被修改了。但編譯器除了有字符串常量池的優化以外,還會對編譯期能夠確認的字符串進行優化,例如如下代碼:
public static void main(String[] args) { String s1 = "abc"; String s2 = "ab" + "c"; String s3 = "a" + "b" + "c"; System.out.println(s1 == s2); System.out.println(s1 == s3); }
按照 String 不能被修改的思想來看,s2 應該會在字符串常量池建立兩個字符串「ab」和「c」,s3 會建立三個字符串,他們的引用對比結果也必定是 false,但其實不是,他們的結果都是 true,這是編譯器優化的功勞。
一樣咱們使用 javac StringExample.java 先編譯代碼,再使用 javap -c StringExample 命令查看編譯的代碼以下:
警告: 文件 ./StringExample.class 不包含類 StringExample Compiled from "StringExample.java"
public class com.example.StringExample {
public com.example.StringExample();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #7 // String abc
2: astore_1
3: ldc #7 // String abc
5: astore_2
6: ldc #7 // String abc
8: astore_3
9: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
12: aload_1
13: aload_2
14: if_acmpne 21
17: iconst_1
18: goto 22
21: iconst_0
22: invokevirtual #15 // Method java/io/PrintStream.println:(Z)V
25: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
28: aload_1
29: aload_3
30: if_acmpne 37
33: iconst_1
34: goto 38
37: iconst_0
38: invokevirtual #15 // Method java/io/PrintStream.println:(Z)V
41: return
}
從 Code 三、6 能夠看出字符串都被編譯器優化成了字符串「abc」了。
總結
本文咱們經過 javap -v XXX 的方式查看編譯的代碼發現 new String 首次會在字符串常量池中建立此字符串,那也就是說,經過 new 建立字符串的方式可能會建立 1 個或 2 個對象,若是常量池中已經存在此字符串只會在堆上建立一個變量,並指向字符串常量池中的值,若是字符串常量池中沒有相關的字符,會先建立字符串在返回此字符串的引用給堆空間的變量。咱們還將了字符串常量池在 JDK 1.7 和 JDK 1.8 的變化以及編譯器對肯定字符串的優化,但願能幫你正在的理解字符串的比較。
最後的話
原創不易,本篇近 3000 的文字描述,以及大量精美的圖片,耗費了做者大概 5 個多小時的時間,寫做是一件很酷,而且能幫助他人的事,做者但願一直能堅持下去。若是以爲有用,請隨手點擊一個贊吧,謝謝。
END
史上最全的延遲任務實現方式彙總!附代碼(強烈推薦)
99%的程序員都在用Lombok,原理居然這麼簡單?我也手擼了一個!|建議收藏!!!
《大廠內部資料》Redis 性能優化的 13 條軍規!全網首發
關注公衆號發送」進羣「,老王拉你進讀者羣。