參考鏈接: 如何在Java中初始化和比較字符串
原文鏈接:http://tangxman.github.io/2015/07/27/the-difference-of-java-string-pool/?
? ? ? ? ? ? ? ? ? ? ? ??
在java的內存分配中,經常聽到很多關于常量池的描述,我開始看的時候也是看的很模糊,網上五花八門的說法簡直太多了,最后查閱各種資料,終于算是差不多理清了,很多網上說法都有問題,筆者嘗試著來區分一下這幾個概念。?
1.全局字符串池(string pool也有叫做string literal pool)?
全局字符串池里的內容是在類加載完成,經過驗證,準備階段之后在堆中生成字符串對象實例,然后將該字符串對象實例的引用值存到string pool中(記住:string pool中存的是引用值而不是具體的實例對象,具體的實例對象是在堆中開辟的一塊空間存放的。)。 在HotSpot VM里實現的string pool功能的是一個StringTable類,它是一個哈希表,里面存的是駐留字符串(也就是我們常說的用雙引號括起來的)的引用(而不是駐留字符串實例本身),也就是說在堆中的某些字符串實例被這個StringTable引用之后就等同被賦予了”駐留字符串”的身份。這個StringTable在每個HotSpot VM的實例只有一份,被所有的類共享。?
2.class文件常量池(class constant pool)?
我們都知道,class文件中除了包含類的版本、字段、方法、接口等描述信息外,還有一項信息就是常量池(constant pool table),用于存放編譯器生成的各種字面量(Literal)和符號引用(Symbolic References)。 字面量就是我們所說的常量概念,如文本字符串、被聲明為final的常量值等。 符號引用是一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可(它與直接引用區分一下,直接引用一般是指向方法區的本地指針,相對偏移量或是一個能間接定位到目標的句柄)。一般包括下面三類常量:?
類和接口的全限定名字段的名稱和描述符方法的名稱和描述符
常量池的每一項常量都是一個表,一共有如下表所示的11種各不相同的表結構數據,這每個表開始的第一位都是一個字節的標志位(取值1-12),代表當前這個常量屬于哪種常量類型。 每種不同類型的常量類型具有不同的結構,具體的結構本文就先不敘述了,本文著重區分這三個常量池的概念(讀者若想深入了解每種常量類型的數據結構可以查看《深入理解java虛擬機》第六章的內容)。?
3.運行時常量池(runtime constant pool)?
當java文件被編譯成class文件之后,也就是會生成我上面所說的class常量池,那么運行時常量池又是什么時候產生的呢??
jvm在執行某個類的時候,必須經過加載、連接、初始化,而連接又包括驗證、準備、解析三個階段。而當類加載到內存中后,jvm就會將class常量池中的內容存放到運行時常量池中,由此可知,運行時常量池也是每個類都有一個。在上面我也說了,class常量池中存的是字面量和符號引用,也就是說他們存的并不是對象的實例,而是對象的符號引用值。而經過解析(resolve)之后,也就是把符號引用替換為直接引用,解析的過程會去查詢全局字符串池,也就是我們上面所說的StringTable,以保證運行時常量池所引用的字符串與全局字符串池中所引用的是一致的。?
舉個實例來說明一下:?
?
??
?public class HelloWorld {? public static void main(String []args) {? String str1 = "abc";? ?String str2 = new String("def");? ?String str3 = "abc";? ?String str4 = str2.intern();? ?String str5 = "def";? ?System.out.println(str1 == str3);//true? ?System.out.println(str2 == str4);//false? ?System.out.println(str4 == str5);//true? }? }?
回到上面的那個程序,現在就很容易解釋整個程序的內存分配過程了,首先,在堆中會有一個”abc”實例,全局StringTable中存放著”abc”的一個引用值,然后在運行第二句的時候會生成兩個實例,一個是”def”的實例對象,并且StringTable中存儲一個”def”的引用值,還有一個是new出來的一個”def”的實例對象,與上面那個是不同的實例,當在解析str3的時候查找StringTable,里面有”abc”的全局駐留字符串引用,所以str3的引用地址與之前的那個已存在的相同,str4是在運行的時候調用intern()函數,返回StringTable中”def”的引用值,如果沒有就將str2的引用值添加進去,在這里,StringTable中已經有了”def”的引用值了,所以返回上面在new str2的時候添加到StringTable中的 “def”引用值,最后str5在解析的時候就也是指向存在于StringTable中的”def”的引用值,那么這樣一分析之后,下面三個打印的值就容易理解了。上面程序的首先經過編譯之后,在該類的class常量池中存放一些符號引用,然后類加載之后,將class常量池中存放的符號引用轉存到運行時常量池中,然后經過驗證,準備階段之后,在堆中生成駐留字符串的實例對象(也就是上例中str1所指向的”abc”實例對象),然后將這個對象的引用存到全局String Pool中,也就是StringTable中,最后在解析階段,要把運行時常量池中的符號引用替換成直接引用,那么就直接查詢StringTable,保證StringTable里的引用值與運行時常量池中的引用值一致,大概整個過程就是這樣了。?
總結?
1.全局常量池在每個VM中只有一份,存放的是字符串常量的引用值。2.class常量池是在編譯的時候每個class都有的,在編譯階段,存放的是常量的符號引用。3.運行時常量池是在類加載完成之后,將每個class常量池中的符號引用值轉存到運行時常量池中,也就是說,每個class都有一個運行時常量池,類在解析之后,將符號引用替換成直接引用,與全局常量池中的引用值保持一致。
??
================================?
??
class文件常量池和運行時常量池?
最近一直被方法區里面存著什么東西困擾著??
? ? ? ?1.方法區里存class文件信息和class文件常量池是個什么關系。?
? ? ? ? 2.class文件常量池和運行時常量池是什么關系。? ? ? ? ?
? ? ? ? 方法區存著類的信息,常量和靜態變量,即類被編譯后的數據。這個說法其實是沒問題的,只是太籠統了。更加詳細一點的說法是方法區里存放著類的版本,字段,方法,接口和常量池。常量池里存儲著字面量和符號引用。?
? ? ? ?符號引用包括:1.類的全限定名,2.字段名和屬性,3.方法名和屬性。?
? ? ? ?下面一張圖是我畫的方法區,class文件信息,class文件常量池和運行時常量池的關系?
? ? ? ??
? ? ? ?下面一張圖用來表示方法區class文件信息包括哪些內容:?
? ? ? ??
? ? ? ? ?可以看到在方法區里的class文件信息包括:魔數,版本號,常量池,類,父類和接口數組,字段,方法等信息,其實類里面又包括字段和方法的信息。?
? ? ? ? ?下面的圖表是class文件中存儲的數據類型? ? ? ? ? ? ??
??
類型名稱數量u4magic1u2minor_version1u2major_version1u2constant_pool_count1cp_infoconstant_poolconstant_pool_count - 1u2access_flags1u2this_class1u2super_class1u2interfaces_count1u2interfacesinterfaces_countu2fields_count1field_infofieldsfields_countu2methods_count1method_infomethodsmethods_countu2attribute_count1attribute_infoattributesattributes_count
??
? ? ? ?下面用一張圖來表示常量池里存儲的內容:?
? ? ? ? ?
??
用一個class文件實際反編譯一下?
下面是原java代碼?
??
[java] view plain copy?
public class TestInt {? ? ? ? private String str = "hello";? ? ? ? void printInt(){? ? ? ? ? ? System.out.println(65535);? ? ? ? }? ? }? ?
?經過反編譯后獲得class文件是下面這樣的?
??
?
可以看出被反編譯的class文件中的內容和上面所說的是能對應上的。這就解答了class文件和class文件常量池的關系?
class文件常量池和運行時常量池的關系以及區別?
class文件常量池存儲的是當class文件被java虛擬機加載進來后存放在方法區的一些字面量和符號引用,字面量包括字符串,基本類型的常量。?
運行時常量池是當class文件被加載完成后,java虛擬機會將class文件常量池里的內容轉移到運行時常量池里,在class文件常量池的符號引用有一部分是會被轉變為直接引用的,比如說類的靜態方法或私有方法,實例構造方法,父類方法,這是因為這些方法不能被重寫其他版本,所以能在加載的時候就可以將符號引用轉變為直接引用,而其他的一些方法是在這個方法被第一次調用的時候才會將符號引用轉變為直接引用的。?
總結:?
方法區里存儲著class文件的信息和運行時常量池,class文件的信息包括類信息和class文件常量池。?
運行時常量池里的內容除了是class文件常量池里的內容外,還將class文件常量池里的符號引用轉變為直接引用,而且運行時常量池里的內容是能動態添加的。例如調用String的intern方法就能將string的值添加到String常量池中,這里String常量池是包含在運行時常量池里的,但在jdk1.8后,將String常量池放到了堆中。?
下面有一篇文章寫的是比較好的?
http://blog.csdn.net/vegetable_bird_001/article/details/51278339? ?
https://www.cnblogs.com/holos/p/6603379.html?
=====================================?
基本類型的包裝類、String類和常量池?
??
一.相關概念?
?
?什么是常量 用final修飾的成員變量表示常量,值一旦給定就無法改變! final修飾的變量有三種:靜態變量、實例變量和局部變量,分別表示三種類型的常量。? Class文件中的常量池 在Class文件結構中,最頭的4個字節用于存儲魔數Magic Number,用于確定一個文件是否能被JVM接受,再接著4個字節用于存儲版本號,前2個字節存儲次版本號,后2個存儲主版本號,再接著是用于存放常量的常量池,由于常量的數量是不固定的,所以常量池的入口放置一個U2類型的數據(constant_pool_count)存儲常量池容量計數值。 常量池主要用于存放兩大類常量:字面量(Literal)和符號引用量(Symbolic References),字面量相當于Java語言層面常量的概念,如文本字符串,聲明為final的常量值等,符號引用則屬于編譯原理方面的概念,包括了如下三種類型的常量:?
類和接口的全限定名字段名稱和描述符方法名稱和描述符
?方法區中的運行時常量池 運行時常量池是方法區的一部分。 CLass文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用于存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的運行時常量池中存放。 運行時常量池相對于CLass文件常量池的另外一個重要特征是具備動態性,Java語言并不要求常量一定只有編譯期才能產生,也就是并非預置入CLass文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用比較多的就是String類的intern()方法。? 常量池的好處 常量池是為了避免頻繁的創建和銷毀對象而影響系統性能,其實現了對象的共享。 例如字符串常量池,在編譯階段就把所有的字符串文字放到一個常量池中。 (1)節省內存空間:常量池中所有相同的字符串常量被合并,只占用一個空間。 (2)節省運行時間:比較字符串時,==比equals()快。對于兩個引用變量,只用==判斷引用是否相等,也就可以判斷實際值是否相等。? 雙等號==的含義 基本數據類型之間應用雙等號,比較的是他們的數值。 復合數據類型(類)之間應用雙等號,比較的是他們在內存中的存放地址。?
二.8種基本類型的包裝類和常量池?
?
java中基本類型的包裝類的大部分都實現了常量池技術, 即Byte,Short,Integer,Long,Character,Boolean;
?
??
?Integer i1 = 40;? Integer i2 = 40;? System.out.println(i1==i2);//輸出TRUE??
這5種包裝類默認創建了數值[-128,127]的相應類型的緩存數據,但是超出此范圍仍然會去創建新的對象。?
?
??
?//Integer 緩存代碼 :? public static Integer valueOf(int i) {? assert IntegerCache.high >= 127;? if (i >= IntegerCache.low && i <= IntegerCache.high)? return IntegerCache.cache[i + (-IntegerCache.low)];? return new Integer(i);? }??
?
??
?Integer i1 = 400;? Integer i2 = 400;? System.out.println(i1==i2);//輸出false??
兩種浮點數類型的包裝類Float,Double并沒有實現常量池技術。
?
??
?Double i1=1.2;? Double i2=1.2;? System.out.println(i1==i2);//輸出false??
應用常量池的場景 (1)Integer i1=40;Java在編譯的時候會直接將代碼封裝成Integer i1=Integer.valueOf(40);,從而使用常量池中的對象。 (2)Integer i1 = new Integer(40);這種情況下會創建新的對象。
?
??
?Integer i1 = 40;? Integer i2 = new Integer(40);? System.out.println(i1==i2);//輸出false??
Integer比較更豐富的一個例子
?
??
?Integer i1 = 40;? Integer i2 = 40;? Integer i3 = 0;? Integer i4 = new Integer(40);? Integer i5 = new Integer(40);? Integer i6 = new Integer(0);? ?System.out.println("i1=i2 " + (i1 == i2));? System.out.println("i1=i2+i3 " + (i1 == i2 + i3));? System.out.println("i1=i4 " + (i1 == i4));? System.out.println("i4=i5 " + (i4 == i5));? System.out.println("i4=i5+i6 " + (i4 == i5 + i6));? ?System.out.println("40=i5+i6 " + (40 == i5 + i6));? ?
?
??
?i1=i2 true? i1=i2+i3 true? i1=i4 false? i4=i5 false? i4=i5+i6 true? 40=i5+i6 true??
解釋:語句i4 == i5 + i6,因為+這個操作符不適用于Integer對象,首先i5和i6進行自動拆箱操作,進行數值相加,即i4 == 40。然后Integer對象無法與數值進行直接比較,所以i4自動拆箱轉為int值40,最終這條語句轉為40 == 40進行數值比較。Java中的自動裝箱與拆箱?
??
三.String類和常量池?
?
String對象創建方式
?
??
?String str1 = "abcd";? String str2 = new String("abcd");? System.out.println(str1==str2);//false??
這兩種不同的創建方法是有差別的,第一種方式是在常量池中拿對象,第二種方式是直接在堆內存空間創建一個新的對象。只要使用new方法,便需要創建新的對象。?
連接表達式 + (1)只有使用引號包含文本的方式創建的String對象之間使用“+”連接產生的新對象才會被加入字符串池中。 (2)對于所有包含new方式新建對象(包括null)的“+”連接表達式,它所產生的新對象都不會被加入字符串池中。
?
??
?String str1 = "str";? String str2 = "ing";? ?String str3 = "str" + "ing";? String str4 = str1 + str2;? System.out.println(str3 == str4);//false? ?String str5 = "string";? System.out.println(str3 == str5);//true??
java基礎:字符串的拼接?
特例1
?
??
?public static final String A = "ab"; // 常量A? public static final String B = "cd"; // 常量B? public static void main(String[] args) {? String s = A + B; // 將兩個常量用+連接對s進行初始化? ?String t = "abcd";? ?if (s == t) {? ?System.out.println("s等于t,它們是同一個對象");? ?} else {? ?System.out.println("s不等于t,它們不是同一個對象");? ?}? ?}? ?s等于t,它們是同一個對象??
A和B都是常量,值是固定的,因此s的值也是固定的,它在類被編譯時就已經確定了。也就是說:String s=A+B; 等同于:String s="ab"+"cd";?
特例2
?
??
?public static final String A; // 常量A? public static final String B; // 常量B? static {? ?A = "ab";? ?B = "cd";? ?}? ?public static void main(String[] args) {? ?// 將兩個常量用+連接對s進行初始化? ?String s = A + B;? ?String t = "abcd";? ?if (s == t) {? ?System.out.println("s等于t,它們是同一個對象");? ?} else {? ?System.out.println("s不等于t,它們不是同一個對象");? ?}? ?}? ?s不等于t,它們不是同一個對象??
A和B雖然被定義為常量,但是它們都沒有馬上被賦值。在運算出s的值之前,他們何時被賦值,以及被賦予什么樣的值,都是個變數。因此A和B在被賦值之前,性質類似于一個變量。那么s就不能在編譯期被確定,而只能在運行時被創建了。?
?String s1 = new String("xyz"); 創建了幾個對象?? 考慮類加載階段和實際執行時。 (1)類加載對一個類只會進行一次。"xyz"在類加載時就已經創建并駐留了(如果該類被加載之前已經有"xyz"字符串被駐留過則不需要重復創建用于駐留的"xyz"實例)。駐留的字符串是放在全局共享的字符串常量池中的。 (2)在這段代碼后續被運行的時候,"xyz"字面量對應的String實例已經固定了,不會再被重復創建。所以這段代碼將常量池中的對象復制一份放到heap中,并且把heap中的這個對象的引用交給s1 持有。 這條語句創建了2個對象。? java.lang.String.intern() 運行時常量池相對于CLass文件常量池的另外一個重要特征是具備動態性,Java語言并不要求常量一定只有編譯期才能產生,也就是并非預置入CLass文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用比較多的就是String類的intern()方法。 String的intern()方法會查找在常量池中是否存在一份equal相等的字符串,如果有則返回該字符串的引用,如果沒有則添加自己的字符串進入常量池。?
?
??
?public static void main(String[] args) {? ?String s1 = new String("計算機");? String s2 = s1.intern();? String s3 = "計算機";? System.out.println("s1 == s2? " + (s1 == s2));? System.out.println("s3 == s2? " + (s3 == s2));? }??
?
??
?s1 == s2? false? s3 == s2? true??
字符串比較更豐富的一個例子
?
??
?public class Test {? public static void main(String[] args) {? ?String hello = "Hello", lo = "lo";? System.out.println((hello == "Hello") + " ");? System.out.println((Other.hello == hello) + " ");? System.out.println((other.Other.hello == hello) + " ");? System.out.println((hello == ("Hel"+"lo")) + " ");? System.out.println((hello == ("Hel"+lo)) + " ");? System.out.println(hello == ("Hel"+lo).intern());? }? ?}? class Other { static String hello = "Hello"; }? package other;? public class Other { public static String hello = "Hello"; }??
?
??
?true true true true false true```? 在同包同類下,引用自同一String對象.? 在同包不同類下,引用自同一String對象.? 在不同包不同類下,依然引用自同一String對象.? 在編譯成.class時能夠識別為同一字符串的,自動優化成常量,引用自同一String對象.? 在運行時創建的字符串具有獨立的內存地址,所以不引用自同一String對象.? ? -----? [2015-08-26]