第 9 章 方法區
1、棧 堆 方法區的互動關系
從記憶體結構來看
這次所講述的是運作時資料區的最後一個部分
從線程共享與否的角度來看
ThreadLocal:如何保證多個線程在并發環境下的安全性?典型應用就是資料庫連接配接管理,以及獨立會話管理
棧、堆、方法區的互動關系
下面就涉及了對象的通路定位
- Person 類的 .class 資訊存放在方法區中
- person 變量存放在 Java 棧的局部變量表中
- 真正的 person 對象存放在 Java 堆中
- 在 person 對象中,有個指針指向方法區中的 person 類型資料,表明這個 person 對象是用方法區中的 Person 類 new 出來的
2、方法區的了解
官方文檔
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4
2.1、方法區的位置
方法區的位置
- 《Java虛拟機規範》中明确說明:盡管所有的方法區在邏輯上是屬于堆的一部分,但一些簡單的實作可能不會選擇去進行垃圾收集或者進行壓縮。
- 但對于HotSpotJVM而言,方法區還有一個别名叫做Non-Heap(非堆),目的就是要和堆分開。
- 是以,方法區可以看作是一塊獨立于Java堆的記憶體空間。
2.2、方法區的了解
方法區的基本了解
方法區主要存放的是 Class,而堆中主要存放的是執行個體化的對象
- 方法區(Method Area)與Java堆一樣,是各個線程共享的記憶體區域
- 多個線程同時加載統一個類時,隻能有一個線程能加載該類,其他線程隻能等等待該線程加載完畢,然後直接使用該類,即類隻能加載一次。
- 方法區在JVM啟動的時候被建立,并且它的實際的實體記憶體空間中和Java堆區一樣都可以是不連續的。
- 方法區的大小,跟堆空間一樣,可以選擇固定大小或者可擴充。
- 方法區的大小決定了系統可以儲存多少個類,如果系統定義了太多的類,導緻方法區溢出,虛拟機同樣會抛出記憶體溢出錯誤:
- java.lang.OutofMemoryError:PermGen space
- 或者
- java.lang.OutOfMemoryError:Metaspace
- 舉例說明方法區 OOM
- 加載大量的第三方的jar包
- Tomcat部署的工程過多(30~50個)
- 大量動态的生成反射類
- 關閉JVM就會釋放這個區域的記憶體。
代碼舉例
- 代碼
public class EdenSurvivorTest {
public static void main(String[] args) {
System.out.println("我隻是來打個醬油~");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複制
- 簡單的程式,加載了好多類
2.3、方法區演進過程
Hotspot 方法區的演進過程
- 在 JDK7 及以前,習慣上把方法區,稱為永久代。JDK8開始,使用元空間取代了永久代。JDK 1.8後,元空間存放在堆外記憶體中
- 我們可以将方法區類比為Java中的接口,将永久代或元空間類比為Java中具體的實作類
- 本質上,方法區和永久代并不等價。僅是對Hotspot而言的可以看作等價。《Java虛拟機規範》對如何實作方法區,不做統一要求。例如:BEAJRockit / IBM J9 中不存在永久代的概念。
- 現在來看,當年使用永久代,不是好的idea。導緻Java程式更容易OOm(超過-XX:MaxPermsize上限)
- 而到了JDK8,終于完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地記憶體中實作的元空間(Metaspace)來代替
- 元空間的本質和永久代類似,都是對JVM規範中方法區的實作。不過元空間與永久代最大的差別在于:元空間不在虛拟機設定的記憶體中,而是使用本地記憶體
- 永久代、元空間二者并不隻是名字變了,内部結構也調整了
- 根據《Java虛拟機規範》的規定,如果方法區無法滿足新的記憶體配置設定需求時,将抛出OOM異常
3、設定方法區大小與 OOM
方法區的大小不必是固定的,JVM可以根據應用的需要動态調整。
3.1、JDK7 永久代
JDK7 之前版本設定永久代大小
- 通過-XX:Permsize來設定永久代初始配置設定空間。預設值是20.75M
- -XX:MaxPermsize來設定永久代最大可配置設定空間。32位機器預設是64M,64位機器模式是82M
- 當JVM加載的類資訊容量超過了這個值,會報異常OutofMemoryError:PermGen space。
3.2、JDK8 元空間
JDK8 版本設定元空間大小
- 中繼資料區大小可以使用參數 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 指定
- 預設值依賴于平台,Windows下,-XX:MetaspaceSize 約為21M,**-XX:MaxMetaspaceSize的值是-1,即沒有限制**。
- 與永久代不同,如果不指定大小,預設情況下,虛拟機會耗盡所有的可用系統記憶體。如果中繼資料區發生溢出,虛拟機一樣會抛出異常OutOfMemoryError:Metaspace
- -XX:MetaspaceSize:設定初始的元空間大小。對于一個 64位 的伺服器端 JVM 來說,其預設的 -XX:MetaspaceSize值為21MB。這就是初始的高水位線,一旦觸及這個水位線,Full GC将會被觸發并解除安裝沒用的類(即這些類對應的類加載器不再存活),然後這個高水位線将會重置。新的高水位線的值取決于GC後釋放了多少元空間。
- 如果釋放的空間不足,那麼在不超過MaxMetaspaceSize時,适當提高該值。
- 如果釋放空間過多,則适當降低該值。
- 如果初始化的高水位線設定過低,上述高水位線調整情況會發生很多次。通過垃圾回收器的日志可以觀察到Full GC多次調用。為了避免頻繁地GC,建議将-XX:MetaspaceSize設定為一個相對較高的值。
配置元空間大小示例
- 代碼
/**
* 測試設定方法區大小參數的預設值
*
* jdk7及以前:
* -XX:PermSize=100m -XX:MaxPermSize=100m
*
* jdk8及以後:
* -XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m
*
* @author shkstart [email protected]
* @create 2020 12:16
*/
public class MethodAreaDemo {
public static void main(String[] args) {
System.out.println("start...");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end...");
}
}
複制
- JVM 參數
-XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m
複制
- CMD 指令檢視設定的元空間大小
C:\Users\Heygo>jps
C:\Users\Heygo>jinfo -flag MetaspaceSize pId
C:\Users\Heygo>jinfo -flag MaxMetaspaceSize pId
複制
3.3、方法區 OOM
方法區 OOM 舉例
- 代碼:OOMTest 類繼承 ClassLoader 類,獲得 defineClass() 方法,可自己進行類的加載
/**
* jdk6/7中:
* -XX:PermSize=10m -XX:MaxPermSize=10m
*
* jdk8中:
* -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
*
* @author shkstart [email protected]
* @create 2020 22:24
*/
public class OOMTest extends ClassLoader {
public static void main(String[] args) {
int j = 0;
try {
OOMTest test = new OOMTest();
for (int i = 0; i < 10000; i++) {
//建立ClassWriter對象,用于生成類的二進制位元組碼
ClassWriter classWriter = new ClassWriter(0);
//指明版本号,修飾符,類名,包名,父類,接口
classWriter.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
//傳回byte[]
byte[] code = classWriter.toByteArray();
//類的加載
test.defineClass("Class" + i, code, 0, code.length);//Class對象
j++;
}
} finally {
System.out.println(j);
}
}
}
複制
不設定元空間的上限
- 使用預設的 JVM 參數,元空間不設定上限
10000
複制
設定元空間的上限
- JVM 參數
-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
複制
- 元空間出現 OOM
com.atguigu.java.OOMTest
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at com.atguigu.java.OOMTest.main(OOMTest.java:29)
8531
複制
3.4、解決 OOM
如何解決 OOM?
- 要解決OOM異常或heap space的異常,一般的手段是首先通過記憶體映像分析工具(如Ec1ipse Memory Analyzer)對dump出來的堆轉儲快照進行分析,重點是确認記憶體中的對象是否是必要的,也就是要先厘清楚到底是出現了記憶體洩漏(Memory Leak)還是記憶體溢出(Memory Overflow)
- 記憶體洩漏就是有大量的引用指向某些對象,但是這些對象以後不會使用了,但是因為它們還和GC ROOT有關聯,是以導緻以後這些對象也不會被回收,這就是記憶體洩漏的問題
- 如果是記憶體洩漏,可進一步通過工具檢視洩漏對象到GC Roots的引用鍊。于是就能找到洩漏對象是通過怎樣的路徑與GC Roots相關聯并導緻垃圾收集器無法自動回收它們的。掌握了洩漏對象的類型資訊,以及GC Roots引用鍊的資訊,就可以比較準确地定位出洩漏代碼的位置。
- 如果不存在記憶體洩漏,換句話說就是記憶體中的對象确實都還必須存活着,那就應當檢查虛拟機的堆參數(-Xmx與-Xms),與機器實體記憶體對比看是否還可以調大,從代碼上檢查是否存在某些對象生命周期過長、持有狀态時間過長的情況,嘗試減少程式運作期的記憶體消耗。
4、方法區的内部結構
4.1、方法區結構
方法區(Method Area)存儲什麼?
《深入了解Java虛拟機》書中對方法區(Method Area)存儲内容描述如下:它用于存儲已被虛拟機加載的類型資訊、常量、靜态變量、即時編譯器編譯後的代碼緩存等。
類型資訊
對每個加載的類型(類class、接口interface、枚舉enum、注解annotation),JVM必須在方法區中存儲以下類型資訊:
- 這個類型的完整有效名稱(全名=包名.類名)
- 這個類型直接父類的完整有效名(對于interface或是java.lang.Object,都沒有父類)
- 這個類型的修飾符(public,abstract,final的某個子集)
- 這個類型直接接口的一個有序清單
域(Field)資訊
- JVM必須在方法區中儲存類型的所有域的相關資訊以及域的聲明順序。
- 域的相關資訊包括:
- 域名稱
- 域類型
- 域修飾符(public,private,protected,static,final,volatile,transient的某個子集)
方法(Method)資訊
JVM必須儲存所有方法的以下資訊,同域資訊一樣包括聲明順序:
- 方法名稱
- 方法的傳回類型(包括 void 傳回類型),void 在 Java 中對應的類為 void.class
- 方法參數的數量和類型(按順序)
- 方法的修飾符(public,private,protected,static,final,synchronized,native,abstract的一個子集)
- 方法的位元組碼(bytecodes)、操作數棧、局部變量表及大小(abstract和native方法除外)
- 異常表(abstract和native方法除外),異常表記錄每個異常處理的開始位置、結束位置、代碼處理在程式計數器中的偏移位址、被捕獲的異常類的常量池索引
代碼示例
- 代碼
/** * 測試方法區的内部構成 * * @author shkstart [email protected] * @create 2020 23:39 */public class MethodInnerStrucTest extends Object implements Comparable<String>, Serializable { //屬性 public int num = 10; private static String str = "測試方法的内部結構"; //構造器沒寫 //方法 public void test1() { int count = 20; System.out.println("count = " + count); } public static int test2(int cal) { int result = 0; try { int value = 30; result = value / cal; } catch (Exception e) { e.printStackTrace(); } return result; } @Override public int compareTo(String o) { return 0; }}
複制
- 反編譯位元組碼檔案,并輸出值文本檔案中,便于檢視
- 參數 -p 確定能檢視 private 權限類型的字段或方法
javap -v -p MethodInnerStrucTest.class > Text.txt
複制
類型資訊
- 插句嘴:在運作時方法區中,類資訊中記錄了哪個加載器加載了該類,同時類加載器也記錄了它加載了哪些類
- 從反編譯檔案可以看出,位元組碼檔案記錄了 MethodInnerStrucTest 繼承了哪些類,實作了哪些方法
public class com.atguigu.java.MethodInnerStrucTest extends java.lang.Object implements java.lang.Comparable<java.lang.String>, java.io.Serializable
複制
域資訊
- descriptor: I 表示字段類型為 Integer
- flags: ACC_PUBLIC 表示字段權限修飾符為 public
public int num; descriptor: I flags: ACC_PUBLIC private static java.lang.String str; descriptor: Ljava/lang/String; flags: ACC_PRIVATE, ACC_STATIC
複制
方法資訊
- descriptor: ()V 表示方法傳回值類型為 void
- flags: ACC_PUBLIC 表示方法權限修飾符為 public
- stack=3 表示操作數棧深度為 3
- locals=2 表示局部變量個數為 2 個(實力方法包含 this)
- test1() 方法雖然沒有參數,但是其 args_size=1 ,這時因為将 this 作為了參數
public void test1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=2, args_size=1 0: bipush 20 2: istore_1 3: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 6: new #4 // class java/lang/StringBuilder 9: dup 10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V 13: ldc #6 // String count = 15: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 18: iload_1 19: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 22: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 25: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 28: return LineNumberTable: line 17: 0 line 18: 3 line 19: 28 LocalVariableTable: Start Length Slot Name Signature 0 29 0 this Lcom/atguigu/java/MethodInnerStrucTest; 3 26 1 count I
複制
4.2、域資訊特殊情況
non-final 類型的類變量
- 靜态變量和類關聯在一起,随着類的加載而加載,他們成為類資料在邏輯上的一部分
- 類變量被類的所有執行個體共享,即使沒有類執行個體時,你也可以通路它
代碼示例
- 如下代碼所示,即使我們把order設定為null,也不會出現空指針異常
- 這更加表明了 static 類型的字段和方法随着類的加載而加載,并不屬于特定的類執行個體
/** * non-final的類變量 * @author shkstart [email protected] * @create 2020 20:37 */public class MethodAreaTest { public static void main(String[] args) { Order order = null; order.hello(); System.out.println(order.count); }}class Order { public static int count = 1; public static final int number = 2; public static void hello() { System.out.println("hello!"); }}// 程式運作結果hello!1
複制
全局常量:static final
- 全局常量就是使用 static final 進行修飾
- 被聲明為final的類變量的處理方法則不同,每個全局常量在編譯的時候就會被配置設定了。
代碼示例
- 代碼
/** * non-final的類變量 * @author shkstart [email protected] * @create 2020 20:37 */public class MethodAreaTest { public static void main(String[] args) { Order order = null; order.hello(); System.out.println(order.count); }}class Order { public static int count = 1; public static final int number = 2; public static void hello() { System.out.println("hello!"); }}
複制
- 反編譯,檢視位元組碼指令,可以發現 number 的值已經寫死在位元組碼檔案中了
public static int count; descriptor: I flags: ACC_PUBLIC, ACC_STATIC public static final int number; descriptor: I flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL ConstantValue: int 2
複制
4.3、運作時常量池
運作時常量池 VS 常量池
官方文檔
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
- 方法區,内部包含了運作時常量池
- 位元組碼檔案,内部包含了常量池
- 要弄清楚方法區,需要了解清楚ClassFile,因為加載類的資訊都在方法區。
- 要弄清楚方法區的運作時常量池,需要了解清楚ClassFile中的常量池。
常量池
- 一個有效的位元組碼檔案中除了包含類的版本資訊、字段、方法以及接口等描述符資訊外
- 還包含一項資訊就是常量池表(Constant Pool Table),包括各種字面量和對類型、域和方法的符号引用
為什麼需要常量池?
- 一個java源檔案中的類、接口,編譯後産生一個位元組碼檔案。而Java中的位元組碼需要資料支援,通常這種資料會很大以至于不能直接存到位元組碼裡,換另一種方式,可以存到常量池
- 這個位元組碼包含了指向常量池的引用。在動态連結的時候會用到運作時常量池,之前有介紹
比如:如下的代碼:
public class SimpleClass { public void sayHello() { System.out.println("hello"); }}
複制
- 雖然上述代碼隻有194位元組,但是裡面卻使用了String、System、PrintStream及Object等結構。
- 如果不使用常量池,就需要将用到的類資訊、方法資訊等記錄在目前的位元組碼檔案中,造成檔案臃腫
- 是以我們将所需用到的結構資訊記錄在常量池中,并通過引用的方式,來加載、調用所需的結構
- 這裡的代碼量其實很少了,如果代碼多的話,引用的結構将會更多,這裡就需要用到常量池了。
常量池中有啥?
- 數量值
- 字元串值
- 類引用
- 字段引用
- 方法引用
常量池代碼舉例
- 代碼
/** * 測試方法區的内部構成 * * @author shkstart [email protected] * @create 2020 23:39 */public class MethodInnerStrucTest extends Object implements Comparable<String>, Serializable { //屬性 public int num = 10; private static String str = "測試方法的内部結構"; //構造器沒寫 //方法 public void test1() { int count = 20; System.out.println("count = " + count); } public static int test2(int cal) { int result = 0; try { int value = 30; result = value / cal; } catch (Exception e) { e.printStackTrace(); } return result; } @Override public int compareTo(String o) { return 0; }}
複制
- 來看下最簡單的 test1() 方法,帶 # 的位元組碼指令,就使用了常量池的引用
- 通過位元組碼指令可以看出,拼接字元串時,編譯器幫我們造了個 StringBuilder 對象,然後調用其 append() 方法完成了字元串的拼接
public void test1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=2, args_size=1 0: bipush 20 2: istore_1 3: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 6: new #4 // class java/lang/StringBuilder 9: dup 10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V 13: ldc #6 // String count = 15: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 18: iload_1 19: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 22: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 25: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 28: return LineNumberTable: line 20: 0 line 21: 3 line 22: 28 LocalVariableTable: Start Length Slot Name Signature 0 29 0 this Lcom/atguigu/java/MethodInnerStrucTest; 3 26 1 count I
複制
- 常量池
Constant pool: #1 = Methodref #18.#52 // java/lang/Object."<init>":()V #2 = Fieldref #17.#53 // com/atguigu/java/MethodInnerStrucTest.num:I #3 = Fieldref #54.#55 // java/lang/System.out:Ljava/io/PrintStream; #4 = Class #56 // java/lang/StringBuilder #5 = Methodref #4.#52 // java/lang/StringBuilder."<init>":()V #6 = String #57 // count = #7 = Methodref #4.#58 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #8 = Methodref #4.#59 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; #9 = Methodref #4.#60 // java/lang/StringBuilder.toString:()Ljava/lang/String; #10 = Methodref #61.#62 // java/io/PrintStream.println:(Ljava/lang/String;)V #11 = Class #63 // java/lang/Exception #12 = Methodref #11.#64 // java/lang/Exception.printStackTrace:()V #13 = Class #65 // java/lang/String #14 = Methodref #17.#66 // com/atguigu/java/MethodInnerStrucTest.compareTo:(Ljava/lang/String;)I #15 = String #67 // 測試方法的内部結構 #16 = Fieldref #17.#68 // com/atguigu/java/MethodInnerStrucTest.str:Ljava/lang/String; #17 = Class #69 // com/atguigu/java/MethodInnerStrucTest #18 = Class #70 // java/lang/Object #19 = Class #71 // java/lang/Comparable #20 = Class #72 // java/io/Serializable #21 = Utf8 num #22 = Utf8 I #23 = Utf8 str #24 = Utf8 Ljava/lang/String; #25 = Utf8 <init> #26 = Utf8 ()V #27 = Utf8 Code #28 = Utf8 LineNumberTable #29 = Utf8 LocalVariableTable #30 = Utf8 this #31 = Utf8 Lcom/atguigu/java/MethodInnerStrucTest; #32 = Utf8 test1 #33 = Utf8 count #34 = Utf8 test2 #35 = Utf8 (I)I #36 = Utf8 value #37 = Utf8 e #38 = Utf8 Ljava/lang/Exception; #39 = Utf8 cal #40 = Utf8 result #41 = Utf8 StackMapTable #42 = Class #63 // java/lang/Exception #43 = Utf8 compareTo #44 = Utf8 (Ljava/lang/String;)I #45 = Utf8 o #46 = Utf8 (Ljava/lang/Object;)I #47 = Utf8 <clinit> #48 = Utf8 Signature #49 = Utf8 Ljava/lang/Object;Ljava/lang/Comparable<Ljava/lang/String;>;Ljava/io/Serializable; #50 = Utf8 SourceFile #51 = Utf8 MethodInnerStrucTest.java #52 = NameAndType #25:#26 // "<init>":()V #53 = NameAndType #21:#22 // num:I #54 = Class #73 // java/lang/System #55 = NameAndType #74:#75 // out:Ljava/io/PrintStream; #56 = Utf8 java/lang/StringBuilder #57 = Utf8 count = #58 = NameAndType #76:#77 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #59 = NameAndType #76:#78 // append:(I)Ljava/lang/StringBuilder; #60 = NameAndType #79:#80 // toString:()Ljava/lang/String; #61 = Class #81 // java/io/PrintStream #62 = NameAndType #82:#83 // println:(Ljava/lang/String;)V #63 = Utf8 java/lang/Exception #64 = NameAndType #84:#26 // printStackTrace:()V #65 = Utf8 java/lang/String #66 = NameAndType #43:#44 // compareTo:(Ljava/lang/String;)I #67 = Utf8 測試方法的内部結構 #68 = NameAndType #23:#24 // str:Ljava/lang/String; #69 = Utf8 com/atguigu/java/MethodInnerStrucTest #70 = Utf8 java/lang/Object #71 = Utf8 java/lang/Comparable #72 = Utf8 java/io/Serializable #73 = Utf8 java/lang/System #74 = Utf8 out #75 = Utf8 Ljava/io/PrintStream; #76 = Utf8 append #77 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #78 = Utf8 (I)Ljava/lang/StringBuilder; #79 = Utf8 toString #80 = Utf8 ()Ljava/lang/String; #81 = Utf8 java/io/PrintStream #82 = Utf8 println #83 = Utf8 (Ljava/lang/String;)V #84 = Utf8 printStackTrace
複制
常量池總結
常量池、可以看做是一張表,虛拟機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量等類型
運作時常量池
- 運作時常量池(Runtime Constant Pool)是方法區的一部分。
- 常量池表(Constant Pool Table)是Class位元組碼檔案的一部分,用于存放編譯期生成的各種字面量與符号引用,這部分内容将在類加載後存放到方法區的運作時常量池中。
- 運作時常量池,在加載類和接口到虛拟機後,就會建立對應的運作時常量池。
- JVM為每個已加載的類型(類或接口)都維護一個常量池。池中的資料項像數組項一樣,是通過索引通路的。
- 運作時常量池中包含多種不同的常量,包括編譯期就已經明确的數值字面量,也包括到運作期解析後才能夠獲得的方法或者字段引用。此時不再是常量池中的符号位址了,這裡換為真實位址。
- 運作時常量池,相對于Class檔案常量池的另一重要特征是:具備動态性。
- 運作時常量池類似于傳統程式設計語言中的符号表(symbol table),但是它所包含的資料卻比符号表要更加豐富一些。
- 當建立類或接口的運作時常量池時,如果構造運作時常量池所需的記憶體空間超過了方法區所能提供的最大值,則JVM會抛OutofMemoryError異常。
5、方法區的使用舉例
方法區圖解示例
- 代碼
/** * @author shkstart [email protected] * @create 2020 14:28 */public class MethodAreaDemo { public static void main(String[] args) { int x = 500; int y = 100; int a = x / y; int b = 50; System.out.println(a + b); }}
複制
圖解位元組碼指令執行流程
- 位元組碼執行過程展示:初始狀态
- 首先将操作數500壓入操作數棧中
- 然後操作數 500 從操作數棧中取出,存儲到局部變量表中索引為 1 的位置
- 然後操作數 100 從操作數棧中取出,存儲到局部變量表中索引為 2 的位置
- 将操作數 100 從
- 讀取本地變量 1 ,壓入操作數棧
- 讀取本地變量 2 ,壓入操作數棧
- 兩數相除,計算結果放在操作數棧頂,之後執行 istore_3 指令,将計算結果從操作數棧中彈出,存入局部變量 3 中
- 将操作數 50 壓入操作數棧
- 将操作數 50 從棧頂彈出,儲存在局部變量 4 中
- 擷取 System.out 輸出流的引用(我不太确定)
- 将本地變量 3 的值取出,壓入操作數棧中,準備進行加法運算
- 執行加法運算後,将計算結果放在操作數棧頂
- 調用靜态方法 println() ,輸出加法結果
- main() 方法執行結束
關于【符号引用 --> 直接飲用】的了解
- 上面代碼調用 System.out.println() 方法時,首先需要看看 System 類有沒有加載,再看看 PrintStream 類有沒有加載
- 如果沒有加載,則執行加載,執行時,将常量池中的符号引用(字面量)轉換為直接引用(真正的位址值)
關于程式計數器的說明
程式計數器始終計算的都是目前代碼運作的位置,目的是為了友善記錄方法調用後能夠正常傳回,或者是進行了CPU切換後,也能回來到原來的代碼進行執行。
6、方法區演進細節
6.1、永久代演進過程
關于永久代的說明
- 首先明确:隻有Hotspot才有永久代。
- BEA JRockit、IBMJ9等來說,是不存在永久代的概念的。原則上如何實作方法區屬于虛拟機實作細節,不受《Java虛拟機規範》管束,并不要求統一
- Hotspot中方法區的變化:
JDK 版本 | 演變細節 |
---|---|
JDK1.6及以前 | 有永久代(permanent generation),靜态變量存儲在永久代上 |
JDK1.7 | 有永久代,但已經逐漸 “去永久代”,字元串常量池,靜态變量移除,儲存在堆中 |
JDK1.8 | 無永久代,類型資訊,字段,方法,常量儲存在本地記憶體的元空間,但字元串常量池、靜态變量仍然在堆中。 |
JDK6
方法區由永久代實作,使用 JVM 虛拟機記憶體
JDK7
方法區由永久代實作,使用 JVM 虛拟機記憶體
JDK8
方法區由元空間實作,使用實體機本地記憶體
6.2、元空間出現原因
永久代為什麼要被元空間替代?
官方文檔
http://openjdk.java.net/jeps/122
- 官方的牽強解釋:JRockit是和HotSpot融合後的結果,因為JRockit沒有永久代,是以他們不需要配置永久代
- 随着Java8的到來,HotSpot VM中再也見不到永久代了。但是這并不意味着類的中繼資料資訊也消失了。這些資料被移到了一個與堆不相連的本地記憶體區域,這個區域叫做元空間(Metaspace)。
由于類的中繼資料配置設定在本地記憶體中,元空間的最大可配置設定空間就是系統可用記憶體空間,這項改動是很有必要的,原因有:
- 為永久代設定空間大小是很難确定的。
-
在某些場景下,如果動态加載類過多,容易産生Perm區的OOM。比如某個實際Web工
程中,因為功能點比較多,在運作過程中,要不斷動态加載很多類,經常出現緻命錯誤。
Exception in thread 'dubbo client x.x connector' java.lang.OutOfMemoryError:PermGen space
-
而元空間和永久代之間最大的差別在于:元空間并不在虛拟機中,而是使用本地記憶體。
是以,預設情況下,元空間的大小僅受本地記憶體限制。
-
- 對永久代進行調優是很困難的。
- 方法區的垃圾收集主要回收兩部分内容:常量池中廢棄的常量和不再用的類型,方法區的調優主要是為了降低Full GC
- 有些人認為方法區(如HotSpot虛拟機中的元空間或者永久代)是沒有垃圾收集行為的,其實不然。《Java虛拟機規範》對方法區的限制是非常寬松的,提到過可以不要求虛拟機在方法區中實作垃圾收集。事實上也确實有未實作或未能完整實作方法區類型解除安裝的收集器存在(如JDK11時期的ZGC收集器就不支援類解除安裝)。
- 一般來說這個區域的回收效果比較難令人滿意,尤其是類型的解除安裝,條件相當苛刻。但是這部分區域的回收有時又确實是必要的。以前Sun公司的Bug清單中,曾出現過的若幹個嚴重的Bug就是由于低版本的HotSpot虛拟機對此區域未完全回收而導緻記憶體洩漏
6.3、字元串常量池
字元串常量池 StringTable 為什麼要調整位置?
- JDK7中将StringTable放到了堆空間中。因為永久代的回收效率很低,在Full GC的時候才會執行永久代的垃圾回收,而Full GC是老年代的空間不足、永久代不足時才會觸發。
- 這就導緻StringTable回收效率不高,而我們開發中會有大量的字元串被建立,回收效率低,導緻永久代記憶體不足。放到堆裡,能及時回收記憶體。
6.4、靜态變量位置
靜态變量存放在那裡?
代碼示例 1
- 代碼
/** * 結論: * 靜态變量在jdk6/7存在與永久代中,在jdk8存在于堆中 * 靜态引用對應的對象實體始終都存在堆空間 * * jdk7: * -Xms200m -Xmx200m -XX:PermSize=300m -XX:MaxPermSize=300m -XX:+PrintGCDetails * jdk 8: * -Xms200m -Xmx200m -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails * @author shkstart [email protected] * @create 2020 21:20 */public class StaticFieldTest { private static byte[] arr = new byte[1024 * 1024 * 100];//100MB public static void main(String[] args) { System.out.println(StaticFieldTest.arr); }}
複制
- 運作環境 JDK8 ,JVM 參數
-Xms200m -Xmx200m -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails
複制
- 通過 GC 日志可以看出:靜态變量引用對應的對象實體始終都存在堆空間(arr 數組對象直接怼到老年區去了)
[B@4554617cHeap PSYoungGen total 59904K, used 5171K [0x00000000fbd80000, 0x0000000100000000, 0x0000000100000000) eden space 51712K, 10% used [0x00000000fbd80000,0x00000000fc28ceb0,0x00000000ff000000) from space 8192K, 0% used [0x00000000ff800000,0x00000000ff800000,0x0000000100000000) to space 8192K, 0% used [0x00000000ff000000,0x00000000ff000000,0x00000000ff800000) ParOldGen total 136704K, used 102400K [0x00000000f3800000, 0x00000000fbd80000, 0x00000000fbd80000) object space 136704K, 74% used [0x00000000f3800000,0x00000000f9c00010,0x00000000fbd80000) Metaspace used 3473K, capacity 4496K, committed 4864K, reserved 1056768K class space used 381K, capacity 388K, committed 512K, reserved 1048576K
複制
代碼示例 2
- 代碼
/** * 《深入了解Java虛拟機》中的案例: * staticObj、instanceObj、localObj存放在哪裡? * * @author shkstart [email protected] * @create 2020 11:39 */public class StaticObjTest { static class Test { static ObjectHolder staticObj = new ObjectHolder(); ObjectHolder instanceObj = new ObjectHolder(); void foo() { ObjectHolder localObj = new ObjectHolder(); System.out.println("done"); } } private static class ObjectHolder { } public static void main(String[] args) { Test test = new StaticObjTest.Test(); test.foo(); }}
複制
- 可以使用 JHSDB.exe,在JDK9的時候才引入的
- 分析:staticObj随着Test的類型資訊存放在方法區,instanceObj随着Test的對象執行個體存放在Java堆,localObject則是存放在foo()方法棧幀的局部變量表中。
- 測試發現:三個對象的資料在記憶體中的位址都落在Eden區範圍内,是以結論:隻要是對象執行個體必然會在Java堆中配置設定。
- 接着,找到了一個引用該staticObj對象的地方,是在一個java.lang.Class的執行個體裡,并且給出了這個執行個體的位址,通過Inspector檢視該對象執行個體,可以清楚看到這确實是一個java.lang.Class類型的對象執行個體,裡面有一個名為staticobj的執行個體字段:
- 從《Java虛拟機規範》所定義的概念模型來看,所有Class相關的資訊都應該存放在方法區之中,但方法區該如何實作,《Java虛拟機規範》并未做出規定,這就成了一件允許不同虛拟機自己靈活把握的事情。JDK7及其以後版本的HotSpot虛拟機選擇把靜态變量與類型在Java語言一端的映射Class對象存放在一起,存儲于Java堆之中,從我們的實驗中也明确驗證了這一點
7、方法區的垃圾回收
方法區垃圾收集
- 有些人認為方法區(如Hotspot虛拟機中的元空間或者永久代)是沒有垃圾收集行為的,其實不然。
- 《Java虛拟機規範》對方法區的限制是非常寬松的,提到過可以不要求虛拟機在方法區中實作垃圾收集。事實上也确實有未實作或未能完整實作方法區類型解除安裝的收集器存在(如JDK11時期的ZGC收集器就不支援類解除安裝)。
- 一般來說這個區域的回收效果比較難令人滿意,尤其是類型的解除安裝,條件相當苛刻。但是這部分區域的回收有時又确實是必要的。以前sun公司的Bug清單中,曾出現過的若幹個嚴重的Bug就是由于低版本的HotSpot虛拟機對此區域未完全回收而導緻記憶體洩漏。
- 方法區的垃圾收集主要回收兩部分内容:常量池中廢棄的常量和不再使用的類型。
方法區常量的回收
- 先來說說方法區内常量池之中主要存放的兩大類常量:字面量和符号引用
- 字面量比較接近Java語言層次的常量概念,如文本字元串、被聲明為final的常量值等
- 而符号引用則屬于編譯原理方面的概念,包括下面三類常量:
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
- HotSpot虛拟機對常量池的回收政策是很明确的,隻要常量池中的常量沒有被任何地方引用,就可以被回收。
- 回收廢棄常量與回收Java堆中的對象非常類似。(關于常量的回收比較簡單,重點是類的回收)
方法區類的回收
判定一個常量是否“廢棄”還是相對簡單,而要判定一個類型是否屬于“不再被使用的類”的條件就比較苛刻了。需要同時滿足下面三個條件:
- 該類所有的執行個體都已經被回收,也就是Java堆中不存在該類及其任何派生子類的執行個體。
- 加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,如OSGi、JSP的重加載等,否則通常是很難達成的。
- 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射通路該類的方法。
Java虛拟機被允許對滿足上述三個條件的無用類進行回收,這裡說的僅僅是“被允許”,而并不是和對象一樣,沒有引用了就必然會回收。關于是否要對類型進行回收,HotSpot虛拟機提供了
-Xnoclassgc
參數進行控制,還可以使用
-verbose:class
以及
-XX:+TraceClass-Loading
、
-XX:+TraceClassUnLoading
檢視類加載和解除安裝資訊
在大量使用反射、動态代理、CGLib等位元組碼架構,動态生成JSP以及OSGi這類頻繁自定義類加載器的場景中,通常都需要Java虛拟機具備類型解除安裝的能力,以保證不會對方法區造成過大的記憶體壓力。
8、運作時資料區總結
- 線程私有結構:程式計數器、虛拟機棧、本地方法棧
- 每個虛拟機棧由由具體的棧幀組成,在棧幀的動态連結中,儲存至對方法的引用
- 方法區在 JDK7 之前,使用永久代實作,在 JDK8 之後,使用元空間實作
- Minor GC 針對于新生區,Major GC 針對于老年區,Full GC 針對于整個堆空間和方法區
9、大場面試題
- 百度
- 三面:說一下JVM記憶體模型吧,有哪些區?分别幹什麼的?
- 螞蟻金服:
- Java8的記憶體分代改進
- JVM記憶體分哪幾個區,每個區的作用是什麼?
- 一面:JVM記憶體分布/記憶體結構?棧和堆的差別?堆的結構?為什麼兩個survivor區?
- 二面:Eden和survior的比例配置設定
- 小米:
- jvm記憶體分區,為什麼要有新生代和老年代
- 位元組跳動:
- 二面:Java的記憶體分區
- 二面:講講vm運作時資料庫區
- 什麼時候對象會進入老年代?
- 京東:
- JVM的記憶體結構,Eden和Survivor比例。
- JVM記憶體為什麼要分成新生代,老年代,持久代。新生代中為什麼要分為Eden和survivor。
- 天貓:
- 一面:Jvm記憶體模型以及分區,需要詳細到每個區放什麼。
- 一面:JVM的記憶體模型,Java8做了什麼改
- 拼多多:
- JVM記憶體分哪幾個區,每個區的作用是什麼?
- 美團:
- java記憶體配置設定
- jvm的永久代中會發生垃圾回收嗎?
- 一面:jvm記憶體分區,為什麼要有新生代和老年代?