官术网_书友最值得收藏!

1.16 字符串創(chuàng)建與存儲(chǔ)的機(jī)制

在Java語(yǔ)言中,對(duì)String對(duì)象提供了專門的字符串常量池。為了便于理解,首先介紹在Java語(yǔ)言中字符串的存儲(chǔ)機(jī)制,在Java語(yǔ)言中,字符串的聲明與初始化主要有如下兩種情況:

1)對(duì)于String s1=new String("abc")語(yǔ)句與String s2=new String("abc")語(yǔ)句,存在兩個(gè)引用對(duì)象s1、s2,兩個(gè)內(nèi)容相同的字符串對(duì)象"abc",它們?cè)趦?nèi)存中的地址是不同的。只要用到new總會(huì)生成新的對(duì)象。

2)對(duì)于String s1="abc"語(yǔ)句與String s2="abc"語(yǔ)句,在JVM中存在著一個(gè)字符串池,其中保存著很多String對(duì)象,并且可以被共享使用,s1、s2引用的是同一個(gè)常量池中的對(duì)象。由于String的實(shí)現(xiàn)采用了Flyweight的設(shè)計(jì)模式,當(dāng)創(chuàng)建一個(gè)字符串常量的時(shí)候,例如String s ="abc",會(huì)首先在字符串常量池中查找是否已經(jīng)有相同的字符串被定義,它的判斷依據(jù)是String類equals(Object obj)方法的返回值。如果已經(jīng)定義,那么直接獲取對(duì)其的引用,此時(shí)不需要?jiǎng)?chuàng)建新的對(duì)象,如果沒(méi)有定義,那么首先創(chuàng)建這個(gè)對(duì)象,然后把它加入字符串池中,再將它的引用返回。由于String是不可變類,一旦創(chuàng)建好了就不能被修改,因此String對(duì)象可以被共享而且不會(huì)導(dǎo)致程序的混亂。

具體而言:

再例如:

為了便于理解,可以把String s = new String("abc")語(yǔ)句的執(zhí)行人為地分解成兩個(gè)過(guò)程:第一個(gè)過(guò)程是新建對(duì)象的過(guò)程,即new String("abc"),第二個(gè)過(guò)程是賦值的過(guò)程,即String s=new String("abc")。由于第二個(gè)過(guò)程中只是定義了一個(gè)名為s的String類型的變量,將一個(gè)String類型對(duì)象的引用賦值給s,因此在這個(gè)過(guò)程中不會(huì)創(chuàng)建新的對(duì)象。第一個(gè)過(guò)程中new String("abc")會(huì)調(diào)用String類的構(gòu)方法:

由于在調(diào)用這個(gè)構(gòu)造方法的時(shí)候,傳入了一個(gè)字符串常量,因此語(yǔ)句new String("abc")也就等價(jià)于"abc"和new String()兩個(gè)操作。如果在字符串池中不存在"abc",那么會(huì)創(chuàng)建一個(gè)字符串常量"abc",并將其添加到字符串池中,如果存在,那么不創(chuàng)建,然后new String()會(huì)在堆中創(chuàng)建一個(gè)新的對(duì)象。所以str3與str4指向的是堆中不同的String對(duì)象,地址自然也不相同了。如圖1-6所示。

圖1-6 兩種字符串存儲(chǔ)方式

從上面的分析可以看出,在創(chuàng)建字符串對(duì)象的時(shí)候,會(huì)根據(jù)不同的情況來(lái)確定字符串被放在常量區(qū)還是堆中。而intern方法主要用來(lái)把字符串放入字符串常量池中。在以下兩種情況下,字符串會(huì)被放到字符串常量池中:

1)直接使用雙引號(hào)聲明的String對(duì)象都會(huì)直接存儲(chǔ)在常量池中。

2)通過(guò)調(diào)用String提供的intern方法把字符串放到常量池中,intern方法會(huì)從字符串常量池中查詢當(dāng)前字符串是否存在,若不存在,則會(huì)將當(dāng)前字符串放入常量池中。

intern方法在JDK1.6和JDK1.8下有著不同的工作原理,下面通過(guò)一個(gè)例子來(lái)介紹它們的不同之處。

以上程序的運(yùn)行結(jié)果為:

從上面例子的運(yùn)行結(jié)果可以看出,在JDK1.6及以前的版本中,兩種寫法得到的結(jié)果是類似的,從JDK1.7開始的版本中對(duì)intern方法的處理是不同的,下面分別介紹這兩種不同的實(shí)現(xiàn)方式。

(1)在JDK1.6及以前版本中的實(shí)現(xiàn)原理

intern()方法會(huì)查詢字符串常量池是否存在當(dāng)前字符串,若不存在則將當(dāng)前字符串復(fù)制到字符串常量池中,并返回字符串常量池中的引用。

如圖1-7所示,在JDK1.6中的字符串常量池是在Perm區(qū)中,前面提到過(guò)使用引號(hào)聲明的字符串會(huì)直接存儲(chǔ)在字符串常量池中,而new出來(lái)的String對(duì)象是放在堆區(qū)。即使通過(guò)調(diào)用intern方法把字符串放入字符串常量區(qū)中,由于堆和Perm區(qū)是兩塊獨(dú)立的存儲(chǔ)空間,存儲(chǔ)在堆和Perm區(qū)中的對(duì)象一定會(huì)有不同的存儲(chǔ)空間,因此,它們也有不同的地址。

圖1-7 intern方法在JDK1.6及更低版本中的實(shí)現(xiàn)原理

(2)在JDK1.7及以上版本中的實(shí)現(xiàn)原理

intern()方法會(huì)先查詢字符串常量池是否存在當(dāng)前字符串,若字符串常量池中不存在則再?gòu)亩阎胁樵儯缓蟠鎯?chǔ)并返回相關(guān)引用;若都不存在則將當(dāng)前字符串復(fù)制到字符串常量池中,并返回字符串常量池中的引用。實(shí)現(xiàn)原理如圖1-8所示。

圖1-8 intern方法在JDK1.7及以上版本中的實(shí)現(xiàn)原理

1)String s1=new String("a")。這句代碼生成了兩個(gè)對(duì)象,常量池中的“a”和堆中的字符串對(duì)象。s1.intern();這一句代碼執(zhí)行的時(shí)候,s1對(duì)象首先去常量池中尋找,由于發(fā)現(xiàn)“a”已經(jīng)在常量池里了,因此不做任何操作。

2)接下來(lái)執(zhí)行String s2="a"。這句代碼是在棧中生成一個(gè)s2的引用,這個(gè)引用指向常量池中的“a”對(duì)象。顯然s1與s2有不同的地址。

3)String s3=new String("a")+new String("a")。這行代碼在字符串常量池中生成“a”(由于已經(jīng)存在了,不會(huì)創(chuàng)建新的字符串),并且在堆中生成一個(gè)字符串對(duì)象(字符串的內(nèi)容為“aa”),s3指向這個(gè)堆中的對(duì)象。需要注意的是,此時(shí)常量池中還不存在字符串“aa”。

4)接下來(lái)執(zhí)行s3.intern()。這句代碼執(zhí)行的過(guò)程是,首先判斷“aa”在字符串常量區(qū)中不存在,此時(shí)會(huì)把“aa”放入字符串常量區(qū)中,在JDK1.6中,會(huì)在常量池中生成一個(gè)“aa”的對(duì)象。由于從JDK1.7開始字符串常量池從Perm區(qū)移到堆中了,在這種情況下,常量池中不需要再存儲(chǔ)一份對(duì)象,而是直接存儲(chǔ)堆中的引用。這份引用指向s3引用的對(duì)象。如圖1-9所示,字符串常量區(qū)中的字符串“aa”直接指向堆中的字符串對(duì)象。由此可見(jiàn),這種實(shí)現(xiàn)方式能夠大大降低字符串所占用的內(nèi)存空間。

5)執(zhí)行String s4 = "aa"的時(shí)候,由于這個(gè)字符串在字符串常量區(qū)中已經(jīng)存在了(指向s3引用對(duì)象的一個(gè)引用),所以s4引用就指向和s3一樣了。因此s3==s4的結(jié)果是true。

如果把上面例子中的代碼的順序調(diào)整,那么就會(huì)得到不同的運(yùn)行結(jié)果,如下例所示:

上述代碼的運(yùn)行結(jié)果為:

1)String s1=newString("a"),生成了常量池中的字符串“a”、堆空間中的字符串對(duì)象和指向堆空間對(duì)象的引用s1。

2)String s2="a",這行代碼是生成一個(gè)s2的引用并直接指向常量池中的“a”對(duì)象。

3)s1.intern(),由于“a”已經(jīng)在字符串常量區(qū)中存在了,因此這一行代碼沒(méi)有什么實(shí)際作用。顯然s1與s2的引用地址是不相同的。

4)String s3 = new String("a") + newString("a"),這行代碼在字符串常量池中生成“a”(由于已經(jīng)存在了,不會(huì)創(chuàng)建新的字符串),并且在堆中生成一個(gè)字符串對(duì)象(字符串的內(nèi)容為“aa”),s3指向這個(gè)堆中的對(duì)象。需要注意的是,此時(shí)常量池中還不存在字符串“aa”。

5)String s4 = "aa",這一行代碼執(zhí)行的時(shí)候,首先在字符串常量區(qū)中生成字符串“aa”,接著s4指向字符串常量區(qū)中的“aa”。

6)s3.intern(),由于“aa”已經(jīng)存在了,這一行代碼沒(méi)有實(shí)際的作用。

引申1:intern方法內(nèi)部是怎么實(shí)現(xiàn)的?

intern方法主要通過(guò)JNI調(diào)用C++實(shí)現(xiàn)的StringTable的intern方法來(lái)實(shí)現(xiàn)的,StringTable的intern方法與Java中的HashMap的實(shí)現(xiàn)非常類似,但是C++中的StringTable沒(méi)有自動(dòng)擴(kuò)容的功能。在JDK1.6中,它的默認(rèn)大小為1009。由此可見(jiàn),String的String Pool使用了一個(gè)固定大小的Hashtable來(lái)實(shí)現(xiàn),如果往字符串常量區(qū)中放入過(guò)多的字符串,那么就會(huì)造成Hash沖突嚴(yán)重,解決沖突需要額外的時(shí)間,這就會(huì)導(dǎo)致使用字符串常量池的時(shí)候性能會(huì)下降。因此在編寫代碼的時(shí)候需要注意這個(gè)問(wèn)題。為了提供一定的靈活性,JDK1.7中提供了下面的參數(shù)來(lái)指定StringTable的長(zhǎng)度:

引申2:如何驗(yàn)證從JDK1.7開始字符串常量被移到堆中了?

可以通過(guò)intern方法把大量的字符串都存放在字符串常量池中,直到常量池空間不夠了導(dǎo)致溢出,根據(jù)拋出的異常可以查看是哪部分內(nèi)存不夠而導(dǎo)致溢出的,如下例所示:

在JDK1.6及以下的版本運(yùn)行會(huì)拋出“java.lang.OutOfMemoryError:PermGen space”異常,說(shuō)明字符串常量池是存儲(chǔ)在永久代中的。而在JDK1.7及以上的版本中運(yùn)行上述代碼,會(huì)拋出“java.lang.OutOfMemoryError:Java heap space”異常,說(shuō)明從JDK1.7開始,字符串常量池被存儲(chǔ)在堆中。

常見(jiàn)面試筆試題:

(1)new String("abc")創(chuàng)建了幾個(gè)對(duì)象?

答案:一個(gè)或兩個(gè)。如果常量池中原來(lái)有“abc”,那么只創(chuàng)建一個(gè)對(duì)象,如果常量池中原來(lái)沒(méi)有“abc”,那么就會(huì)創(chuàng)建兩個(gè)對(duì)象。

(2)Java中由substring方法是否會(huì)引起內(nèi)存泄漏?

答案:這道題考查了兩方面的內(nèi)容,一方面是對(duì)Java中String類的substring方法的理解,另一方面考查的是對(duì)Java中內(nèi)存泄漏的理解。眾所周知,在Java編程中,程序員是不需要關(guān)心內(nèi)存的分配與釋放的,這些工作都是由垃圾回收器來(lái)完成的。但是垃圾回收器只能回收不再被使用的對(duì)象,如果想讓垃圾回收器回收一個(gè)對(duì)象,那么必須要保證這個(gè)對(duì)象不再被引用,否則垃圾回收器無(wú)法回收這個(gè)對(duì)象。在Java中,內(nèi)存泄漏通常指的是程序員認(rèn)為一個(gè)對(duì)象會(huì)被垃圾回收器收集,但是由于某種原因垃圾回收器無(wú)法回收這個(gè)對(duì)象。

對(duì)于這道題而言,首先需要理解subString的內(nèi)部實(shí)現(xiàn)原理。只有Java1.6之前的版本才會(huì)有內(nèi)存泄漏的問(wèn)題。substring(int beginIndex, int endIndex)方法返回一個(gè)字符串的子串,這個(gè)子串從beginIndex開始,結(jié)束于endindex-1(下標(biāo)從0開始,子字符串包含beginIndex而不包含endIndex)。例如:

前面介紹過(guò)String是不可變量,給字符串賦新值會(huì)創(chuàng)建一個(gè)新的字符串。也就是說(shuō)在上面的例子中,在執(zhí)行第一行代碼的時(shí)候,會(huì)在常量池中創(chuàng)建一個(gè)字符串“Hello world”,第二行代碼執(zhí)行后,s會(huì)指向常量池中新的字符串“world”,因此,“Hello world”就沒(méi)有人訪問(wèn)了,可以被垃圾回收器回收。但是在Java1.6中,“Hello world”是無(wú)法被垃圾回收器回收的。為了理解其中的原因,下面首先給出substring的實(shí)現(xiàn)源碼:

在JDK1.6中,String類中存儲(chǔ)了三個(gè)重要的屬性:char[] value、int offset和int count,分別用來(lái)表示字符串對(duì)應(yīng)的字符數(shù)組、數(shù)組的起始位置及String中包含的字符數(shù)。由這三個(gè)變量就可以唯一決定一個(gè)字符串。在調(diào)用substring方法的時(shí)候,雖然會(huì)創(chuàng)建一個(gè)新的字符串,但是新對(duì)象的value仍然會(huì)使用原來(lái)字符串的value屬性。只是count和offset的值不一樣而已,如圖1-9所示。

圖1-9 String在JDK1.6中的存儲(chǔ)方式

雖然字符串在堆中是一個(gè)新的對(duì)象,但是它與原字符串都指向了相同的字符數(shù)組。對(duì)于垃圾回收器來(lái)說(shuō),這個(gè)字符數(shù)組仍然被使用,因此無(wú)法回收。“Hello world”這個(gè)字符串雖然不被使用了,但是仍然無(wú)法被垃圾回收器回收,因此就造成了內(nèi)存泄漏。

從JDK1.7開始,這個(gè)方法內(nèi)部的實(shí)現(xiàn)被修改了,從而避免了內(nèi)存泄漏,下面是JDK1.7中substring的實(shí)現(xiàn)源碼:

從上面的代碼可以看出,在copyOfRange方法中,新的子串通過(guò)new char[newLength]創(chuàng)建了一個(gè)獨(dú)立的字符數(shù)組,顯然沒(méi)有與原字符串使用相同的字符數(shù)組,如圖1-10所示。

圖1-10 String在JDK1.7中的存儲(chǔ)方式

從圖1-10可以看出,在調(diào)用substring后,字符串“Hello world”將不再被引用,因此可以被垃圾回收器回收。

主站蜘蛛池模板: 桦南县| 溧阳市| 仲巴县| 大丰市| 会宁县| 鄂伦春自治旗| 镇沅| 沙洋县| 洮南市| 广平县| 奉贤区| 安庆市| 政和县| 彰化市| 名山县| 远安县| 蕉岭县| 温宿县| 齐河县| 观塘区| 中阳县| 韶山市| 剑河县| 阜宁县| 桂阳县| 防城港市| 霍邱县| 五大连池市| 宁国市| 牙克石市| 耿马| 青冈县| 奇台县| 通河县| 香河县| 马关县| 苗栗市| 乌鲁木齐县| 定日县| 汾西县| 玉龙|