请别再拿“String s = new String(xyz);创建了多少个String实例”来面试了吧---转
http://www.iteye.com/topic/774673
羞愧呀,不知道多少人干過,我也干過,面壁去!
這帖是用來回復(fù)高級(jí)語言虛擬機(jī)圈子里的一個(gè)問題,一道Java筆試題的。?
本來因?yàn)橐姷锰嘁呀?jīng)吐槽無力,但這次實(shí)在忍不住了就又爆發(fā)了一把。寫得太長(zhǎng)干脆單獨(dú)開了一帖。?
順帶廣告:對(duì)JVM感興趣的同學(xué)們同志們請(qǐng)多多支持高級(jí)語言虛擬機(jī)圈子??
以下是回復(fù)內(nèi)容。文中的“樓主”是針對(duì)原問題帖而言。?
===============================================================?
樓主是看各種寶典了么……以后我面試人的時(shí)候就要專找寶典答案是錯(cuò)的來問,方便篩人orz?
樓主要注意了:這題或類似的題雖然經(jīng)常見,但使用這個(gè)描述方式實(shí)際上沒有任何意義:?
這個(gè)問題自身就沒有合理的答案,樓主所引用的“標(biāo)準(zhǔn)答案”自然也就不準(zhǔn)確了:?
(好吧這個(gè)答案的吐槽點(diǎn)很多……大家慢慢來)?
這問題的毛病是什么呢?它并沒有定義“創(chuàng)建了”的意義。?
什么叫“創(chuàng)建了”?什么時(shí)候創(chuàng)建了什么??
而且這段Java代碼片段實(shí)際運(yùn)行的時(shí)候真的會(huì)“創(chuàng)建兩個(gè)String實(shí)例”么??
如果這道是面試題,那么可以當(dāng)面讓面試官澄清“創(chuàng)建了”的定義,然后再對(duì)應(yīng)的回答。這種時(shí)候面試官多半會(huì)讓被面試者自己解釋,那就好辦了,好好曬給面試官看。?
如果是筆試題就沒有提問要求澄清的機(jī)會(huì)了。不過會(huì)出這種題目的地方水平多半也不怎么樣。說不定出題的人就是從各種寶典上把題抄來的,那就按照寶典把那不大對(duì)勁的答案寫上去就能混過去了?
===============================================================?
先換成另一個(gè)問題來問:?
一種合理的解答是:?
這是根據(jù)Java語言規(guī)范相關(guān)規(guī)定可以給出的合理答案。考慮到Java語言規(guī)范中明確指出了:?
也就是規(guī)定了Java語言一般是編譯為Java虛擬機(jī)規(guī)范所定義的Class文件,但并沒有規(guī)定“一定”(must),留有不使用JVM來實(shí)現(xiàn)Java語言的余地。?
考慮上Java虛擬機(jī)規(guī)范,確實(shí)在這段代碼里涉及的常量種類為CONSTANT_String_info的字符串常量也只有"xyz"一個(gè)。CONSTANT_String_info是用來表示Java語言中String類型的常量表達(dá)式的值(包括字符串字面量)用的常量種類,只在這個(gè)層面上考慮的話,這個(gè)解答也沒問題。?
所以這種解答可以認(rèn)為是合理的。?
值得注意的是問題中“在運(yùn)行時(shí)”既包括類加載階段,也包括這個(gè)代碼片段自身執(zhí)行的時(shí)候。下文會(huì)再討論這個(gè)細(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)的定義有下面一些:?
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:?
- Evaluation of a class instance creation expression creates a new instance of the class whose name appears in the expression.
- Invocation of the newInstance method of class Class creates a new instance of the class represented by the Class object for which the method was invoked.
A new class instance may be implicitly created in the following situations:?
- Loading of a class or interface that contains a String literal may create a new String object?(§2.4.8)?to represent that literal. This may not occur if the a String object has already been created to represent a previous occurrence of that literal, or if the String.intern method has been invoked on a String object representing the same string as the literal.
- Execution of a string concatenation operator that is not part of a constant expression sometimes creates a new String object to represent the result. String concatenation operators may also create temporary wrapper objects for a value of a primitive type?(§2.4.1).
Each of these situations identifies a particular constructor to be called with specified arguments (possibly none) as part of the class instance creation process.?
5.1 The Runtime Constant Pool?
...?
● A string literal?(§2.3)?is derived from a CONSTANT_String_info structure?(§4.4.3)?in the binary representation of a class or interface. The CONSTANT_String_info structure gives the sequence of Unicode characters constituting the string literal.?
● The Java programming language requires that identical string literals (that is, literals that contain the same sequence of characters) must refer to the same instance of class String. In addition, if the method String.intern is called on any string, the result is a reference to the same class instance that would be returned if that string appeared as a literal. Thus,?
Java代碼??
must have the value true.?
● To derive a string literal, the Java virtual machine examines the sequence of characters given by the CONSTANT_String_info structure.?
? ○ If the method String.intern has previously been called on an instance of class String containing a sequence of Unicode characters identical to that given by the CONSTANT_String_info structure, then the result of string literal derivation is a reference to that same instance of class String.?
? ○ Otherwise, a new instance of class String is created containing the sequence of Unicode characters given by the CONSTANT_String_info structure; that class instance is the result of string literal derivation. Finally, the intern method of the new String instance is invoked.?
...?
The remaining structures in the constant_pool table of the binary representation of a class or interface, the CONSTANT_NameAndType_info?(§4.4.6)?and CONSTANT_Utf8_info?(§4.4.7)?structures are only used indirectly when deriving symbolic references to classes, interfaces, methods, and fields, and when deriving string literals.
把Sun的JDK看作參考實(shí)現(xiàn)(reference implementation, RI),其中String.intern()的JavaDoc為:?
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.
===============================================================?
再換一個(gè)問題來問:?
答案也很簡(jiǎn)單:?
把問題換成下面這個(gè)版本,答案也一樣:?
Java里變量就是變量,引用類型的變量只是對(duì)某個(gè)對(duì)象實(shí)例或者null的引用,不是實(shí)例本身。聲明變量的個(gè)數(shù)跟創(chuàng)建實(shí)例的個(gè)數(shù)沒有必然關(guān)系,像是說:?
這段代碼會(huì)涉及3個(gè)String類型的變量,?
1、s1,指向下面String實(shí)例的1?
2、s2,指向與s1相同?
3、s3,值為null,不指向任何實(shí)例?
以及3個(gè)String實(shí)例,?
1、"a"字面量對(duì)應(yīng)的駐留的字符串常量的String實(shí)例?
2、""字面量對(duì)應(yīng)的駐留的字符串常量的String實(shí)例?
(String.concat()是個(gè)有趣的方法,當(dāng)發(fā)現(xiàn)傳入的參數(shù)是空字符串時(shí)會(huì)返回this,所以這里不會(huì)額外創(chuàng)建新的String實(shí)例)?
3、通過new String(String)創(chuàng)建的新String實(shí)例;沒有任何變量指向它。?
===============================================================?
回到樓主開頭引用的問題與“標(biāo)準(zhǔn)答案”?
答案:兩個(gè)(一個(gè)是“xyz”,一個(gè)是指向“xyz”的引用對(duì)象s)
用歸謬法論證。假定問題問的是“在執(zhí)行這段代碼片段時(shí)創(chuàng)建了幾個(gè)String實(shí)例”。如果“標(biāo)準(zhǔn)答案”是正確的,那么下面的代碼片段在執(zhí)行時(shí)就應(yīng)該創(chuàng)建4個(gè)String實(shí)例了:?
馬上就會(huì)有人跳出來說上下兩個(gè)"xyz"字面量都是引用了同一個(gè)String對(duì)象,所以不應(yīng)該是創(chuàng)建了4個(gè)對(duì)象。?
那么應(yīng)該是多少個(gè)??
運(yùn)行時(shí)的類加載過程與實(shí)際執(zhí)行某個(gè)代碼片段,兩者必須分開討論才有那么點(diǎn)意義。?
為了執(zhí)行問題中的代碼片段,其所在的類必然要先被加載,而且同一個(gè)類最多只會(huì)被加載一次(要注意對(duì)JVM來說“同一個(gè)類”并不是類的全限定名相同就足夠了,而是<類全限定名, 定義類加載器>一對(duì)都相同才行)。?
根據(jù)上文引用的規(guī)范的內(nèi)容,符合規(guī)范的JVM實(shí)現(xiàn)應(yīng)該在類加載的過程中創(chuàng)建并駐留一個(gè)String實(shí)例作為常量來對(duì)應(yīng)"xyz"字面量;具體是在類加載的resolve階段進(jìn)行的。這個(gè)常量是全局共享的,只在先前尚未有內(nèi)容相同的字符串駐留過的前提下才需要?jiǎng)?chuàng)建新的String實(shí)例。?
等到真正執(zhí)行原問題中的代碼片段時(shí),JVM需要執(zhí)行的字節(jié)碼類似這樣:?
這之中出現(xiàn)過多少次new java/lang/String就是創(chuàng)建了多少個(gè)String對(duì)象。也就是說原問題中的代碼在每執(zhí)行一次只會(huì)新創(chuàng)建一個(gè)String實(shí)例。?
這里,ldc指令只是把先前在類加載過程中已經(jīng)創(chuàng)建好的一個(gè)String對(duì)象("xyz")的一個(gè)引用壓到操作數(shù)棧頂而已,并不新創(chuàng)建String對(duì)象。?
所以剛才用于歸謬的代碼片段:?
每執(zhí)行一次只會(huì)新創(chuàng)建2個(gè)String實(shí)例。?
---------------------------------------------------------------?
為了避免一些同學(xué)犯糊涂,再強(qiáng)調(diào)一次:?
在Java語言里,“new”表達(dá)式是負(fù)責(zé)創(chuàng)建實(shí)例的,其中會(huì)調(diào)用構(gòu)造器去對(duì)實(shí)例做初始化;構(gòu)造器自身的返回值類型是void,并不是“構(gòu)造器返回了新創(chuàng)建的對(duì)象的引用”,而是new表達(dá)式的值是新創(chuàng)建的對(duì)象的引用。?
對(duì)應(yīng)的,在JVM里,“new”字節(jié)碼指令只負(fù)責(zé)把實(shí)例創(chuàng)建出來(包括分配空間、設(shè)定類型、所有字段設(shè)置默認(rèn)值等工作),并且把指向新創(chuàng)建對(duì)象的引用壓到操作數(shù)棧頂。此時(shí)該引用還不能直接使用,處于未初始化狀態(tài)(uninitialized);如果某方法a含有代碼試圖通過未初始化狀態(tài)的引用來調(diào)用任何實(shí)例方法,那么方法a會(huì)通不過JVM的字節(jié)碼校驗(yàn),從而被JVM拒絕執(zhí)行。?
能對(duì)未初始化狀態(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í)例的引用,對(duì)于實(shí)例構(gòu)造器來說就是“this”)與顯式聲明的第一個(gè)實(shí)際參數(shù)("xyz"常量的引用)壓到操作數(shù)棧上。?
在構(gòu)造器返回之后,新創(chuàng)建的實(shí)例的引用就可以正常使用了。?
關(guān)于構(gòu)造器的討論,可以參考我之前的一帖,實(shí)例構(gòu)造器是不是靜態(tài)方法??
===============================================================?
以上討論都只是針對(duì)規(guī)范所定義的Java語言與Java虛擬機(jī)而言。概念上是如此,但實(shí)際的JVM實(shí)現(xiàn)可以做得更優(yōu)化,原問題中的代碼片段有可能在實(shí)際執(zhí)行的時(shí)候一個(gè)String實(shí)例也不會(huì)完整創(chuàng)建(沒有分配空間)。?
例如說,在x86、Windows Vista SP2、Sun JDK 6 update 14的fastdebug版上跑下面的測(cè)試代碼:?
照常用javac用默認(rèn)參數(shù)編譯,然后先用server模式的默認(rèn)配置來跑,順帶打出GC和JIT編譯日志來看?
看到的日志的開頭一段如下:?
上面的日志中,后面的方法名的行是JIT編譯的日志,而以[GC開頭的是minor GC的日志。?
程序一直跑,GC的日志還會(huì)不斷的打出來。這是理所當(dāng)然的對(duì)吧?HotSpot的堆就那么大,而測(cè)試代碼在不斷新創(chuàng)建String對(duì)象,肯定得不斷觸發(fā)GC的。?
用不同的VM啟動(dòng)參數(shù)來跑的話,?
還是同樣的Java測(cè)試程序,同樣的Sun JDK 6 update 14,但打開了逃逸分析和空間分配消除功能,再運(yùn)行,看到的全部日志如下:?
繼續(xù)跑下去也沒有再打出GC日志了。難道新創(chuàng)建String對(duì)象都不吃內(nèi)存了么??
實(shí)際情況是:經(jīng)過HotSpot的server模式編譯器的優(yōu)化后,FooA、FooB、FooC、FooD四個(gè)版本的foo()實(shí)現(xiàn)都不新創(chuàng)建String實(shí)例了。這樣自然不吃內(nèi)存,也就不再觸發(fā)GC了。?
經(jīng)過的分析和優(yōu)化籠統(tǒng)說有方法內(nèi)聯(lián)(method inlining)、逃逸分析(escape analysis)、標(biāo)量替換(scalar replacement)、無用代碼削除(dead-code elimination)之類。?
FooA.foo()最短,就以它舉例來大致演示一下優(yōu)化的過程。?
它其實(shí)就是創(chuàng)建并初始化了一個(gè)String對(duì)象而已。調(diào)用的構(gòu)造器的源碼是:?
因?yàn)閰?shù)是"xyz",可以確定在我們的測(cè)試代碼里不會(huì)走到構(gòu)造器的if分支里,下面為了演示方便就省略掉那部分代碼(實(shí)際代碼還是存在的,只是沒執(zhí)行而已)?
那么把構(gòu)造器內(nèi)聯(lián)到FooA.foo()里,?
然后經(jīng)過逃逸分析與標(biāo)量替換,?
注意,到這里就已經(jīng)把新創(chuàng)建String在堆上分配空間的代碼全部削除了,原本新建的String實(shí)例的字段變成了FooA.foo()的局部變量。?
最后再經(jīng)過無用代碼削除,把sOffset、sCount和sValue這三個(gè)沒被讀過的局部變量給削除掉,?
這就跟FooA.foo()被優(yōu)化編譯后實(shí)際執(zhí)行的代碼基本一致了。?
實(shí)際執(zhí)行的x86代碼如下:?
看,確實(shí)沒有新創(chuàng)建String對(duì)象了。?
另外三個(gè)版本的foo()實(shí)現(xiàn)也是類似,HotSpot成功的把無用的new String("xyz")全部干掉了。?
關(guān)于逃逸分析的例子,可以參考我以前一篇帖,HotSpot 17.0-b12的逃逸分析/標(biāo)量替換的一個(gè)演示?
再回頭看看樓主的原問題,問題中的代碼片段執(zhí)行的時(shí)候(對(duì)應(yīng)到FooA.foo()被調(diào)用的時(shí)候)一個(gè)String對(duì)象也沒有新建。于是那“標(biāo)準(zhǔn)答案”在現(xiàn)實(shí)中的指導(dǎo)意義又有多少呢??
===============================================================?
另外,樓主還提到了PermGen:?
這里也是需要強(qiáng)調(diào)一點(diǎn):永生代(“Perm Gen”)只是Sun JDK的一個(gè)實(shí)現(xiàn)細(xì)節(jié)而已,Java語言規(guī)范和Java虛擬機(jī)規(guī)范都沒有規(guī)定必須有“Permanent Generation”這么一塊空間,甚至沒規(guī)定要用什么GC算法——不用分代式GC算法哪兒來的“永生代”??
HotSpot的PermGen是用來實(shí)現(xiàn)Java虛擬機(jī)規(guī)范中的“方法區(qū)”(method area)的。如果使用“方法區(qū)”這個(gè)術(shù)語,在討論概念中的JVM時(shí)就安全得多——大家都必須實(shí)現(xiàn)出這個(gè)表象。?
當(dāng)然如何實(shí)現(xiàn)又是另一回事了。Oracle JRockit沒有PermGen,IBM J9也沒有,事實(shí)上有這么一塊空間特別管理的反而是少數(shù)吧orz?
===============================================================?
費(fèi)那么多口舌,最后點(diǎn)題:請(qǐng)別再拿“String s = new String("xyz");創(chuàng)建了多少個(gè)String實(shí)例”來面試了吧,既沒意義又不漲面子。?
困,睡覺去……
轉(zhuǎn)載于:https://www.cnblogs.com/davidwang456/p/3464425.html
總結(jié)
以上是生活随笔為你收集整理的请别再拿“String s = new String(xyz);创建了多少个String实例”来面试了吧---转的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux 的启动流程--转
- 下一篇: jmap查看内存使用情况与生成heapd