本文修改於:https://www.zhihu.com/question/29884421/answer/113785601
前言:在Java中,String有兩種賦值方式,第一種是通過“字面量”賦值,如:String str="hello",第二種是通過new關鍵字創建新對象,如String str=new String("hello")。那么這兩種賦值的方式到底有什么區別呢,下面給出具體分析。
1.首先回顧Java虛擬機的結構圖
在上面的虛擬機結構圖中,中間的五彩區域叫“運行時數據區(Run-time Data Areas)”。也就是虛擬機管理的內存,就是大白話的“內存”。其中后面兩個,程序計數器(PC Registers)和本地方法棧(Native Method Stack)與所講沒關系,先忽略。一般講起來虛擬機內存最主要的就是以下三塊:
1)堆(Heap):最大一塊空間。存放對象實例和數組。全局共享。
2)棧(Stack):全稱 “虛擬機棧(JVM Stacks)”。存放基本型,以及對象引用。線程私有。
3)方法區(Method Area):“類”被加載后的信息,常量,靜態變量存放於此。全局共享。在HotSpot里也叫“永生代”。但兩者不能等同。
2.棧、堆和非堆
上圖中,首先Heap堆分成“新生代”,“老年代”,先不用管它,這是GC垃圾回收時候的事。重要的是Stack棧區里的“局部變量表(Local Variables)”和“操作數棧(Operand Stack)”。因為棧是線程私有的,每個方法被執行的時候都會創建一個“棧幀(Stack Frame)”,而每個棧幀里對應的都維護着一個局部變量表和操作數棧。基本數據類型和對象引用就存在棧里,其實就是存在局部變量表里,而操作數棧是線程實際的操作台。
如下圖,做個加法100+98,局部變量表就是存數據的地方,一直不變,到加法做完再把和加進去。操作數棧就很忙了,先把兩個數字壓進去,再求和,算出來以后再彈出去。
中間這個非堆(Non-Heap)可以粗略地理解為非堆里包含了永生代,而永生代里又包括了方法區。上面說了,每個類加載完之后,類的信息都存在方法區里。和String最相關的是里面的“運行時常量池(Run-time Constant Pool)”,它是每個類私有的,后面會講到。每個class文件里的“常量池”在類被加載器加載之后,就映射存放在這個地方。另外一個是“字符串常量池(String Pool)”,和運行時常量池不是一個概念。字符串常量池是全局共享的。位置就在第二張圖里Interned String的位置,可以理解為在永生代里,方法區外面。后面會講到,String.intern()方法,字符串駐留之后,引用就放在這個String Pool。
3.具體分析
如下面的Test.java文件,在主線程方法main里聲明了一個字面量是"Hello"的字符串str。
1 package com.test.java.string; 2 class Test{ 3 public void f(String s){...}; 4 public static void main(String[] args){ 5 String str = "Hello"; 6 ... 7 } 8 }
編譯成Test.class文件之后,如下圖,除了版本、字段、方法、接口等描述信息外,還有一個也叫“常量池(Constant Pool Table)”的東西(淡綠色區塊)。但這個常量池和內存里的常量池不一樣。class文件里的常量池主要存兩個東西:“字面量(Literal)”和“符號引用量(Symbolic References)”。其中字面量就包括類中定義的一些常量,因為String是不可變的,由final關鍵字修飾,所以代碼里的“Hello”字符串,就是作為字面量(常量)寫在class的常量池里。
運行程序用到Test類的時候,Test.class文件的信息就會被解析到內存的方法區里。class文件里常量池里大部分數據會被加載到“運行時常量池”,但String不是。例子中的"Hello"的一個引用會被存到同樣在Non Heap區的字符串常量池(String Pool)里,而“Hello”本體還是和所有對象一樣,創建在Heap堆區。http://rednaxelafx.iteye.com/blog/774673文章里,測試的結果是在新生代的Eden區。但因為一直有一個引用駐留在字符串常量池,所以不會被GC清理掉。這個Hello對象會生存到整個線程結束。如下圖所示,字符串常量池的具體位置是在過去說的永生代里,方法區的外面。
注意:這只是在Test類被類加載器加載時候的情形。主線程中的str變量這時候都還沒有被創建,但Hello的實例已經在Heap里了,對它的引用也已經在字符串常量池里了。
等主線程開始創建str變量的時候,虛擬機就會到字符串常量池里找,看有沒有能equals("Hello")的String。如果找到了,就在棧區當前棧幀的局部變量表里創建str變量,然后把字符串常量池里對Hello對象的引用復制給str變量;找不到的話,才會在heap堆重新創建一個對象,然后把引用駐留到字符串常量區。然后再把引用復制棧幀的局部變量表。
如果我們當時定義了很多個值為"Hello"的String,比如像下面代碼,有三個變量str1,str2,str3,也不會在堆上增加String實例。局部變量表里三個變量統一指向同一個堆內存地址。
1 package com.test.java.string; 2 class Test{ 3 public void f(String s){...}; 4 public static void main(String[] args){ 5 String str1 = "Hello"; 6 String str2 = "Hello"; 7 String str3 = "Hello"; 8 ... 9 } 10 }
上圖中str1,str2,str3之間可以用==來連接。
但如果是用new關鍵字來創建字符串,情況就不一樣了。
1 package com.test.java.string; 2 class Test{ 3 public void f(String s){...}; 4 public static void main(String[] args){ 5 String str1 = "Hello"; 6 String str2 = "Hello"; 7 String str3 = new String("Hello"); 8 ... 9 } 10 }
這時候,str1和str2還是和之前一樣。但str3因為new關鍵字會在Heap堆申請一塊全新的內存來創建新的對象。雖然字面還是"Hello",但是完全不同的對象,有不同的內存地址。
當然String#intern()方法讓我們能手動檢查字符串常量池,把有新字面值的字符串地址駐留到常量池里。
最后補充一下,JDK 7開始Hotspot把Interned String從PermGen移到Heap堆,JDK 8又徹底取消了 PermGen。但不管怎樣,基本原理還是不變的。
總結:通過以上的分析,可以非常清楚的發現String兩種賦值方式的區別,每次閱讀都收益頗多。
by Shawn Chen,2018.3.20日,下午。