Java之String字符串在JVM中的存儲及其內(nèi)存地址的問題
String字符串在JVM中的存儲及其內(nèi)存地址問題
概要
String 的內(nèi)存地址問題是Java面試中常被問到的一個點,比如直接復制的String 和 new 出來的 String 有什么區(qū)別?
字符串拼接過程中地址是如何變化的?
等等。要想理清這些地址問題,我們首先應當知道 String 在JVM中是如何存儲的。
1.String 對象在JVM中的存儲
先給出定義:
字符串存放在方法區(qū)的常量池(Constant Pool)中,常量池是什么呢?
常量池在編譯期間就會被生成,用于存放編譯器生成的各種字面量和符號引用。
比如int a = 8;String a = “abc”; 這種里面的8和"abc"在編譯階段就會被放入常量池。
當然常量池在運行期間也可以被拓展,將新的常量放入池中,用的比較多的就是String 的 intern() 方法,后面會詳細描述。
再來看這樣一段簡單的代碼
String str = "aa"; String str1 = "aa"; String str2 = new String("aa"); String str3 = new String("aa"); System.out.println(str == str1);//true System.out.println(str2 == str3);//false System.out.println(str == str2);//false
為什么會是這樣的結果呢?
按照程序的執(zhí)行順序,首先,“aa”作為一個字面量,也就是常量,會在編譯期間被加入常量池,然后JVM將其在常量池中的地址賦給str;到了str1這里,JVM先在常量池中查找有沒有“aa”這個常量,由于給str賦值的時候已經(jīng)在常量池里創(chuàng)建過“aa”了,所以JVM直接返回這個地址給str1。因此str和str1的地址是一樣的,結果為true。
來到str2和str3,這兩個是new出來的String,是對象,對象存放在哪里呢?存放在堆中,所以本質上str2和str3存放的是這兩個對象在堆中的地址。new了兩次,他倆是不同的對象,所以str2和str3地址不相同,返回false。
現(xiàn)在再來看最后一個輸出,str == str2?這倆一個存的是常量池中的地址,一個存的是堆中的地址,怎么可能相等嘛,返回false。
2.關于字符串拼接
來看這樣一段代碼
String a = "Hello2"; String b = "Hello"; final String c = "Hello"; String d = b + "2"; String e = c + "2"; String f = "Hello" + "2"; System.out.println(a==d);//false System.out.println(a==e);//true System.out.println(a==f);//true
首先a、b、c都是字面量直接賦值,所以現(xiàn)在常量池中有 “Hello2” 和 "Hello"兩個字符串。
根據(jù)上面的結果我們可以得到如下兩個結論:
(1)字符串相加的時候,都是靜態(tài)字符串相加的結果會添加到常量池,如果常量池中有這個結果則直接返回其引用;如果沒有,則創(chuàng)建該字符串再返回引用。因為現(xiàn)在常量池已經(jīng)有 “Hello2” 了,所以e和f的值其實都是常量池中 “Hello2” 的引用,a、e、f都是相等的。
(2)字符串相加的時候,如果其中含有變量,如d中的b,則不會進入常量池中。在IDEA中DeBug,我們強制進入 String d = b + “2”; 這條語句,會發(fā)現(xiàn)程序底層其實是先創(chuàng)建了一個StringBuffer,然后調(diào)用append()方法,把b和"2"加入該StringBuffer,然后調(diào)用toString()返回拼接后的字符串,而最終的toString()源碼長這個樣子:
@Override public String toString() { // Create a copy, don't share the array return new String(value, 0, count); }
看到了嗎!它返回了一個new的String!這個String的地址當然不會任何一個現(xiàn)有的對象相同了。關于字符串拼接,分清楚這兩種情況即可。
3.關于intern()方法
前面我們提到過,new 出來的String不直接存放在常量池中,而intern()方法的作用就是把這個字符串加入到常量池中,然后返回這個字符串在常量池中的地址。
String str1 = "a"; String str2 = "b"; String str3 = "ab"; String str4 = str1 + str2; String str5 = new String("ab"); System.out.println(str5 == str3);//false System.out.println(str5.intern() == str3);//true System.out.println(str5.intern() == str4);//false System.out.println(str5.intern() == str4.intern());//true
調(diào)用str5.intern()時,JVM在常量池中查詢有沒有"ab"這個字符串,因為在給str3賦值時已經(jīng)創(chuàng)建過,所以直接返回其地址。
str4由兩個變量相加得到,所以也相當于是new出來的。
還有一個需要注意的點就是:
如果只是調(diào)用str5.intern(),那str5本身并不會改變,還是存放的堆里的地址,想讓str5存放常量池中的地址需要把str5.intern()的返回值再賦給str5。
可以做如下測試:
String str3 = "ab"; String str5 = new String("ab"); str5.intern(); System.out.println(str5 == str3);//false str5 = str5.intern(); System.out.println(str5 == str3);//true
Java中字符串存儲在JVM的哪部分?
1、 java7之前,方法區(qū)位于永久代(PermGen),永久代和堆相互隔離,永久代的大小在啟動JVM時可以設置一個固定值,不可變;
2、 java7中,static變量從永久代移到堆中;
3、 java8中,取消永久代,方法存放于元空間(Metaspace),元空間仍然與堆不相連,但與堆共享物理內(nèi)存,邏輯上可認為在堆中
現(xiàn)在總結一下:基本類型的變量數(shù)據(jù)和對象的引用都是放在棧里面的,對象本身放在堆里面,顯式的String常量放在常量池,String對象放在堆中。
常量池的說明
常量池之前是放在方法區(qū)里面的,也就是在永久代里面的,從JDK7開始移到了堆里面。
這一改變我們可以從oracle的release version的notes里的** Important RFEs Addressed in JDK 7 **看到。
Area: HotSpot
Synopsis: In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application. This change will result in more data residing in the main Java heap, and less data in the permanent generation, and thus may require heap sizes to be adjusted. Most applications will see only relatively small differences in heap usage due to this change, but larger applications that load many classes or make heavy use of the String.intern() method will see more significant differences.
RFE: 6962931
String內(nèi)存位置說明
顯式的String常量
String a = "holten"; String b = "holten";
- 第一句代碼執(zhí)行后就在常量池中創(chuàng)建了一個值為holten的String對象;
- 第二句執(zhí)行時,因為常量池中存在holten所以就不再創(chuàng)建新的String對象了。
- 此時該字符串的引用在虛擬機棧里面。
String對象
String a = new String("holtenObj"); String b = new String("holtenObj");
- Class被加載時就在常量池中創(chuàng)建了一個值為holtenObj的String對象,第一句執(zhí)行時會在堆里創(chuàng)建new String("holtenObj")對象;
- 第二句執(zhí)行時,因為常量池中存在holtenObj所以就不再創(chuàng)建新的String對象了,直接在堆里創(chuàng)建new String("holtenObj")對象。
驗證一下
/** * Created by holten.gao on 2016/8/16. */ public class Main { public static void main(String[] args){ String str1 = "高小天"; String str2 = "高小天"; System.out.println(str1==str2);//true String str3 = new String("高大天"); String str4 = new String("高大天"); System.out.println(str3==str4);//false } }
返回結果:
true
false
總結
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
springboot?serviceImpl初始化注入對象實現(xiàn)方式
這篇文章主要介紹了springboot?serviceImpl初始化注入對象實現(xiàn)方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-05-05SSM框架下如何實現(xiàn)數(shù)據(jù)從后臺傳輸?shù)角芭_
這篇文章主要介紹了SSM框架下如何實現(xiàn)數(shù)據(jù)從后臺傳輸?shù)角芭_,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-05-05jboss( WildFly)上運行 springboot程序的步驟詳解
這篇文章主要介紹了jboss( WildFly)上運行 springboot程序的步驟詳解,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-02-02Java傳入用戶名和密碼并自動提交表單實現(xiàn)登錄到其他系統(tǒng)的實例代碼
這篇文章主要介紹了Java傳入用戶名和密碼并自動提交表單實現(xiàn)登錄到其他系統(tǒng),非常不錯,具有參考借鑒價值,需要的朋友可以參考下2017-01-01