String s = new String("xyz"); 建立了幾個實例?
這是一道很經典的面試題,在一本所謂的Java寶典上,我看到的「標準答案」是這樣的:java
兩個,一個堆區的「xyz」,一個棧區指向「xyz」的s。git
這個所謂的「標準答案」槽點太多,後面咱們慢慢分析。程序員
可是我以爲這個問題自己不具備什麼意義,由於他沒有既定義「建立」的具體含義,又沒有指定「建立」的時間,是運行時嗎?包不包括類加載的時候?有沒有上下文代碼語境?也沒有定義實例是指什麼實例,是指Java實例嗎?仍是單指String實例?包不包括JVM中的C++實例?github
顯然,這是一個「有問題的問題」。也是一個「有問題的答案」。面試
在分析以前,爲了方便後面畫內存圖,咱們須要對Java中的String結構有一個大體瞭解:數組
從上圖能夠看出,String類有三個屬性:緩存
value:char數組,用於用於存儲字符。oracle
hash:緩存字符串的哈希碼,默認爲0(String的hash值在真正調用hashCode
方法的時候纔會去計算)。jvm
serialVersionUID:序列化用的。oop
在上面的題幹上加上一些限定詞,能夠獲得一個新的問題:
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類型的變量:
以及3個String實例:
對於String s = new String("xyz");建立幾個String實例?這個問題。
彷佛網上的全部答案都把類加載過程和實際執行過程合在一塊兒分析的。
看起來好像是沒有什麼問題的,由於想要執行某個代碼片斷,其所在的類必然要被加載,並且對於同一個類加載器,最多加載一次。
可是咱們看一下這段代碼的字節碼:
彷佛只出現了一次new java/lang/String
,也就是隻建立了一個String實例。也就是說原問題中的代碼在每執行一次只會新建立一個String實例。 這裏的ldc指令只是把先前在類加載過程當中已經建立好的一個String實例("xyz")的一個引用壓到操做數棧頂而已,並無建立新的String實例。
不是應該有兩個實例嗎?還有一個String實例是在何時建立的呢?
咱們都知道類加載的解析階段是Java虛擬機將常量池內的符號引用替換爲直接引用的過程,根據JVM規範,符合規範的JVM實現應該在類加載的過程當中建立並駐留一個String實例做爲常量來對應"xyz"字面量,具體是在類加載的解析階段進行的。這個常量是全局共享的,只在先前還沒有有內容相同的字符串駐留過的前提下才須要建立新的String實例。
因此你能夠理解成,在類加載的解析階段,其實已經建立了一個String實例,執行代碼的時候,又new了一個String實例。固然,你把二者放在一塊兒討論並不會有什麼問題。
以上討論都只是針對規範所定義的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還有不少像這樣的優化,好比方法內聯、標量替換和無用代碼削除。
若是題幹上沒有加上「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體系中,有三種常量池:
今天,咱們重點說的是字符串常量池,即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實例等等等等,每一個點都值得聊一聊的。
下次有人問你,你不妨把這篇的文章分享給他。
爲了寫這一篇文章,我翻看了不少@RednaxelaFX前輩和周志明前輩的博客,過程當中收益良多。在這裏感謝前輩們爲國內JVM的科普與發展作出的貢獻!
還有一個頗有趣的故事,我在查找「如何經過HSDB來了解String」相關資料的時候,看到一篇寫的很好的文章,驚呼國內還有這麼多低調的大神,後來添加了文章旁邊的公衆號,發現這個大神原來是PerfMa的創始人「寒泉子」李嘉鵬前輩,冒犯了冒犯了!
本着對每一篇發出去的文章負責的原則,文中涉及知識理論,我都會盡可能在官方文檔和權威書籍找到並加以驗證。但即便這樣,我也不能保證文中每一個點都是正確的,若是你發現錯誤之處,歡迎指出,我會對其修正。
創做不易,你的正反饋對我來講很是重要!點個贊,點個再看,點個關注甚至評論區發送一條666都是對我最大的支持!
我是CoderW,一個普通的程序員。
謝謝你的閱讀,咱們下期再見
文中涉及代碼:https://github.com/xiaoyingzhi/blog
JVM Spec Java SE 8Edition:https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf
參考文章:http://isfeasible.cn/posts/view/5b84b6ab3957bb300a5bca94