Java之String字符串在JVM中的存儲(chǔ)及其內(nèi)存地址的問題
String字符串在JVM中的存儲(chǔ)及其內(nèi)存地址問題
概要
String 的內(nèi)存地址問題是Java面試中常被問到的一個(gè)點(diǎn),比如直接復(fù)制的String 和 new 出來的 String 有什么區(qū)別?
字符串拼接過程中地址是如何變化的?
等等。要想理清這些地址問題,我們首先應(yīng)當(dāng)知道 String 在JVM中是如何存儲(chǔ)的。
1.String 對象在JVM中的存儲(chǔ)
先給出定義:
字符串存放在方法區(qū)的常量池(Constant Pool)中,常量池是什么呢?
常量池在編譯期間就會(huì)被生成,用于存放編譯器生成的各種字面量和符號引用。
比如int a = 8;String a = “abc”; 這種里面的8和"abc"在編譯階段就會(huì)被放入常量池。
當(dāng)然常量池在運(yùn)行期間也可以被拓展,將新的常量放入池中,用的比較多的就是String 的 intern() 方法,后面會(huì)詳細(xì)描述。
再來看這樣一段簡單的代碼
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
為什么會(huì)是這樣的結(jié)果呢?
按照程序的執(zhí)行順序,首先,“aa”作為一個(gè)字面量,也就是常量,會(huì)在編譯期間被加入常量池,然后JVM將其在常量池中的地址賦給str;到了str1這里,JVM先在常量池中查找有沒有“aa”這個(gè)常量,由于給str賦值的時(shí)候已經(jīng)在常量池里創(chuàng)建過“aa”了,所以JVM直接返回這個(gè)地址給str1。因此str和str1的地址是一樣的,結(jié)果為true。
來到str2和str3,這兩個(gè)是new出來的String,是對象,對象存放在哪里呢?存放在堆中,所以本質(zhì)上str2和str3存放的是這兩個(gè)對象在堆中的地址。new了兩次,他倆是不同的對象,所以str2和str3地址不相同,返回false。
現(xiàn)在再來看最后一個(gè)輸出,str == str2?這倆一個(gè)存的是常量池中的地址,一個(gè)存的是堆中的地址,怎么可能相等嘛,返回false。
2.關(guān)于字符串拼接
來看這樣一段代碼
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"兩個(gè)字符串。
根據(jù)上面的結(jié)果我們可以得到如下兩個(gè)結(jié)論:
(1)字符串相加的時(shí)候,都是靜態(tài)字符串相加的結(jié)果會(huì)添加到常量池,如果常量池中有這個(gè)結(jié)果則直接返回其引用;如果沒有,則創(chuàng)建該字符串再返回引用。因?yàn)楝F(xiàn)在常量池已經(jīng)有 “Hello2” 了,所以e和f的值其實(shí)都是常量池中 “Hello2” 的引用,a、e、f都是相等的。
(2)字符串相加的時(shí)候,如果其中含有變量,如d中的b,則不會(huì)進(jìn)入常量池中。在IDEA中DeBug,我們強(qiáng)制進(jìn)入 String d = b + “2”; 這條語句,會(huì)發(fā)現(xiàn)程序底層其實(shí)是先創(chuàng)建了一個(gè)StringBuffer,然后調(diào)用append()方法,把b和"2"加入該StringBuffer,然后調(diào)用toString()返回拼接后的字符串,而最終的toString()源碼長這個(gè)樣子:
@Override public String toString() { // Create a copy, don't share the array return new String(value, 0, count); }
看到了嗎!它返回了一個(gè)new的String!這個(gè)String的地址當(dāng)然不會(huì)任何一個(gè)現(xiàn)有的對象相同了。關(guān)于字符串拼接,分清楚這兩種情況即可。
3.關(guān)于intern()方法
前面我們提到過,new 出來的String不直接存放在常量池中,而intern()方法的作用就是把這個(gè)字符串加入到常量池中,然后返回這個(gè)字符串在常量池中的地址。
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()時(shí),JVM在常量池中查詢有沒有"ab"這個(gè)字符串,因?yàn)樵诮ostr3賦值時(shí)已經(jīng)創(chuàng)建過,所以直接返回其地址。
str4由兩個(gè)變量相加得到,所以也相當(dāng)于是new出來的。
還有一個(gè)需要注意的點(diǎn)就是:
如果只是調(diào)用str5.intern(),那str5本身并不會(huì)改變,還是存放的堆里的地址,想讓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中字符串存儲(chǔ)在JVM的哪部分?
1、 java7之前,方法區(qū)位于永久代(PermGen),永久代和堆相互隔離,永久代的大小在啟動(dòng)JVM時(shí)可以設(shè)置一個(gè)固定值,不可變;
2、 java7中,static變量從永久代移到堆中;
3、 java8中,取消永久代,方法存放于元空間(Metaspace),元空間仍然與堆不相連,但與堆共享物理內(nèi)存,邏輯上可認(rèn)為在堆中
現(xiàn)在總結(jié)一下:基本類型的變量數(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)建了一個(gè)值為holten的String對象;
- 第二句執(zhí)行時(shí),因?yàn)槌A砍刂写嬖趆olten所以就不再創(chuàng)建新的String對象了。
- 此時(shí)該字符串的引用在虛擬機(jī)棧里面。
String對象
String a = new String("holtenObj"); String b = new String("holtenObj");
- Class被加載時(shí)就在常量池中創(chuàng)建了一個(gè)值為holtenObj的String對象,第一句執(zhí)行時(shí)會(huì)在堆里創(chuàng)建new String("holtenObj")對象;
- 第二句執(zhí)行時(shí),因?yàn)槌A砍刂写嬖趆oltenObj所以就不再創(chuàng)建新的String對象了,直接在堆里創(chuàng)建new String("holtenObj")對象。
驗(yàn)證一下
/** * 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 } }
返回結(jié)果:
true
false
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
java編程SpringSecurity入門原理及應(yīng)用簡介
Spring 是非常流行和成功的 Java 應(yīng)用開發(fā)框架,Spring Security 正是 Spring 家族中的成員。Spring Security 基于 Spring 框架,提供了一套 Web 應(yīng)用安全性的完整解決方案2021-09-09Java利用廣度優(yōu)先搜索實(shí)現(xiàn)抓牛問題
廣度優(yōu)先搜索是最簡便的圖的搜索算法之一,這一算法也是很多重要的圖的算法的原型。本文將利用廣度優(yōu)先搜索實(shí)現(xiàn)抓牛問題,感興趣的可以了解下2022-06-06springboot?serviceImpl初始化注入對象實(shí)現(xiàn)方式
這篇文章主要介紹了springboot?serviceImpl初始化注入對象實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05SSM框架下如何實(shí)現(xiàn)數(shù)據(jù)從后臺傳輸?shù)角芭_
這篇文章主要介紹了SSM框架下如何實(shí)現(xiàn)數(shù)據(jù)從后臺傳輸?shù)角芭_,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-05-05jboss( WildFly)上運(yùn)行 springboot程序的步驟詳解
這篇文章主要介紹了jboss( WildFly)上運(yùn)行 springboot程序的步驟詳解,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02Java傳入用戶名和密碼并自動(dòng)提交表單實(shí)現(xiàn)登錄到其他系統(tǒng)的實(shí)例代碼
這篇文章主要介紹了Java傳入用戶名和密碼并自動(dòng)提交表單實(shí)現(xiàn)登錄到其他系統(tǒng),非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-01-01