天天看點

JDK方法區、元空間差別 & String.intern相關面試題

JDK方法區、元空間差別 & String.intern相關面試題

你了解方法區、永久代以及元空間的功能和差別嗎?String.intern()原理你懂了嗎?帶着疑問,一起來看看吧.....

方法區也是各個線程共享的記憶體區域,它用于存儲已經被虛拟機加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料。

方法區域又被稱為“永久代”,但這僅僅對于Sun HotSpot來講,JRockit和IBM J9虛拟機中并不存在永久代的概念。

Java虛拟機規範把方法區描述為Java堆的一個邏輯部分,而且它和Java Heap一樣不需要連續的記憶體,可以選擇固定大小或可擴充,另外,虛拟機規範允許該區域可以選擇不實作垃圾回收。

相對而言,垃圾收集行為在這個區域比較少出現。該區域的記憶體回收目标主要針是對廢棄常量的和無用類的回收。

運作時常量池是方法區的一部分,Class檔案中除了有類的版本、字段、方法、接口等描述資訊外,還有一項資訊是常量池(Class檔案常量池),用于存放編譯器生成的各種字面量和符号引用,這部分内容将在類加載後存放到方法區的運作時常量池中。

運作時常量池相對于Class檔案常量池的另一個重要特征是具備動态性,Java語言并不要求常量一定隻能在編譯期産生,也就是并非預置入Class檔案中的常量池的内容才能進入方法區的運作時常量池,運作期間也可能将新的常量放入池中,這種特性被開發人員利用比較多的是String類的intern()方法。

在JDK1.7之前 運作時常量池邏輯包含字元串常量池存放在方法區, 此時hotspot虛拟機對方法區的實作為永久代

在JDK1.7 字元串常量池被從方法區拿到了堆(方法區是堆的一個邏輯分區)中, 這裡沒有提到運作時常量池,也就是說字元串常量池被單獨拿到堆,運作時常量池剩下的東西還在方法區, 也就是hotspot中的永久代

在JDK1.8及之後 hotspot移除了永久代用元空間(Metaspace)取而代之, 這時候字元串常量池還在堆, 運作時常量池還在方法區, 隻不過方法區的實作從永久代變成了元空間(Metaspace)。

如下圖Java堆記憶體結構,注意,在Java虛拟機規範将永久代(方法區)中描述為Java堆的一個邏輯部分。

JDK方法區、元空間差別 & String.intern相關面試題

JDK8 HotSpot JVM 将永久代移除,使用本地記憶體來存儲類中繼資料資訊并稱之為:元空間(Metaspace)。

以下是JVM記憶體模型中方法區的變動:

新生代:Eden+From Survivor+To Survivor

老年代:OldGen

永久代(方法區的實作) : PermGen替換為Metaspace(本地記憶體中)

方法區和“PermGen space”又有着本質的差別。前者是 JVM 的規範,而後者則是 JVM 規範的一種實作,并且隻有 HotSpot 才有 “PermGen space”,而對于其他類型的虛拟機,如 JRockit(Oracle)、J9(IBM) 并沒有“PermGen space”。由于方法區主要存儲類的相關資訊,是以對于動态生成類的情況比較容易出現永久代的記憶體溢出。

元空間的本質和永久代類似,都是對JVM規範中方法區的實作。不過元空間與永久代之間最大的差別在于:元空間并不在虛拟機中,而是使用本地記憶體。是以,預設情況下,元空間的大小僅受本地記憶體限制,但可以通過以下參數來指定元空間的大小:

  -XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型解除安裝,同時GC會對該值進行調整:如果釋放了大量的空間,就适當降低該值;如果釋放了很少的空間,那麼在不超過MaxMetaspaceSize時,适當提高該值。   -XX:MaxMetaspaceSize,最大空間,預設是沒有限制的。  除了上面兩個指定大小的選項以外,還有兩個與 GC 相關的屬性:   -XX:MinMetaspaceFreeRatio,在GC之後,最小的Metaspace剩餘空間容量的百分比,減少為配置設定空間所導緻的垃圾收集   -XX:MaxMetaspaceFreeRatio,在GC之後,最大的Metaspace剩餘空間容量的百分比,減少為釋放空間所導緻的垃圾收集

字元串存在永久代中,現實使用中易出問題, 由于永久代記憶體經常不夠用或發生記憶體洩露,爆出異常 java.lang.OutOfMemoryError: PermGen

類及方法的資訊等比較難确定其大小,是以對于永久代的大小指定比較困難,太小容易出現永久代溢出,太大則容易導緻老年代溢出。

永久代會為 GC 帶來不必要的複雜度,并且回收效率偏低。

This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation. 即:移除永久代是為融合HotSpot JVM與 JRockit VM而做出的努力,因為JRockit沒有永久代,不需要配置永久代。

由于類的中繼資料配置設定在本地記憶體中,元空間的最大可配置設定空間就是系統可用記憶體空間。是以,我們就不會遇到永久代存在時的記憶體溢出錯誤,也不會出現洩漏的資料移到交換區這樣的事情。最終使用者可以為元空間設定一個可用空間最大值,如果不進行設定,JVM會自動根據類的中繼資料大小動态增加元空間的容量。

注意:永久代的移除并不代表自定義的類加載器洩露問題就解決了。是以,你還必須監控你的記憶體消耗情況,因為一旦發生洩漏,會占用你的大量本地記憶體,并且還可能導緻交換區交換更加糟糕。

了解更多:元空間的記憶體管理

intern() 方法傳回字元串對象的規範化表示形式。

它遵循以下規則:對于任意兩個字元串 s 和 t,當且僅當 s.equals(t) 為 true 時,s.intern() == t.intern() 才為 true。

String.intern方法究竟做了什麼?jdk源碼中對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 section 3.10.5 of the The Java? Language Specification.

簡單來說就是intern用來傳回常量池中的某字元串,如果常量池中已經存在該字元串,則直接傳回常量池中該對象的引用。否則,在常量池中加入該對象,然後 傳回引用。(這裡的加入該對象對于java1.7前後的處理方式不同, 往後看)

看下一個例子:

 

為什麼會得到這樣的一個結果呢?我們一步一步的分析。

第一、str5.equals(str3)這個結果為true,不用太多的解釋,因為字元串的值的内容相同。

第二、str5 == str3對比的是引用的位址是否相同,由于str5采用new String方式定義的,是以位址引用一定不相等。是以結果為false。

第三、當str5調用intern的時候,會檢查字元串池中是否含有該字元串。由于之前定義的str3已經進入字元串池中,是以會得到相同的引用。

第四,當str4 = str1 + str2後,str4的值也為”ab”,但是為什麼這個結果會是false呢?先看下面代碼:

  

由運作結果可以看出來,b.intern() == a和b.intern() == c可知,采用new 建立的字元串對象不進入字元串池,并且通過b.intern() == d和b.intern() == f可知,字元串相加的時候,都是靜态字元串的結果會添加到字元串池,如果其中含有變量(如f中的e)則不會進入字元串池中。但是字元串一旦進入字元串池中,就會先查找池中有無此對象。如果有此對象,則讓對象引用指向此對象。如果無此對象,則先建立此對象,再讓對象引用指向此對象。

 結果是:

true false

《深入了解java虛拟機》中寫道,如果JDK1.6會傳回兩個false,JDK1.7運作則會傳回一個true一個false。

JDK1.6中,intern()方法會把首次遇到的字元串執行個體複制到永久代中,傳回的也是永久代中這個字元串的執行個體的引用,而StringBulder建立的字元串執行個體在Java堆上,是以必然不是同一個引用,将傳回false。

JDK1.7中,intern()的實作不會在複制執行個體,隻是在常量池中記錄首次出現的執行個體引用,是以傳回的是引用和由StringBuilder.toString()建立的那個字元串執行個體是同一個。

str2的比較傳回false因為"java"這個字元串在執行StringBuilder.toString()之前已經出現過,字元串常量池中已經有它的引用了,不符合“首次出現”的原則,而“計算機軟體”這個字元串是首次出現,是以傳回true。(System類自動由java虛拟機調用, 其中把"java"加入到了常量池中)

new String都是在堆上建立字元串對象。當調用 intern() 方法時,編譯器會将字元串添加到常量池中(stringTable維護),并傳回指向該常量的引用。

通過字面量指派建立字元串(如:String str=”twm”)時,會先在常量池中查找是否存在相同的字元串,若存在,則将棧中的引用直接指向該字元串;若不存在,則在常量池中生成一個字元串,再将棧中的引用指向該字元串。

常量字元串的“+”操作,編譯階段直接會合成為一個字元串。如string str=”JA”+”VA”,在編譯階段會直接合并成語句String str=”JAVA”,于是會去常量池中查找是否存在”JAVA”,進而進行建立或引用。

常量字元串和變量拼接時(如:String str3=baseStr + “01”;)會調用stringBuilder.append()在堆上建立新的對象。

對于final字段,編譯期直接進行了常量替換(而對于非final字段則是在運作期進行指派處理的)。在編譯時,直接替換成了String str3=”ja”+”va”,根據第三條規則,再次替換成String str3=”JAVA”。

JDK 1.7後,intern方法還是會先去查詢常量池中是否有已經存在,如果存在,則傳回常量池中的引用,這一點與之前沒有差別,差別在于,如果在常量池找不到對應的字元串,則不會再将字元串拷貝到常量池,而隻是在常量池中生成一個對原字元串的引用。簡單的說,就是往常量池放的東西變了:原來在常量池中找不到時,複制一個副本放到常量池,1.7後則是将在堆上的位址引用複制到常量池。

https://blog.csdn.net/q5706503/article/details/84640762

https://blog.csdn.net/q5706503/article/details/84621210

https://blog.csdn.net/q5706503/article/details/84586219

https://blog.csdn.net/soonfly/article/details/70147205

繼續閱讀