這帖是用來回復(fù)高級語言虛擬機(jī)圈子里的一個問題,一道java筆試題的。本來因?yàn)橐姷锰嘁呀?jīng)吐槽無力,但這次實(shí)在忍不住了就又爆發(fā)了一把。寫得太長干脆單獨(dú)開了一帖。順帶廣告:對JVM感興趣的同學(xué)們同志們請多多支持高級語言虛擬機(jī)圈子
以下是回復(fù)內(nèi)容。文中的“樓主”是針對原問題帖而言。===============================================================樓主是看各種寶典了么……以后我面試人的時候就要專找寶典答案是錯的來問,方便篩人orz樓主要注意了:這題或類似的題雖然經(jīng)常見,但使用這個描述方式實(shí)際上沒有任何意義:

這個問題自身就沒有合理的答案,樓主所引用的“標(biāo)準(zhǔn)答案”自然也就不準(zhǔn)確了:
引用答案:兩個(一個是“xyz”,一個是指向“xyz”的引用對象s)(好吧這個答案的吐槽點(diǎn)很多……大家慢慢來)這問題的毛病是什么呢?它并沒有定義“創(chuàng)建了”的意義。什么叫“創(chuàng)建了”?什么時候創(chuàng)建了什么?而且這段Java代碼片段實(shí)際運(yùn)行的時候真的會“創(chuàng)建兩個String實(shí)例”么?如果這道是面試題,那么可以當(dāng)面讓面試官澄清“創(chuàng)建了”的定義,然后再對應(yīng)的回答。這種時候面試官多半會讓被面試者自己解釋,那就好辦了,好好曬給面試官看。如果是筆試題就沒有提問要求澄清的機(jī)會了。不過會出這種題目的地方水平多半也不怎么樣。說不定出題的人就是從各種寶典上把題抄來的,那就按照寶典把那不大對勁的答案寫上去就能混過去了
===============================================================先換成另一個問題來問:

一種合理的解答是:
引用答案:兩個,一個是字符串字面量"xyz"所對應(yīng)的、駐留(intern)在一個全局共享的字符串常量池中的實(shí)例,另一個是通過new String(String)創(chuàng)建并初始化的、內(nèi)容與"xyz"相同的實(shí)例這是根據(jù)Java語言規(guī)范相關(guān)規(guī)定可以給出的合理答案。考慮到Java語言規(guī)范中明確指出了:
The Java Language Specification, Third Edition 寫道The Java PRogramming language is normally compiled to the bytecoded instruction set and binary format defined inThe Java Virtual Machine Specification, Second Edition(Addison-Wesley, 1999).也就是規(guī)定了Java語言一般是編譯為Java虛擬機(jī)規(guī)范所定義的Class文件,但并沒有規(guī)定“一定”(must),留有不使用JVM來實(shí)現(xiàn)Java語言的余地。考慮上Java虛擬機(jī)規(guī)范,確實(shí)在這段代碼里涉及的常量種類為CONSTANT_String_info的字符串常量也只有"xyz"一個。CONSTANT_String_info是用來表示Java語言中String類型的常量表達(dá)式的值(包括字符串字面量)用的常量種類,只在這個層面上考慮的話,這個解答也沒問題。所以這種解答可以認(rèn)為是合理的。值得注意的是問題中“在運(yùn)行時”既包括類加載階段,也包括這個代碼片段自身執(zhí)行的時候。下文會再討論這個細(xì)節(jié)與樓主原本給出的問題的關(guān)系。碰到這種問題首先應(yīng)該想到去查閱相關(guān)的規(guī)范,這里具體是Java語言規(guī)范與Java虛擬機(jī)規(guī)范,以及一些相關(guān)API的JavaDoc。很多人喜歡把“按道理說”當(dāng)作口頭禪,規(guī)范就是用來定義各種“道理”的——“為什么XXX是YYY的意思?”“因?yàn)橐?guī)范里是這樣定義的!”——無敵了。在Java虛擬機(jī)規(guī)范中相關(guān)的定義有下面一些:
The Java Virtual Machine Specification, Second Edition 寫道2.3 Literals A literal is the source code representation of a value of a primitive type(§2.4.1), the String type(§2.4.8), or the null type(§2.4).String literals and, more generally, strings that are the values of constant expressions are "interned" so as to share unique instances, using the method String.intern. The null type has one value, the null reference, denoted by the literal null. The boolean type has two values, denoted by the literals true and false.2.4.8 The Class String Instances of class String represent sequences of Unicode characters(§2.1). A String object has a constant, unchanging value.String literals(§2.3)are references to instances of class String.2.17.6 Creation of New Class Instances A new class instance is explicitly created when one of the following situations occurs:
把Sun的JDK看作參考實(shí)現(xiàn)(reference implementation, RI),其中String.intern()的JavaDoc為:
JavaDoc 寫道public String intern() Returns a canonical representation for the string object. A pool of strings, initially empty, is maintained privately by the class String. When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned. It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true. All literal strings and string-valued constant expressions are interned. String literals are defined in §3.10.5 of the Java Language Specification Returns: a string that has the same contents as this string, but is guaranteed to be from a pool of unique strings.===============================================================再換一個問題來問:
引用問題:Java代碼
答案也很簡單:
引用答案:一個,就是String s。把問題換成下面這個版本,答案也一樣:
引用問題:Java代碼
Java里變量就是變量,引用類型的變量只是對某個對象實(shí)例或者null的引用,不是實(shí)例本身。聲明變量的個數(shù)跟創(chuàng)建實(shí)例的個數(shù)沒有必然關(guān)系,像是說:
Java代碼
這段代碼會涉及3個String類型的變量,1、s1,指向下面String實(shí)例的12、s2,指向與s1相同3、s3,值為null,不指向任何實(shí)例以及3個String實(shí)例,1、"a"字面量對應(yīng)的駐留的字符串常量的String實(shí)例2、""字面量對應(yīng)的駐留的字符串常量的String實(shí)例(String.concat()是個有趣的方法,當(dāng)發(fā)現(xiàn)傳入的參數(shù)是空字符串時會返回this,所以這里不會額外創(chuàng)建新的String實(shí)例)3、通過new String(String)創(chuàng)建的新String實(shí)例;沒有任何變量指向它。===============================================================回到樓主開頭引用的問題與“標(biāo)準(zhǔn)答案”
引用問題:Java代碼
用歸謬法論證。假定問題問的是“在執(zhí)行這段代碼片段時創(chuàng)建了幾個String實(shí)例”。如果“標(biāo)準(zhǔn)答案”是正確的,那么下面的代碼片段在執(zhí)行時就應(yīng)該創(chuàng)建4個String實(shí)例了:
Java代碼
馬上就會有人跳出來說上下兩個"xyz"字面量都是引用了同一個String對象,所以不應(yīng)該是創(chuàng)建了4個對象。那么應(yīng)該是多少個?運(yùn)行時的類加載過程與實(shí)際執(zhí)行某個代碼片段,兩者必須分開討論才有那么點(diǎn)意義。為了執(zhí)行問題中的代碼片段,其所在的類必然要先被加載,而且同一個類最多只會被加載一次(要注意對JVM來說“同一個類”并不是類的全限定名相同就足夠了,而是<類全限定名, 定義類加載器>一對都相同才行)。根據(jù)上文引用的規(guī)范的內(nèi)容,符合規(guī)范的JVM實(shí)現(xiàn)應(yīng)該在類加載的過程中創(chuàng)建并駐留一個String實(shí)例作為常量來對應(yīng)"xyz"字面量;具體是在類加載的resolve階段進(jìn)行的。這個常量是全局共享的,只在先前尚未有內(nèi)容相同的字符串駐留過的前提下才需要創(chuàng)建新的String實(shí)例。等到真正執(zhí)行原問題中的代碼片段時,JVM需要執(zhí)行的字節(jié)碼類似這樣:
Java bytecode代碼
這之中出現(xiàn)過多少次new java/lang/String就是創(chuàng)建了多少個String對象。也就是說原問題中的代碼在每執(zhí)行一次只會新創(chuàng)建一個String實(shí)例。這里,ldc指令只是把先前在類加載過程中已經(jīng)創(chuàng)建好的一個String對象("xyz")的一個引用壓到操作數(shù)棧頂而已,并不新創(chuàng)建String對象。所以剛才用于歸謬的代碼片段:
Java代碼
每執(zhí)行一次只會新創(chuàng)建2個String實(shí)例。---------------------------------------------------------------為了避免一些同學(xué)犯糊涂,再強(qiáng)調(diào)一次:在Java語言里,“new”表達(dá)式是負(fù)責(zé)創(chuàng)建實(shí)例的,其中會調(diào)用構(gòu)造器去對實(shí)例做初始化;構(gòu)造器自身的返回值類型是void,并不是“構(gòu)造器返回了新創(chuàng)建的對象的引用”,而是new表達(dá)式的值是新創(chuàng)建的對象的引用。對應(yīng)的,在JVM里,“new”字節(jié)碼指令只負(fù)責(zé)把實(shí)例創(chuàng)建出來(包括分配空間、設(shè)定類型、所有字段設(shè)置默認(rèn)值等工作),并且把指向新創(chuàng)建對象的引用壓到操作數(shù)棧頂。此時該引用還不能直接使用,處于未初始化狀態(tài)(uninitialized);如果某方法a含有代碼試圖通過未初始化狀態(tài)的引用來調(diào)用任何實(shí)例方法,那么方法a會通不過JVM的字節(jié)碼校驗(yàn),從而被JVM拒絕執(zhí)行。能對未初始化狀態(tài)的引用做的唯一一種事情就是通過它調(diào)用實(shí)例構(gòu)造器,在Class文件層面表現(xiàn)為特殊初始化方法“<init>”。實(shí)際調(diào)用的指令是invokespecial,而在實(shí)際調(diào)用前要把需要的參數(shù)按順序壓到操作數(shù)棧上。在上面的字節(jié)碼例子中,壓參數(shù)的指令包括dup和ldc兩條,分別把隱藏參數(shù)(新創(chuàng)建的實(shí)例的引用,對于實(shí)例構(gòu)造器來說就是“this”)與顯式聲明的第一個實(shí)際參數(shù)("xyz"常量的引用)壓到操作數(shù)棧上。在構(gòu)造器返回之后,新創(chuàng)建的實(shí)例的引用就可以正常使用了。關(guān)于構(gòu)造器的討論,可以參考我之前的一帖,實(shí)例構(gòu)造器是不是靜態(tài)方法?===============================================================以上討論都只是針對規(guī)范所定義的Java語言與Java虛擬機(jī)而言。概念上是如此,但實(shí)際的JVM實(shí)現(xiàn)可以做得更優(yōu)化,原問題中的代碼片段有可能在實(shí)際執(zhí)行的時候一個String實(shí)例也不會完整創(chuàng)建(沒有分配空間)。例如說,在x86、Windows Vista SP2、Sun JDK 6 update 14的fastdebug版上跑下面的測試代碼:
Java代碼
照常用javac用默認(rèn)參數(shù)編譯,然后先用server模式的默認(rèn)配置來跑,順帶打出GC和JIT編譯日志來看
Command prompt代碼
看到的日志的開頭一段如下:
Hotspot log代碼
新聞熱點(diǎn)
疑難解答
圖片精選