首頁>技術>

從面試題說起

String s = new String("xyz"); 建立了幾個例項?

這是一道很經典的面試題,在一本所謂的Java寶典上,我看到的“標準答案”是這樣的:

兩個,一個堆區的“xyz”,一個棧區指向“xyz”的s。

這個所謂的“標準答案”槽點太多,後面我們慢慢分析。

雖然答案很離譜,但是我覺得這個問題本身也不具有什麼意義,因為問題沒有既定義“建立”的具體含義,又沒有指定“建立”的時間,是執行時嗎?包不包括類載入的時候?有沒有上下文程式碼語境?也沒有定義例項是指什麼例項,是指Java例項嗎?還是單指String例項?包不包括JVM中的C++例項?

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

String結構

在分析之前,為了方便後面畫記憶體圖,我們需要對Java中的String結構有一個大致瞭解:

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

value:char陣列,用於用於儲存字元。hash:快取字串的雜湊碼,預設為0(String的hash值在真正呼叫 hashCode 方法的時候才會去計算)。serialVersionUID:序列化用的。正常的問題與合理的解釋

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

String s = new String("xyz");建立幾個String例項?

對於這個問題,在網上能找到一些比較高讚的答案:

兩個。一個是字串字面量"xyz"所對應的、存在於全域性共享的常量池中的例項,另一個是透過new String(String)建立並初始化的、內容(字元)與"xyz"相同的例項。考慮到如果常量池中如果有這個字串,就只會建立一個。同時在棧區還會有一個對new出來的String例項的s。

考慮到了棧與堆,提到了常量池,我認為這已經達到大部分面試官對這個題目答案的期許了,或許這也是面試官想要考察的點。

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

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

其次,就算答主說的“常量池”就是“字串常量池”,可“字串常量池”中存的是String例項的引用,而不是字串,這是有很大區別的。而且這個答案是沒有考慮程式碼執行的環境。

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

分清變數和例項

我們先回到開頭的問題與“標準答案” :

問題:String s = new String("xyz"); 建立了幾個例項?答案:兩個,一個堆區的“xyz”,一個棧區指向“xyz”的s

很明顯寫答案的人沒有把變數和例項分清楚。在Java裡,變數就是變數,型別的變數只是對某個物件例項或者null的,不是例項本身。宣告變數的個數跟建立例項的個數沒有必然關係。

舉個例子:

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

這段程式碼會涉及3個String型別的變數:

s1,指向下面String例項的1s2,指向與s1相同s3,值為null,不指向任何例項

以及3個String例項:

"xyz"字面量對應的駐留的字串常量的String例項""字面量對應的駐留的字串常量的String例項透過new String(String)建立的新String例項,沒有任何變數指向它類載入

對於String s = new String("xyz");建立幾個String例項?這個問題。

似乎網上的所有答案都把類載入過程和實際執行過程合在一起分析的。

看起來好像是沒有什麼問題的,因為想要執行某個程式碼片段,其所在的類必然要被載入,而且對於同一個類載入器,最多載入一次。

但是我們看一下這段程式碼的位元組碼:

似乎只出現了一次 new java/lang/String ,也就是隻建立了一個String例項。也就是說原問題中的程式碼在每執行一次只會新建立一個String例項。 這裡的ldc指令只是把先前在類載入過程中已經建立好的一個String例項("xyz")的一個引用壓到運算元棧頂而已,並沒有建立新的String例項。

不是應該有兩個例項嗎?還有一個String例項是在什麼時候建立的呢?

我們都知道類載入的解析階段是Java虛擬機器將常量池內的符號引用替換為直接引用的過程,根據JVM規範,符合規範的JVM實現應該在類載入的過程中建立並駐留一個String例項作為常量來對應"xyz"字面量,具體是在類載入的解析階段進行的。這個常量是全域性共享的,只在先前尚未有內容相同的字串駐留過的前提下才需要建立新的String例項。

JVM最佳化

以上討論都只是針對規範所定義的Java語言與Java虛擬機器而言。概念上是如此,但實際的JVM實現可以做得更最佳化,原問題中的程式碼片段有可能在實際執行的時候一個String例項也不會完整建立(沒有分配空間)。

不結合上下文程式碼來看就直接說是“標準答案”就是耍流氓。

我們看下這段程式碼:

執行這段程式碼,會不斷的建立String物件吃記憶體,然後頻繁的造成GC。

對於這個結論相信大家都沒有意見,我們加上 -XX:+PrintGC -XX:-DoEscapeAnalysis 列印日誌,關閉逃逸分析(JDK8預設開啟此最佳化,我們先關閉)

執行一下看看:

結果確實如我們所料,不斷的建立String物件吃記憶體導致頻繁GC。

我們現在將 -XX:-DoEscapeAnalysis 改成 -XX:+DoEscapeAnalysis ,重新跑一下這段程式碼:

神奇的事情發生了,繼續跑下去也沒有再打出GC日誌了。難道新建立String物件都不吃記憶體了麼?

實際情況是:經過HotSpot VM的的最佳化後,newString()方法不會新建立String例項了。這樣自然不吃記憶體,也就不再觸發GC了。

現在再來看開篇的那個問題,不結合具體情況,還能簡單的說String s = new String("xyz");會建立兩個String例項嗎?

我只是舉了一個逃逸分析的例子,HotSpot VM還有很多像這樣的最佳化,比如方法內聯、標量替換和無用程式碼削除。

klass-oop

如果題幹上沒有加上“Java”例項的定語,那JVM中的oop例項我們也不應該忽略。

為了後面能更好的說清楚這一點,需要補充一下klass-opp模型的知識。先做一個約定,全文只要涉及JVM具體實現的內容都是基於Jdk8中HotSpot VM展開的。

HotSpot VM是基於C++實現,而C++是一門面向物件的語言,本身是具備面向物件基本特徵的,所以Java中的物件表示,最簡單的做法是為每個Java類生成一個C++類與之對應。但HotSpot VM並沒有這麼做,而是設計了一套klass-oop模型。

klass ,它是Java類的元資訊在JVM中的存在形式。一個Java類被JVM類載入器載入之後,就是以klass的形式存在於JVM之中。

oop ,它是Java物件在JVM中的存在形式。每建立一個新的物件,在JVM內部就會相應地建立一個對應型別的OOP物件。

其中instanceOopDesc表示非陣列物件,arrayOopDesc表示陣列物件;

而objArrayOopDesc表示引用型別陣列物件,typeArrayOopDesc表示基本型別陣列物件。

舉個例子:Java中String類的一個例項,在JVM中會有一個對應的instanceOopDesc例項。

字串常量池

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

class位元組碼中的常量池:存在於硬碟上。主要存放兩大類常量:字面量、符號引用。執行時常量池:方法區的一部分。我們常說的常量池,就是指這一塊區域:方法區中的執行時常量池。字串常量池:存在於堆區。這個常量池在JVM層面就是一個StringTable,只儲存對java.lang.String例項的引用,而不儲存String物件的內容。一般我們說一個字串進入了字串常量池其實是說在這個StringTable中儲存了對它的引用,反之,如果說沒有在其中就是說StringTable中沒有對它的引用。

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

字串常量池,即String Pool。在JVM中對應的類是StringTable,底層實現是一個Hashtable。利用的是雜湊思想。

下面這段程式碼,是往字串常量池新增字串方法。雖然是C++程式碼,但我相信學過Java的人都能看懂,至少也能明白這段程式碼幹了什麼事情。會透過String的內容+長度生成的hash值定位下標index,然後將Java的String類的例項對應的instanceOopDesc封裝成HashtableEntry作為儲存結構儲存到常量池。

補充完字串常量池的知識之後,我們再回到文章開頭的那一題:

String s = new String("xyz");建立了幾個例項?

我們畫一個記憶體圖,圖中省略了兩個String對應的instanceOopDesc例項。

不難得出答案:

如果包括JVM中的C++例項的話,有兩個Java的String例項,兩個String例項對應的instanceOopDesc例項,還有一個char[]陣列對應的typeArrayOopDesc例項。加一起一共是5個,也可以說2個String例項加上3個oop例項。
總結

String s = new String("xyz"); 建立了幾個例項?

透過以上的分析,我們會發現,每在這道題目的題幹上每加一個定語,這道題目就會有不同的答案。

是否考慮類載入過程,是否考慮JVM最佳化,是否包括對應的oop例項等等等等,每個點都值得聊一聊的。

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

19
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • ReentrantLock和SpringJPA引起的死鎖問題