请别再拿“String s = new String(xyz);创建了多少个String实例”来面试了吧---转
http://www.iteye.com/topic/774673
羞愧呀,不知道多少人干過,我也干過,面壁去!
這帖是用來回復高級語言虛擬機圈子里的一個問題,一道Java筆試題的。?
本來因為見得太多已經吐槽無力,但這次實在忍不住了就又爆發了一把。寫得太長干脆單獨開了一帖。?
順帶廣告:對JVM感興趣的同學們同志們請多多支持高級語言虛擬機圈子??
以下是回復內容。文中的“樓主”是針對原問題帖而言。?
===============================================================?
樓主是看各種寶典了么……以后我面試人的時候就要專找寶典答案是錯的來問,方便篩人orz?
樓主要注意了:這題或類似的題雖然經常見,但使用這個描述方式實際上沒有任何意義:?
這個問題自身就沒有合理的答案,樓主所引用的“標準答案”自然也就不準確了:?
(好吧這個答案的吐槽點很多……大家慢慢來)?
這問題的毛病是什么呢?它并沒有定義“創建了”的意義。?
什么叫“創建了”?什么時候創建了什么??
而且這段Java代碼片段實際運行的時候真的會“創建兩個String實例”么??
如果這道是面試題,那么可以當面讓面試官澄清“創建了”的定義,然后再對應的回答。這種時候面試官多半會讓被面試者自己解釋,那就好辦了,好好曬給面試官看。?
如果是筆試題就沒有提問要求澄清的機會了。不過會出這種題目的地方水平多半也不怎么樣。說不定出題的人就是從各種寶典上把題抄來的,那就按照寶典把那不大對勁的答案寫上去就能混過去了?
===============================================================?
先換成另一個問題來問:?
一種合理的解答是:?
這是根據Java語言規范相關規定可以給出的合理答案。考慮到Java語言規范中明確指出了:?
也就是規定了Java語言一般是編譯為Java虛擬機規范所定義的Class文件,但并沒有規定“一定”(must),留有不使用JVM來實現Java語言的余地。?
考慮上Java虛擬機規范,確實在這段代碼里涉及的常量種類為CONSTANT_String_info的字符串常量也只有"xyz"一個。CONSTANT_String_info是用來表示Java語言中String類型的常量表達式的值(包括字符串字面量)用的常量種類,只在這個層面上考慮的話,這個解答也沒問題。?
所以這種解答可以認為是合理的。?
值得注意的是問題中“在運行時”既包括類加載階段,也包括這個代碼片段自身執行的時候。下文會再討論這個細節與樓主原本給出的問題的關系。?
碰到這種問題首先應該想到去查閱相關的規范,這里具體是Java語言規范與Java虛擬機規范,以及一些相關API的JavaDoc。很多人喜歡把“按道理說”當作口頭禪,規范就是用來定義各種“道理”的——“為什么XXX是YYY的意思?”“因為規范里是這樣定義的!”——無敵了。?
在Java虛擬機規范中相關的定義有下面一些:?
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看作參考實現(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.
===============================================================?
再換一個問題來問:?
答案也很簡單:?
把問題換成下面這個版本,答案也一樣:?
Java里變量就是變量,引用類型的變量只是對某個對象實例或者null的引用,不是實例本身。聲明變量的個數跟創建實例的個數沒有必然關系,像是說:?
這段代碼會涉及3個String類型的變量,?
1、s1,指向下面String實例的1?
2、s2,指向與s1相同?
3、s3,值為null,不指向任何實例?
以及3個String實例,?
1、"a"字面量對應的駐留的字符串常量的String實例?
2、""字面量對應的駐留的字符串常量的String實例?
(String.concat()是個有趣的方法,當發現傳入的參數是空字符串時會返回this,所以這里不會額外創建新的String實例)?
3、通過new String(String)創建的新String實例;沒有任何變量指向它。?
===============================================================?
回到樓主開頭引用的問題與“標準答案”?
答案:兩個(一個是“xyz”,一個是指向“xyz”的引用對象s)
用歸謬法論證。假定問題問的是“在執行這段代碼片段時創建了幾個String實例”。如果“標準答案”是正確的,那么下面的代碼片段在執行時就應該創建4個String實例了:?
馬上就會有人跳出來說上下兩個"xyz"字面量都是引用了同一個String對象,所以不應該是創建了4個對象。?
那么應該是多少個??
運行時的類加載過程與實際執行某個代碼片段,兩者必須分開討論才有那么點意義。?
為了執行問題中的代碼片段,其所在的類必然要先被加載,而且同一個類最多只會被加載一次(要注意對JVM來說“同一個類”并不是類的全限定名相同就足夠了,而是<類全限定名, 定義類加載器>一對都相同才行)。?
根據上文引用的規范的內容,符合規范的JVM實現應該在類加載的過程中創建并駐留一個String實例作為常量來對應"xyz"字面量;具體是在類加載的resolve階段進行的。這個常量是全局共享的,只在先前尚未有內容相同的字符串駐留過的前提下才需要創建新的String實例。?
等到真正執行原問題中的代碼片段時,JVM需要執行的字節碼類似這樣:?
這之中出現過多少次new java/lang/String就是創建了多少個String對象。也就是說原問題中的代碼在每執行一次只會新創建一個String實例。?
這里,ldc指令只是把先前在類加載過程中已經創建好的一個String對象("xyz")的一個引用壓到操作數棧頂而已,并不新創建String對象。?
所以剛才用于歸謬的代碼片段:?
每執行一次只會新創建2個String實例。?
---------------------------------------------------------------?
為了避免一些同學犯糊涂,再強調一次:?
在Java語言里,“new”表達式是負責創建實例的,其中會調用構造器去對實例做初始化;構造器自身的返回值類型是void,并不是“構造器返回了新創建的對象的引用”,而是new表達式的值是新創建的對象的引用。?
對應的,在JVM里,“new”字節碼指令只負責把實例創建出來(包括分配空間、設定類型、所有字段設置默認值等工作),并且把指向新創建對象的引用壓到操作數棧頂。此時該引用還不能直接使用,處于未初始化狀態(uninitialized);如果某方法a含有代碼試圖通過未初始化狀態的引用來調用任何實例方法,那么方法a會通不過JVM的字節碼校驗,從而被JVM拒絕執行。?
能對未初始化狀態的引用做的唯一一種事情就是通過它調用實例構造器,在Class文件層面表現為特殊初始化方法“<init>”。實際調用的指令是invokespecial,而在實際調用前要把需要的參數按順序壓到操作數棧上。在上面的字節碼例子中,壓參數的指令包括dup和ldc兩條,分別把隱藏參數(新創建的實例的引用,對于實例構造器來說就是“this”)與顯式聲明的第一個實際參數("xyz"常量的引用)壓到操作數棧上。?
在構造器返回之后,新創建的實例的引用就可以正常使用了。?
關于構造器的討論,可以參考我之前的一帖,實例構造器是不是靜態方法??
===============================================================?
以上討論都只是針對規范所定義的Java語言與Java虛擬機而言。概念上是如此,但實際的JVM實現可以做得更優化,原問題中的代碼片段有可能在實際執行的時候一個String實例也不會完整創建(沒有分配空間)。?
例如說,在x86、Windows Vista SP2、Sun JDK 6 update 14的fastdebug版上跑下面的測試代碼:?
照常用javac用默認參數編譯,然后先用server模式的默認配置來跑,順帶打出GC和JIT編譯日志來看?
看到的日志的開頭一段如下:?
上面的日志中,后面的方法名的行是JIT編譯的日志,而以[GC開頭的是minor GC的日志。?
程序一直跑,GC的日志還會不斷的打出來。這是理所當然的對吧?HotSpot的堆就那么大,而測試代碼在不斷新創建String對象,肯定得不斷觸發GC的。?
用不同的VM啟動參數來跑的話,?
還是同樣的Java測試程序,同樣的Sun JDK 6 update 14,但打開了逃逸分析和空間分配消除功能,再運行,看到的全部日志如下:?
繼續跑下去也沒有再打出GC日志了。難道新創建String對象都不吃內存了么??
實際情況是:經過HotSpot的server模式編譯器的優化后,FooA、FooB、FooC、FooD四個版本的foo()實現都不新創建String實例了。這樣自然不吃內存,也就不再觸發GC了。?
經過的分析和優化籠統說有方法內聯(method inlining)、逃逸分析(escape analysis)、標量替換(scalar replacement)、無用代碼削除(dead-code elimination)之類。?
FooA.foo()最短,就以它舉例來大致演示一下優化的過程。?
它其實就是創建并初始化了一個String對象而已。調用的構造器的源碼是:?
因為參數是"xyz",可以確定在我們的測試代碼里不會走到構造器的if分支里,下面為了演示方便就省略掉那部分代碼(實際代碼還是存在的,只是沒執行而已)?
那么把構造器內聯到FooA.foo()里,?
然后經過逃逸分析與標量替換,?
注意,到這里就已經把新創建String在堆上分配空間的代碼全部削除了,原本新建的String實例的字段變成了FooA.foo()的局部變量。?
最后再經過無用代碼削除,把sOffset、sCount和sValue這三個沒被讀過的局部變量給削除掉,?
這就跟FooA.foo()被優化編譯后實際執行的代碼基本一致了。?
實際執行的x86代碼如下:?
看,確實沒有新創建String對象了。?
另外三個版本的foo()實現也是類似,HotSpot成功的把無用的new String("xyz")全部干掉了。?
關于逃逸分析的例子,可以參考我以前一篇帖,HotSpot 17.0-b12的逃逸分析/標量替換的一個演示?
再回頭看看樓主的原問題,問題中的代碼片段執行的時候(對應到FooA.foo()被調用的時候)一個String對象也沒有新建。于是那“標準答案”在現實中的指導意義又有多少呢??
===============================================================?
另外,樓主還提到了PermGen:?
這里也是需要強調一點:永生代(“Perm Gen”)只是Sun JDK的一個實現細節而已,Java語言規范和Java虛擬機規范都沒有規定必須有“Permanent Generation”這么一塊空間,甚至沒規定要用什么GC算法——不用分代式GC算法哪兒來的“永生代”??
HotSpot的PermGen是用來實現Java虛擬機規范中的“方法區”(method area)的。如果使用“方法區”這個術語,在討論概念中的JVM時就安全得多——大家都必須實現出這個表象。?
當然如何實現又是另一回事了。Oracle JRockit沒有PermGen,IBM J9也沒有,事實上有這么一塊空間特別管理的反而是少數吧orz?
===============================================================?
費那么多口舌,最后點題:請別再拿“String s = new String("xyz");創建了多少個String實例”來面試了吧,既沒意義又不漲面子。?
困,睡覺去……
轉載于:https://www.cnblogs.com/davidwang456/p/3464425.html
總結
以上是生活随笔為你收集整理的请别再拿“String s = new String(xyz);创建了多少个String实例”来面试了吧---转的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux 的启动流程--转
- 下一篇: jmap查看内存使用情况与生成heapd