Strings=newString("xyz")創(chuàng)建幾個實(shí)例

從面試題說起

String s = new String("xyz"); 創(chuàng)建了幾個實(shí)例?

這是一道很經(jīng)典的面試題,在一本所謂的Java寶典上,我看到的“標(biāo)準(zhǔn)答案”是這樣的:

兩個,一個堆區(qū)的“xyz”,一個棧區(qū)指向“xyz”的s。

這個所謂的“標(biāo)準(zhǔn)答案”槽點(diǎn)太多,后面我們慢慢分析。

雖然答案很離譜,但是我覺得這個問題本身也不具有什么意義,因?yàn)閱栴}沒有既定義“創(chuàng)建”的具體含義,又沒有指定“創(chuàng)建”的時間,是運(yùn)行時嗎?包不包括類加載的時候?有沒有上下文代碼語境?也沒有定義實(shí)例是指什么實(shí)例,是指Java實(shí)例嗎?還是單指String實(shí)例?包不包括JVM中的C++實(shí)例?

顯然,這個問題是一個“有問題的問題”。這個答案也是一個“有問題的答案”。

String結(jié)構(gòu)

在分析之前,為了方便后面畫內(nèi)存圖,我們需要對Java中的String結(jié)構(gòu)有一個大致了解:

Strings=newString("xyz")創(chuàng)建幾個實(shí)例

從上圖可以看出,String類有三個屬性:

  • value:char數(shù)組,用于用于存儲字符。
  • hash:緩存字符串的哈希碼,默認(rèn)為0(String的hash值在真正調(diào)用 hashCode 方法的時候才會去計(jì)算)。
  • serialVersionUID:序列化用的。

正常的問題與合理的解釋

在上面的題干上加上一些限定詞,可以得到一個新的問題:

String s = new String("xyz");創(chuàng)建幾個String實(shí)例?

對于這個問題,在網(wǎng)上能找到一些比較高贊的答案:

兩個。
一個是字符串字面量"xyz"所對應(yīng)的、存在于全局共享的常量池中的實(shí)例,
另一個是通過new String(String)創(chuàng)建并初始化的、內(nèi)容(字符)與"xyz"相同的實(shí)例。
考慮到如果常量池中如果有這個字符串,就只會創(chuàng)建一個。同時在棧區(qū)還會有一個對new出來的String實(shí)例的s。

考慮到了棧與堆,提到了常量池,我認(rèn)為這已經(jīng)達(dá)到大部分面試官對這個題目答案的期許了,或許這也是面試官想要考察的點(diǎn)。

但這個答案也僅是比較合理,并不完全正確。

首先,我不理解的是為什么很多答主總是用“常量池”來代替“字符串常量池”,在Java體系中,其實(shí)是有三個常量池的,三個常量池的概念和用處都不相同,混淆在一起容易給別人造成誤解。

其次,就算答主說的“常量池”就是“字符串常量池”,可“字符串常量池”中存的是String實(shí)例的引用,而不是字符串,這是有很大區(qū)別的。而且這個答案是沒有考慮代碼執(zhí)行的環(huán)境。

這些問題,下面都會一一分析。

分清變量和實(shí)例

我們先回到開頭的問題與“標(biāo)準(zhǔn)答案” :

問題:String s = new String("xyz"); 創(chuàng)建了幾個實(shí)例?
答案:兩個,一個堆區(qū)的“xyz”,一個棧區(qū)指向“xyz”的s

很明顯寫答案的人沒有把變量和實(shí)例分清楚。在Java里,變量就是變量,類型的變量只是對某個對象實(shí)例或者null的,不是實(shí)例本身。聲明變量的個數(shù)跟創(chuàng)建實(shí)例的個數(shù)沒有必然關(guān)系。

舉個例子:

String s1 = "xyz";  
String s2 = s1.concat("");  
String s3 = null;  
new String(s1);  

這段代碼會涉及3個String類型的變量:

  • s1,指向下面String實(shí)例的1
  • s2,指向與s1相同
  • s3,值為null,不指向任何實(shí)例

以及3個String實(shí)例:

  • "xyz"字面量對應(yīng)的駐留的字符串常量的String實(shí)例
  • ""字面量對應(yīng)的駐留的字符串常量的String實(shí)例
  • 通過new String(String)創(chuàng)建的新String實(shí)例,沒有任何變量指向它

類加載

對于String s = new String("xyz");創(chuàng)建幾個String實(shí)例?這個問題。

似乎網(wǎng)上的所有答案都把類加載過程和實(shí)際執(zhí)行過程合在一起分析的。

看起來好像是沒有什么問題的,因?yàn)橄胍獔?zhí)行某個代碼片段,其所在的類必然要被加載,而且對于同一個類加載器,最多加載一次。

但是我們看一下這段代碼的字節(jié)碼:

Strings=newString("xyz")創(chuàng)建幾個實(shí)例

似乎只出現(xiàn)了一次 new java/lang/String ,也就是只創(chuàng)建了一個String實(shí)例。也就是說原問題中的代碼在每執(zhí)行一次只會新創(chuàng)建一個String實(shí)例。 這里的ldc指令只是把先前在類加載過程中已經(jīng)創(chuàng)建好的一個String實(shí)例("xyz")的一個引用壓到操作數(shù)棧頂而已,并沒有創(chuàng)建新的String實(shí)例。

不是應(yīng)該有兩個實(shí)例嗎?還有一個String實(shí)例是在什么時候創(chuàng)建的呢?

我們都知道類加載的解析階段是Java虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程,根據(jù)JVM規(guī)范,符合規(guī)范的JVM實(shí)現(xiàn)應(yīng)該在類加載的過程中創(chuàng)建并駐留一個String實(shí)例作為常量來對應(yīng)"xyz"字面量,具體是在類加載的解析階段進(jìn)行的。這個常量是全局共享的,只在先前尚未有內(nèi)容相同的字符串駐留過的前提下才需要創(chuàng)建新的String實(shí)例。

所以你可以理解成,在類加載的解析階段,其實(shí)已經(jīng)創(chuàng)建了一個String實(shí)例,執(zhí)行代碼的時候,又new了一個String實(shí)例。當(dāng)然,你把兩者放在一起討論并不會有什么問題。

JVM優(yōu)化

以上討論都只是針對規(guī)范所定義的Java語言與Java虛擬機(jī)而言。概念上是如此,但實(shí)際的JVM實(shí)現(xiàn)可以做得更優(yōu)化,原問題中的代碼片段有可能在實(shí)際執(zhí)行的時候一個String實(shí)例也不會完整創(chuàng)建(沒有分配空間)。

不結(jié)合上下文代碼來看就直接說是“標(biāo)準(zhǔn)答案”就是耍流氓。

我們看下這段代碼:

Strings=newString("xyz")創(chuàng)建幾個實(shí)例

運(yùn)行這段代碼,會不斷的創(chuàng)建String對象吃內(nèi)存,然后頻繁的造成GC。

對于這個結(jié)論相信大家都沒有意見,我們加上 -XX:+PrintGC -XX:-DoEscapeAnalysis 打印日志,關(guān)閉逃逸分析(JDK8默認(rèn)開啟此優(yōu)化,我們先關(guān)閉)

Strings=newString("xyz")創(chuàng)建幾個實(shí)例

運(yùn)行一下看看:

Strings=newString("xyz")創(chuàng)建幾個實(shí)例

結(jié)果確實(shí)如我們所料,不斷的創(chuàng)建String對象吃內(nèi)存導(dǎo)致頻繁GC。

我們現(xiàn)在將 -XX:-DoEscapeAnalysis 改成 -XX:+DoEscapeAnalysis ,重新跑一下這段代碼:

Strings=newString("xyz")創(chuàng)建幾個實(shí)例

神奇的事情發(fā)生了,繼續(xù)跑下去也沒有再打出GC日志了。難道新創(chuàng)建String對象都不吃內(nèi)存了么?

實(shí)際情況是:經(jīng)過HotSpot VM的的優(yōu)化后,newString()方法不會新創(chuàng)建String實(shí)例了。這樣自然不吃內(nèi)存,也就不再觸發(fā)GC了。

現(xiàn)在再來看開篇的那個問題,不結(jié)合具體情況,還能簡單的說String s = new String("xyz");會創(chuàng)建兩個String實(shí)例嗎?

我只是舉了一個逃逸分析的例子,HotSpot VM還有很多像這樣的優(yōu)化,比如方法內(nèi)聯(lián)、標(biāo)量替換和無用代碼削除。

klass-oop

如果題干上沒有加上“Java”實(shí)例的定語,那JVM中的oop實(shí)例我們也不應(yīng)該忽略。

為了后面能更好的說清楚這一點(diǎn),需要補(bǔ)充一下klass-opp模型的知識。先做一個約定,全文只要涉及JVM具體實(shí)現(xiàn)的內(nèi)容都是基于Jdk8中HotSpot VM展開的。

HotSpot VM是基于C++實(shí)現(xiàn),而C++是一門面向?qū)ο蟮恼Z言,本身是具備面向?qū)ο蠡咎卣鞯?,所以Java中的對象表示,最簡單的做法是為每個Java類生成一個C++類與之對應(yīng)。但HotSpot VM并沒有這么做,而是設(shè)計(jì)了一套klass-oop模型。

klass ,它是Java類的元信息在JVM中的存在形式。一個Java類被JVM類加載器加載之后,就是以klass的形式存在于JVM之中。

Strings=newString("xyz")創(chuàng)建幾個實(shí)例

oop ,它是Java對象在JVM中的存在形式。每創(chuàng)建一個新的對象,在JVM內(nèi)部就會相應(yīng)地創(chuàng)建一個對應(yīng)類型的OOP對象。

其中instanceOopDesc表示非數(shù)組對象,arrayOopDesc表示數(shù)組對象;

而objArrayOopDesc表示引用類型數(shù)組對象,typeArrayOopDesc表示基本類型數(shù)組對象。

舉個例子:Java中String類的一個實(shí)例,在JVM中會有一個對應(yīng)的instanceOopDesc實(shí)例。

Strings=newString("xyz")創(chuàng)建幾個實(shí)例

字符串常量池

在Java體系中,有三種常量池:

  • class字節(jié)碼中的常量池:存在于硬盤上。主要存放兩大類常量:字面量、符號引用。
  • 運(yùn)行時常量池:方法區(qū)的一部分。我們常說的常量池,就是指這一塊區(qū)域:方法區(qū)中的運(yùn)行時常量池。
  • 字符串常量池:存在于堆區(qū)。這個常量池在JVM層面就是一個StringTable,只存儲對java.lang.String實(shí)例的引用,而不存儲String對象的內(nèi)容。一般我們說一個字符串進(jìn)入了字符串常量池其實(shí)是說在這個StringTable中保存了對它的引用,反之,如果說沒有在其中就是說StringTable中沒有對它的引用。

今天,我們要了解的是字符串常量池。

字符串常量池,即String Pool。在JVM中對應(yīng)的類是StringTable,底層實(shí)現(xiàn)是一個Hashtable。利用的是哈希思想。

Strings=newString("xyz")創(chuàng)建幾個實(shí)例

下面這段代碼,是往字符串常量池添加字符串方法。雖然是C++代碼,但我相信學(xué)過Java的人都能看懂,至少也能明白這段代碼干了什么事情。會通過String的內(nèi)容+長度生成的hash值定位下標(biāo)index,然后將Java的String類的實(shí)例對應(yīng)的instanceOopDesc封裝成HashtableEntry作為存儲結(jié)構(gòu)存儲到常量池。

Strings=newString("xyz")創(chuàng)建幾個實(shí)例

補(bǔ)充完字符串常量池的知識之后,我們再回到文章開頭的那一題:

String s = new String("xyz");創(chuàng)建了幾個實(shí)例?

我們畫一個內(nèi)存圖,圖中省略了兩個String對應(yīng)的instanceOopDesc實(shí)例。

Strings=newString("xyz")創(chuàng)建幾個實(shí)例

不難得出答案:

如果包括JVM中的C++實(shí)例的話,
有兩個Java的String實(shí)例,
兩個String實(shí)例對應(yīng)的instanceOopDesc實(shí)例,
還有一個char[]數(shù)組對應(yīng)的typeArrayOopDesc實(shí)例。
加一起一共是5個,也可以說2個String實(shí)例加上3個oop實(shí)例。

總結(jié)

String s = new String("xyz"); 創(chuàng)建了幾個實(shí)例?

通過以上的分析,我們會發(fā)現(xiàn),每在這道題目的題干上每加一個定語,這道題目就會有不同的答案。

是否考慮類加載過程,是否考慮JVM優(yōu)化,是否包括對應(yīng)的oop實(shí)例等等等等,每個點(diǎn)都值得聊一聊的。

下次有人問你,你不妨把這篇的文章分享給他。

來源:https://www.tuicool.com/articles/IruE3yy

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容