前言
再次聲明本章是一個系列中的其中一段,若想了解更多JVM虛擬機底層原理 點擊首頁,或關注博主,今天來談方法區,對於方法區的內容今天多,各位聽我慢慢說。
方法區
單單從名字上來看方法區似乎與我們的方法定義有關,確實如此,但是還不夠嚴謹,我也在網上看了很多方法區的定義,但是五花八門,總感覺不夠清晰!所以我們一起看看JVM規範中對方法是怎麼定義的?
JVM規範-方法區定義 https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html
定義:
翻譯:首先是所有Java虛擬機線程共享的一塊區域,他存放了根類結構相關的一些信息,
有類的變量,方法信息,構造方法和構造器信息,和一些特殊方法(主要指類構造器)。
方法區在虛擬機啓動時被創建,邏輯上來講是堆的一個組成部分,但是並不強制你的具體位置,(說白了就是不同的JVM廠商在實現的時候不一定完全按照標準來創造)
最後對於內存定義:方法區在內存不足是也會拋出 OutOfMemoryError
解釋:
說了半天可能有人聽不懂了,那就來解釋一下!
拿oracle的hospost虛擬機舉例(也就是我們日常用的)
組成:
在JDK8之前hospost虛擬機對方法區的實現,叫做永久代,永久代就是使用堆的一部分空間去做實現的
而在JDK8之後把永久代移除了,換了一個實現,叫做元空間 元空間用的不是堆的內存,而是本地內存,也就是操作系統的內存
所以不同的實現對於方法區的選擇位置也不同
方法區內存溢出:
有人會疑問,方法區不就存放類的一些信息和實例嗎?大小也不足以內存溢出啊,具體什麼情況,拿個例子看下
/** 元空間
* 演示元空間內存溢出 java.lang.OutOfMemoryError: Metaspace
* -XX:MaxMetaspaceSize=8m
*/
public class Demo1_8 extends ClassLoader { // ClassLoader可以用來加載類的二進制字節碼
public static void main(String[] args) {
int j = 0;
try {
Demo1_8 test = new Demo1_8();
for (int i = 0; i < 10000; i++, j++) {
// ClassWriter 作用是生成類的二進制字節碼
ClassWriter cw = new ClassWriter(0);
// 版本號, public, 類名, 包名, 父類, 接口
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
// 返回 byte[]
byte[] code = cw.toByteArray();
// 執行了類的加載
test.defineClass("Class" + i, code, 0, code.length); // Class 對象
}
} finally {
System.out.println(j);
}
}
}
代碼註釋都有,自行理解,由於電腦物理內存大不方便演示效果,所以要加 -XX:MaxMetaspaceSize=8m 參數
JVM1.6執行結果:永久代內存溢出導致OutOfMemoryError
JVM1.8 之後會導致元空間內存溢出 java.lang.OutOfMemoryError: Metaspace
雖然都是OutOfMemoryError 但是可以看到1.8之前是permGen space 1.8之後是Metaspace
這種場景是非常多的, 看過spring mybatis源碼的應該瞭解 代碼使用不當是很容易造成方法區內存溢出的
運行時常量池:
在明白運行時常量池之前首先說說什麼是常量池
-
常量池,就是一張表,虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量等信息
-
運行時常量池,常量池是 *.class 文件中的,當該類被加載,它的常量池信息就會放入運行時常量池,並把裏面的符號地址變爲真實地址
瞭解運行時常量池之後,不得不說運行時常量池的一個重要組成部分StringTable
說StringTable先看幾道經典面試題
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern(); // 問
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
String x2 = new String("c") + new String("d");
String x1 = "cd";
x2.intern();
// 問,如果調換了【最後兩行代碼】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2);
講解:
① s3 == s4 //false
s3 = "a" + "b" 兩個字符串相加 stringtable會有編譯期優化,結果就是 "ab"(字符串ab) 常量池沒有,順便入池。
s4 = s1 + s2 兩個變量拼接在運行期使用stringbuilder拼接產生新的字符串 新的字符串相當於new String("ab")出來的 所以在堆中
所以第一題爲false 理由:一個在常量池中一個在堆內存中
② s3 == s5 //true
s5 = "ab" 是一個自變量會首先檢查常量池的內容,結果常量池已經有s3拼好的"ab"了,所以s5不會創建新的對象,會直接引用常量池以有的對象,因此s3和s5都是同一個對象 所以爲true
③ s3 == s6 // true
s6又是調用s4.intern()方法 intern() 方法回去常量池先看有沒有這個對象,如果有就返回常量池的對象,如果沒有就嘗試將s4進行入池,
顯然常量池已有ab,沒能入池成功,但他返回常量池中的對象,所以s6和s3就是一個對象 所以爲true
④ x1 == x2 // false
x2 顯然是堆中的對象 new String("cd") ; x1 顯然是常量池中的對象
x2調用intern方法嘗試將堆內存中的x2進行入池,但是常量池已經有了,所以沒能入池成功,所以最終 x2是堆內存中的對象 x1 是常量池中的對象 結果爲false
StringTable 特性
-
常量池中的字符串僅是符號,第一次用到時才變爲對象
-
利用串池的機制,來避免重複創建字符串對象
-
字符串變量拼接的原理是 StringBuilder (1.8)
-
字符串常量拼接的原理是編譯期優化
-
可以使用 intern 方法,主動將串池中還沒有的字符串對象放入串池
-
1.8 將這個字符串對象嘗試放入串池,如果有則並不會放入,如果沒有則放入串池, 會把串池中的對象返回
-
1.6 將這個字符串對象嘗試放入串池,如果有則並不會放入,如果沒有會把此對象複製一份,放入串池, 會把串池中的對象返回
-
StringTable 位置
在JVM1.6之前stringtable在方法區,具體實現也就是永久代的其中一塊空間 而在1.7 1.8之後 他的實現空間被移動到堆內存中
爲什麼更改? 原因是什麼?
因爲永久代內存不足,而且永久代只有觸發FullGC時候纔會執行他的垃圾回收,但是FullGC只有等到整個老年代的空間不足纔會觸發,回收時間會很晚,間接的導致stringtable的回收效率並不高,所以1.6之後的JVM廠商對此作了優化
StringTable 性能調優
最後分享StringTable的性能調優問題,由於stringtable是桶機制,所以我們需要調整桶的個數,具體數據根據自己硬件以及合適的佔比去調整
調整 -XX:StringTableSize=桶個數
總結
通過本章至少了解到方法區的以下問題
1. 定義
2. 組成
3. 方法區內存溢出
4. 運行時常量池
5. StringTable 特性
6. StringTable 位置
7. StringTable 性能調優
關於方法區的詳細介紹本章到此,下一篇介紹 直接內存
希望同仁志士,前來參考以及指點!共同進步,發揚文化精神!轉載請標明出處!
感覺不錯的點個贊關注一下吧!