一、方法區、永久代、元空間
1.方法區、永久代
方法區也是各個執行緒共享的記憶體區域,它用於儲存已經被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。
方法區域又被稱為“永久代”,但這僅僅對於Sun HotSpot來講,JRockit和IBM J9虛擬機器中並不存在永久代的概念。
Java虛擬機器規範把方法區描述為Java堆的一個邏輯部分,而且它和Java Heap一樣不需要連續的記憶體,可以選擇固定大小或可擴充套件,另外,虛擬機器規範允許該區域可以選擇不實現垃圾回收。
相對而言,垃圾收集行為在這個區域比較少出現。該區域的記憶體回收目標主要針是對廢棄常量的和無用類的回收。
執行時常量池是方法區的一部分,Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池(Class檔案常量池),用於存放編譯器生成的各種字面量和符號引用,這部分內容將在類載入後存放到方法區的執行時常量池中。
執行時常量池相對於Class檔案常量池的另一個重要特徵是具備動態性,Java語言並不要求常量一定只能在編譯期產生,也就是並非預置入Class檔案中的常量池的內容才能進入方法區的執行時常量池,執行期間也可能將新的常量放入池中,這種特性被開發人員利用比較多的是String類的intern()方法。
2.變更歷史
在JDK1.7之前 執行時常量池邏輯包含字串常量池存放在方法區, 此時hotspot虛擬機器對方法區的實現為永久代
在JDK1.7 字串常量池被從方法區拿到了堆(方法區是堆的一個邏輯分割槽)中, 這裡沒有提到執行時常量池,也就是說字串常量池被單獨拿到堆,執行時常量池剩下的東西還在方法區, 也就是hotspot中的永久代
在JDK1.8及之後 hotspot移除了永久代用元空間(Metaspace)取而代之, 這時候字串常量池還在堆, 執行時常量池還在方法區, 只不過方法區的實現從永久代變成了元空間(Metaspace)。
如下圖Java堆記憶體結構,注意,在Java虛擬機器規範將永久代(方法區)中描述為Java堆的一個邏輯部分。
二、元空間
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剩餘空間容量的百分比,減少為釋放空間所導致的垃圾收集
2.變動原因
字串存在永久代中,現實使用中易出問題, 由於永久代記憶體經常不夠用或發生記憶體洩露,爆出異常 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沒有永久代,不需要配置永久代。
3.移除永久代的影響
由於類的元資料分配在本地記憶體中,元空間的最大可分配空間就是系統可用記憶體空間。因此,我們就不會遇到永久代存在時的記憶體溢位錯誤,也不會出現洩漏的資料移到交換區這樣的事情。終端使用者可以為元空間設定一個可用空間最大值,如果不進行設定,JVM會自動根據類的元資料大小動態增加元空間的容量。
注意:永久代的移除並不代表自定義的類載入器洩露問題就解決了。因此,你還必須監控你的記憶體消耗情況,因為一旦發生洩漏,會佔用你的大量本地記憶體,並且還可能導致交換區交換更加糟糕。
瞭解更多:元空間的記憶體管理
三、String.intern面試題
1.intern()方法解釋
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前後的處理方式不同, 往後看)
2.例項
看下一個例子:
String str1 = "a";
String str2 = "b";
String str3 = "ab";
String str4 = str1 + str2;
String str5 = new String("ab"); System.out.println(str5.equals(str3)); // true
System.out.println(str5 == str3); // false
System.out.println(str5.inter() == str3); // true
System.out.println(str5.inter() == str4); // false
為什麼會得到這樣的一個結果呢?我們一步一步的分析。
第一、str5.equals(str3)這個結果為true,不用太多的解釋,因為字串的值的內容相同。
第二、str5 == str3對比的是引用的地址是否相同,由於str5採用new String方式定義的,所以地址引用一定不相等。所以結果為false。
第三、當str5呼叫intern的時候,會檢查字串池中是否含有該字串。由於之前定義的str3已經進入字串池中,所以會得到相同的引用。
第四,當str4 = str1 + str2後,str4的值也為”ab”,但是為什麼這個結果會是false呢?先看下面程式碼:
String a = new String("ab");
String b = new String("ab");
String c = "ab";
String d = "a" + "b";
String e = "b";
String f = "b" + e; System.out.println(b.intern() == a); // false
System.out.println(b.intern() == c); // true
System.out.println(b.intern() == d); // true
System.out.println(b.intern() == f); // false
System.out.println(b.intern() == a.intern()); // true
由執行結果可以看出來,b.intern() == a和b.intern() == c可知,採用new 建立的字串物件不進入字串池,並且通過b.intern() == d和b.intern() == f可知,字串相加的時候,都是靜態字串的結果會新增到字串池,如果其中含有變數(如f中的e)則不會進入字串池中。但是字串一旦進入字串池中,就會先查詢池中有無此物件。如果有此物件,則讓物件引用指向此物件。如果無此物件,則先建立此物件,再讓物件引用指向此物件。
3.在看《深入理解java虛擬機器》中的一個例子:
結果是:
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"加入到了常量池中)
4.總結:
- 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”。
final String str1=”ja”;
final String str2=”va”;
String str3=str1+str2;
- 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