JAVA中常遇到的幾種常量池的區別
1. Class文件常量池
Class文件中除了有類的版本信息,字段,方法,接口等描述信息外,還有一部分叫Class文件常量池,這個常量池可以理解為Class文件中的資源倉庫,它當中主要存放兩大類常量:字面量和符號引用
字面量:如文本字符串,"aaa"; 聲明為final類型的常量值等等
符號引用:又分為三類常量
1)類和接口的全限定名
2)字段的名稱和描述符
3)方法的名稱和描述符
2. 運行時常量池
運行時常量池是方法區的一部分,Class文件常量池中的內容在編譯時就產生了,而在類加載后,這部分內容會存在運行時常量池中,另外,由符號引用轉變成的直接引用也會存在運行時常量池中。
運行時常量池相對于Class文件常量池的一個重要特征是具備動態性,也就是常量并不一定在編譯時產生,運行時也可能將新的常量放入常量池中。
3. 字符串池
這是一個比較難懂的概念,在工作中,String類是我們使用頻率非常高的一種對象類型。JVM為了提升性能和減少內存開銷,避免字符串的重復創建,其維護了一塊特殊的內存空間,即字符串池(String Pool)。這部分內存之前是在方法區,jdk1.8之后已經移除了方法區,轉而替代為Metaspace區,那么這個字符串池應該是被劃到這個Metaspace中了吧(有疑問,還沒弄明白)。
我們知道,在Java中有兩種創建字符串對象的方式:
1)采用字面值的方式賦值
2)采用new關鍵字新建一個字符串對象。
這兩種方式在性能和內存占用方面存在著差別
方式一:采用字面值的方式賦值,例如:
String a = "aaa";
String b = "aaa";
System.out.println(a == b)
我們來分析一下過程,JVM首先會去字符串池中查找是否存在"aaa"這個對象,如果不存在,則在字符串池中創建"aaa"這個對象,然后將池中"aaa"這個對象的引用地址返回給字符串常量a,這樣a會指向池中"aaa"這個字符串對象;如果存在,則不創建任何對象,直接將池中"aaa"這個對象的地址返回,賦給字符串常量b。所以a==b返回值是true,因為二均指向了字符串池中的"aaa".
方式二:采用new關鍵字新建一個字符串對象,例如:
String a = new String("aaa");
String b = new String("aaa");
System.out.println(a == b);
采用new關鍵字,JVM會先從常量池中查看有無"aaa"字符串,有的話就拷貝一份到新new出來的堆內存中,返回的是堆內存的地址;如果沒有的話,直接在堆中new出來一塊空間存放"aaa"的值,同樣返回的是堆內存的地址,那么問題來了
這個時候這個堆內存的"aaa"是否會也在字符串池中創建一份呢?
這個問題在網上爭議很大,有的認為這個時候也會在字符串池中創建一份,這個說法我不太認同,因為這樣的話豈不是造成了堆和字符串池的完全重復?也就是不管字符串池中有沒有"aaa",只要我是new,那都會在堆和字符串池中同時存在"aaa".這樣不就造成了內存的浪費嗎?還有一種說法是,如果字符串池中沒有"aaa",那先在堆中創造出"aaa",如果需要往字符串池中加入"aaa"的話,就調用String的intern方法。我個人比較認同這種說法。
關于intern方法
intern方法使用:一個初始為空的字符串池,它由類String獨自維護。當調用 intern方法時,如果池已經包含一個等于此String對象的字符串(用equals(oject)方法確定),則返回池中的字符串。否則,將此String對象添加到池中,并返回此String對象的引用。 對于任意兩個字符串s和t,當且僅當s.equals(t)為true時,s.instan() == t.instan才為true。所有字面值字符串和字符串賦值常量表達式都使用 intern方法進行操作。
下面看一些經常出現的例子
String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // true
System.out.println(s1 == s4); // false
System.out.println(s1 == s9); // false
System.out.println(s4 == s5); // false
System.out.println(s1 == s6); // true
首先說明一點,在java 中,直接使用==操作符,比較的是兩個字符串的引用地址,并不是比較內容,比較內容請用String.equals()。
s1 == s2這個非常好理解,s1、s2在賦值時,均使用的字符串字面量,說白話點,就是直接把字符串寫死,在編譯期間,這種字面量會直接放入class文件的常量池中,從而實現復用,載入運行時常量池后,s1、s2指向的是同一個內存地址,所以相等。
s1 == s3這個地方有個坑,s3雖然是動態拼接出來的字符串,但是所有參與拼接的部分都是已知的字面量,在編譯期間,這種拼接會被優化,編譯器直接幫你拼好,因此String s3 = "Hel" + "lo";在class文件中被優化成String s3 = "Hello";,所以s1 == s3成立。
s1 == s4當然不相等,s4雖然也是拼接出來的,但new String("lo")這部分不是已知字面量,是一個不可預料的部分,編譯器不會優化,必須等到運行時才可以確定結果,結合字符串不變定理,鬼知道s4被分配到哪去了,所以地址肯定不同。
s1 == s9也不相等,道理差不多,雖然s7、s8在賦值的時候使用的字符串字面量,但是拼接成s9的時候,s7、s8作為兩個變量,都是不可預料的,編譯器畢竟是編譯器,不可能當解釋器用,所以不做優化,等到運行時,s7、s8拼接成的新字符串,在堆中地址不確定,不可能與方法區常量池中的s1地址相同。
s4 == s5已經不用解釋了,絕對不相等,二者都在堆中,但地址不同。
s1 == s6這兩個相等完全歸功于intern方法,s5在堆中,內容為Hello ,intern方法會嘗試將Hello字符串添加到常量池中,并返回其在常量池中的地址,因為常量池中已經有了Hello字符串,所以intern方法直接返回地址;而s1在編譯期就已經指向常量池了,因此s1和s6指向同一地址,相等。
這只是讀書筆記,大多內容都是來自于其他前輩的帖子和《深入理解Java虛擬機》這本書,所有來源均列出,供大家閱讀
Java字符串池和字符串堆的內存分配
String放入運行時常量池的時機與String.intern()方法解惑
Java 6,7,8 中的 String.intern – 字符串池
Java中的字符串常量池與Java中的堆和棧的區別
Java字符串池(String Pool)深度解析
觸摸java常量池
Java中幾種常量池的區分