目錄
1.常量池與Class常量池
2.運行時常量池
- 運行時常量池的簡介
- 方法區的Class文件信息,Class常量池和運行時常量池的三者關系
3.字符串常量池
- 字符串常量池的簡介
- 采用字面值的方式創建字符串對象
- 采用new關鍵字新建一個字符串對象
- 字符串池的優缺點
4.字符串常量池和運行時常量池之間的藕斷絲連
- 常量池和字符串常量池的版本變化
- String.intern在JDK6和JDK7之后的區別(重難點)
1.常量池
常量池,也叫 Class 常量池(常量池==Class常量池)。Java文件被編譯成 Class文件,Class文件中除了包含類的版本、字段、方法、接口等描述信息外,還有一項就是常量池,常量池是當Class文件被Java虛擬機加載進來后存放在方法區 各種字面量 (Literal)和 符號引用 。
在Class文件結構中,最頭的4個字節用於 存儲魔數 (Magic Number),用於確定一個文件是否能被JVM接受,再接着4個字節用於 存儲版本號,前2個字節存儲次版本號,后2個存儲主版本號,再接着是用於存放常量的常量池常量池主要用於存放兩大類常量:字面量和符號引用量,字面量相當於Java語言層面常量的概念,如文本字符串,聲明為final的常量值等,符號引用則屬於編譯原理方面的概念。如下
2.運行時常量池
2.1運行時常量池的簡介
運行時常量池是方法區的一部分。運行時常量池是當Class文件被加載到內存后,Java虛擬機會 將Class文件常量池里的內容轉移到運行時常量池里(運行時常量池也是每個類都有一個)。運行時常量池相對於Class文件常量池的另外一個重要特征是具備動態性,Java語言並不要求常量一定只有編譯期才能產生,也就是並非預置入Class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中
2.2方法區的Class文件信息,Class常量池和運行時常量池的三者關系
字符串常量池
3.1字符串常量池的簡介
字符串常量池又稱為:字符串池,全局字符串池,英文也叫String Pool。 在工作中,String類是我們使用頻率非常高的一種對象類型。JVM為了提升性能和減少內存開銷,避免字符串的重復創建,其維護了一塊特殊的內存空間,這就是我們今天要討論的核心:字符串常量池。字符串常量池由String類私有的維護。
我們理清幾個概念:
在JDK7之前字符串常量池是在永久代里邊的,但是在JDK7之后,把字符串常量池分進了堆里邊。看下面兩張圖:
在堆中的字符串常量池: 里邊的字符串常量池存放的是字符串的引用或者字符串(兩者都有)下面例子會有具體的講
符號引用表會在下面講
我們知道,在Java中有兩種創建字符串對象的方式:
- 采用字面值的方式賦值
- 采用new關鍵字新建一個字符串對象。這兩種方式在性能和內存占用方面存在着差別。
3.2采用字面值的方式創建字符串對象
package Oneday;
public class a {
public static void main(String[] args) {
String str1="aaa";
String str2="aaa";
System.out.println(str1==str2);
}
}
運行結果:
true
采用字面值的方式創建一個字符串時,JVM首先會去字符串池中查找是否存在"aaa"這個對象,如果不存在,則在字符串池中創建"aaa"這個對象,然后將池中"aaa"這個對象的引用地址返回給字符串常量str,這樣str會指向池中"aaa"這個字符串對象;如果存在,則不創建任何對象,直接將池中"aaa"這個對象的地址返回,賦給字符串常量。
對於上述的例子:這是因為,創建字符串對象str2時,字符串池中已經存在"aaa"這個對象,直接把對象"aaa"的引用地址返回給str2,這樣str2指向了池中"aaa"這個對象,也就是說str1和str2指向了同一個對象,因此語句System.out.println(str1== str2)輸出:true
3.3采用new關鍵字新建一個字符串對象
package Oneday;
public class a {
public static void main(String[] args) {
String str1=new String("aaa");
String str2=new String("aaa");
System.out.println(str1==str2);
}
}
運行結果:
false
采用new關鍵字新建一個字符串對象時,JVM首先在字符串常量池中查找有沒有"aaa"這個字符串對象,如果有,則不在池中再去創建"aaa"這個對象了,直接在堆中創建一個"aaa"字符串對象,然后將堆中的這個"aaa"對象的地址返回賦給引用str1,這樣,str1就指向了堆中創建的這個"aaa"字符串對象;如果沒有,則首先在字符串常量池池中創建一個"aaa"字符串對象,然后再在堆中創建一個"aaa"字符串對象,然后將堆中這個"aaa"字符串對象的地址返回賦給str1引用,這樣,str1指向了堆中創建的這個"aaa"字符串對象。
對於上述的例子:
因為,采用new關鍵字創建對象時,每次new出來的都是一個新的對象,也即是說引用str1和str2指向的是兩個不同的對象,因此語句
System.out.println(str1 == str2)輸出:false
字符串池的實現有一個前提條件:String對象是不可變的。因為這樣可以保證多個引用可以同時指向字符串池中的同一個對象。如果字符串是可變的,那么一個引用操作改變了對象的值,對其他引用會有影響,這樣顯然是不合理的。
3.4字符串池的優缺點
字符串池的優點就是避免了相同內容的字符串的創建,節省了內存,省去了創建相同字符串的時間,同時提升了性能;另一方面,字符串池的缺點就是犧牲了JVM在常量池中遍歷對象所需要的時間,不過其時間成本相比而言比較低
4.字符串常量池和運行時常量池之間的藕斷絲連
博主為啥要把他倆放在一起講呢,主要是隨着JDK的改朝換代,字符串常量池有很大的變動,和運行時常量池有關。而且網上眾說紛紜,我真的在看的時候ctm了,所以博主花很長時間把這一塊講明白,如果有錯誤或者異議可以通知博主。博主一定會在第一時間參與討論
4.1常量池和字符串常量池的版本變化
- 在JDK1.7之前運行時常量池邏輯包含字符串常量池存放在方法區, 此時hotspot虛擬機對方法區的實現為永久代
- 在JDK1.7 字符串常量池被從方法區拿到了堆中, 這里沒有提到運行時常量池,也就是說 字符串常量池被單獨拿到堆,運行時常量池剩下的東西還在方法區, 也就是hotspot中的永久代
- 在JDK1.8 hotspot移除了永久代用元空間(Metaspace)取而代之, 這時候字符串常量池還在堆, 運行時常量池還在方法區, 只不過方法區的實現從永久代變成了元空間(Metaspace)
4.2String.intern在JDK6和JDK7之后的區別(重點)
JDK6和JDK7中該方法的功能是一致的,不同的是常量池位置的改變(JDK7將常量池放在了堆空間中),下面會具體說明。intern的方法返回字符串對象的規范表示形式。其中它做的事情是:首先去判斷該字符串是否在常量池中存在,如果存在返回常量池中的字符串,如果在字符串常量池中不存在,先在字符串常量池中添加該字符串,然后返回引用地址
例子1:
String s1 = new String("1");
s1.intern();
String s2 = "1";
System.out.println(s1 == s2);
運行結果:
JDK6運行結果:false
JDK7運行結果:false
我們首先看一張圖:
上邊例子中s1是new出來對象存放的位置的引用,s2是存放在字符串常量池的字符串的引用,所以兩者不同
例子2:
String s1 = new String("1");
System.out.println(s1.intern() == s1);
運行結果:
JDK6運行結果:false
JDK7運行結果:false
上邊例子中s1是new出來對象存放的位置的引用,s1.intern()返回的是字符串常量池里字符串的引用
例子3:
String s1 = new String("1") + new String("1");
s1.intern();
String s2 = "11";
System.out.println(s1 == s2);
運行結果:
JDK6運行結果:false
JDK7運行結果:true
JDK6中,s1.intern()運行時,首先去常量池查找,發現沒有該常量,則在常量池中開辟空間存儲"11",返回常量池中的值(注意這里也沒有使用該返回值),第三行中,s2直接指向常量池里邊的字符串,所以s1和s2不相等。有可能會有小伙伴問為啥s1.intern()發現沒有該常量呢,那是因為:
String s1 = new String(“1”) + new String(“1”);這行代碼實際操作是,創建了一個StringBuilder對象,然后一路append,最后toString,而toString其實是又重新new了一個String對象,然后把對象給s1,此時並沒有在字符串常量池中添加常量
JDK7中,由於字符串常量池在堆空間中,所以在s1.intern()運行時,發現字符串 常量池沒有常量,則添加常量並使其指向堆空間地址,返回堆空間地址(注意這里也沒有使用該返回值),這時s2通過查找常量池中的常量,同樣也指向了堆空間地址,所以s1和s2相等。
例子4:
String s1 = new String("1") + new String("1");
System.out.println(s1.intern() == s1);
JDK6中,常量池在永久代中,s1.intern()去常量池中查找"11",發現沒有該常量,則在常量池中開辟空間存儲"11",返回常量池中的值,s1指向堆空間地址,所以二者不相等。
JDK7中,常量池在堆空間,s1.intern()去常量池中查找"11",發現沒有該常量,則在字符串常量池中開辟空間,指向堆空間地址,則返回字符串常量池指向的堆空間地址,s1也是堆空間地址,所以二者相等。
例子5:
package Oneday;
public class a {
public static void main(String[] args) {
String str=new String("123");
System.out.println(str.intern()==str);
}
}
運行結果:
JDK7:false
