Java中String對象兩種賦值方式的區別


本文修改於: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日,下午。 


免責聲明!

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



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