5.1 方法區的了解
5.1.1 方法區、堆、棧的互動關系
User user = new User()
- User 就是方法區,存儲類的資訊
- user 就是棧,存儲引用
- new User()就是堆,配置設定對象空間
5.1.2 概述
方法區相當于接口,jdk7中的實作被稱為永久代,jdk8中的實作被稱為元空間
方法區邏輯上屬于堆的一部分,但是實際情況中可以把堆和方法區區分開,方法區又稱之為 非堆(non-Heap),而且實際上堆的jvm參數設定大小也并不包括方法區
- 方法區與堆類似,都屬于多線程共享的
- 方法區在jvm啟動時建立,同樣的與堆類似,在實體上可以是不連續的
- 方法區的大小與堆類似,可以設定為固定大小/動态擴充
- 方法區大小決定了系統可以建立多少類,如果類太多,方法區就會報 OOM: Jdk7 PermGen space jdk8 MetaSpace
- 關閉jvm就會銷毀方法區,釋放方法區的記憶體
5.1.3 方法區的演變
jdk7 方法區被稱為永久代
jdk8 方法區被稱為元空間
- 嚴格來說:方法區≠永久代,僅僅對hotspot來說有永久代的概念,J9/JRocket中都不存在永久代的概念
- 從現在看,永久代并不是一個好的概念,導緻了大量的OOM jvm參數 -XX:MaxPermSize設定永久代最大空間
- jdk8以後廢棄永久代的概念,改用本地記憶體實作的元空間代替
- 永久代與元空間的本質類似,都是對方法區的實作,但是元空間不再使用虛拟機設定的記憶體,而是改用本地記憶體
- 元空間的内部結構也發生變化
- 元空間也有可能出現OOM(超出本地記憶體大小)
5.1.4 方法區大小設定與OOM
jdk1.7
- -XX:PermSize 設定永久代初始記憶體 預設20.75M
- -XX:MaxPermSize 設定永久代最大記憶體,32位機器預設64M, 64位機器預設82M
- 超過 -XX:MaxPermSize 的大小 就會報OOM
jdk1.8及以後
- 使用 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize替換上述兩個參數,并且上述兩個參數在jdk1.8中已廢棄
- 預設值與平台相關 windows下 -XX:MetaspaceSize為21M -XX:MaxMetaspaceSize無限制(-1)
- 與永久代不同,不指定大小的情況下,元空間會耗盡所有系統可用記憶體,如果依然發生了記憶體溢出,就會報OOM
- -XX:MetaspaceSize 預設21M,如果超過此水位線,就會觸發FullGC,然後解除安裝一些不用的類(類對應的類加載器不在存活),然後這個水位線就會重置,新的水位線大小取決于FullGC釋放的大小,如果釋放的不足,那麼在不超過MaxMetaspaceSize的前提下,會适當提高水位線;相反,如果釋放比較多的空間,那麼就會适當降低水位線
- 如果初始化的水位線設定過低,那麼在程式運作過程中可能就會觸發多次FullGC調整水位線,為了避免這種情況,可以适當的把水位線-XX:MetaspaceSize調高
package com.zy.study10;
/**
* @Author: Zy
* @Date: 2021/8/30 23:04
* 測試調整jdk1.8的元空間大小
* -XX:MetaspaceSize=50M
* -XX:MaxMetaspaceSize=100M
*/
public class MethadAreaTest {
public static void main(String[] args) throws InterruptedException {
System.out.println("start");
Thread.sleep(1000000);
}
}
通過jps和jinfo檢視jvm參數
5.2 方法區的結構
方法區主要存儲類型資訊,常量,靜态變量,JIT編譯後的代碼緩存,域資訊,方法資訊等.
5.2.1 類型資訊
類,接口,枚舉,注解等類型必須存儲的資訊:
- 完整有效名稱(完整包名.類名)
- 直接父類的完整有效名稱, 接口/Object沒有父類
- 類型的修飾符
- 直接接口的一個有序清單
5.2.2 域資訊(Field)
儲存類型的所有域的資訊和域的聲明順序
域的相關資訊包括:
- 域名稱
- 域類型
- 域修飾符(public private protected static final volatile transient的某個子集)
5.2.3 方法資訊(Method)
儲存類型中的所有方法的資訊
方法的相關資訊包括:
- 方法名稱
- 方法的傳回值(包括void)
- 方法的參數和類型(按順序)
- 方法的修飾符(public private protected static final volatile transient的某個子集)
- 方法的位元組碼(Bytes),操作數棧和局部變量表及大小(abstract和native方法除外)
- 異常表 (abstract和native方法除外)
-
- 每個異常處理的開始位置,結束位置,代碼處理在程式計數器的中偏移位址,被捕獲的異常類在常量池中的索引
* @Date: 2021/8/31 9:56
* 測試方法區反編譯效果
public class MethodStructureTest {
public int num = 0;
private String str = "測試内部結構";
public void test1(){
System.out.println("num"+num);
System.out.println(str);
public void test2(){
int i = 0;
int j = 0;
try {
int k = i/j;
} catch (Exception e) {
e.printStackTrace();
}
可以反編譯後檢視class檔案,進而看到方法區的具體結構:
Classfile /E:/張堯/idea項目/jvm/target/classes/com/zy/study10/MethodStructureTest.class
Last modified 2021-8-31; size 1137 bytes
MD5 checksum 0de91cc4cb8d502aa743a5e961bcbfec
Compiled from "MethodStructureTest.java"
public class com.zy.study10.MethodStructureTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #16.#39 // java/lang/Object."<init>":()V
#2 = Fieldref #15.#40 // com/zy/study10/MethodStructureTest.num:I
#3 = String #41 // 測試内部結構
#4 = Fieldref #15.#42 // com/zy/study10/MethodStructureTest.str:Ljava/lang/String;
#5 = Fieldref #43.#44 // java/lang/System.out:Ljava/io/PrintStream;
#6 = Class #45 // java/lang/StringBuilder
#7 = Methodref #6.#39 // java/lang/StringBuilder."<init>":()V
#8 = String #17 // num
#9 = Methodref #6.#46 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#10 = Methodref #6.#47 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#11 = Methodref #6.#48 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#12 = Methodref #49.#50 // java/io/PrintStream.println:(Ljava/lang/String;)V
#13 = Class #51 // java/lang/Exception
#14 = Methodref #13.#52 // java/lang/Exception.printStackTrace:()V
#15 = Class #53 // com/zy/study10/MethodStructureTest
#16 = Class #54 // java/lang/Object
#17 = Utf8 num
#18 = Utf8 I
#19 = Utf8 str
#20 = Utf8 Ljava/lang/String;
#21 = Utf8 <init>
#22 = Utf8 ()V
#23 = Utf8 Code
#24 = Utf8 LineNumberTable
#25 = Utf8 LocalVariableTable
#26 = Utf8 this
#27 = Utf8 Lcom/zy/study10/MethodStructureTest;
#28 = Utf8 test1
#29 = Utf8 test2
#30 = Utf8 e
#31 = Utf8 Ljava/lang/Exception;
#32 = Utf8 i
#33 = Utf8 j
#34 = Utf8 StackMapTable
#35 = Class #53 // com/zy/study10/MethodStructureTest
#36 = Class #51 // java/lang/Exception
#37 = Utf8 SourceFile
#38 = Utf8 MethodStructureTest.java
#39 = NameAndType #21:#22 // "<init>":()V
#40 = NameAndType #17:#18 // num:I
#41 = Utf8 測試内部結構
#42 = NameAndType #19:#20 // str:Ljava/lang/String;
#43 = Class #55 // java/lang/System
#44 = NameAndType #56:#57 // out:Ljava/io/PrintStream;
#45 = Utf8 java/lang/StringBuilder
#46 = NameAndType #58:#59 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#47 = NameAndType #58:#60 // append:(I)Ljava/lang/StringBuilder;
#48 = NameAndType #61:#62 // toString:()Ljava/lang/String;
#49 = Class #63 // java/io/PrintStream
#50 = NameAndType #64:#65 // println:(Ljava/lang/String;)V
#51 = Utf8 java/lang/Exception
#52 = NameAndType #66:#22 // printStackTrace:()V
#53 = Utf8 com/zy/study10/MethodStructureTest
#54 = Utf8 java/lang/Object
#55 = Utf8 java/lang/System
#56 = Utf8 out
#57 = Utf8 Ljava/io/PrintStream;
#58 = Utf8 append
#59 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#60 = Utf8 (I)Ljava/lang/StringBuilder;
#61 = Utf8 toString
#62 = Utf8 ()Ljava/lang/String;
#63 = Utf8 java/io/PrintStream
#64 = Utf8 println
#65 = Utf8 (Ljava/lang/String;)V
#66 = Utf8 printStackTrace
{
public int num;
descriptor: I
flags: ACC_PUBLIC
private java.lang.String str;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE
public com.zy.study10.MethodStructureTest();
descriptor: ()V
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field num:I
9: aload_0
10: ldc #3 // String 測試内部結構
12: putfield #4 // Field str:Ljava/lang/String;
15: return
LineNumberTable:
line 8: 0
line 9: 4
line 10: 9
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this Lcom/zy/study10/MethodStructureTest;
public void test1();
stack=3, locals=1, args_size=1
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #6 // class java/lang/StringBuilder
6: dup
7: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
10: ldc #8 // String num
12: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: aload_0
16: getfield #2 // Field num:I
19: invokevirtual #10 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
22: invokevirtual #11 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
28: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
31: aload_0
32: getfield #4 // Field str:Ljava/lang/String;
35: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
38: return
line 14: 0
line 15: 28
line 16: 38
0 39 0 this Lcom/zy/study10/MethodStructureTest;
public void test2();
stack=2, locals=4, args_size=1
0: iconst_0
1: istore_1
2: iconst_0
3: istore_2
4: iload_1
5: iload_2
6: idiv
7: istore_3
8: goto 16
11: astore_3
12: aload_3
13: invokevirtual #14 // Method java/lang/Exception.printStackTrace:()V
16: return
Exception table:
from to target type
4 8 11 Class java/lang/Exception
line 19: 0
line 20: 2
line 23: 4
line 26: 8
line 24: 11
line 25: 12
line 27: 16
12 4 3 e Ljava/lang/Exception;
0 17 0 this Lcom/zy/study10/MethodStructureTest;
2 15 1 i I
4 13 2 j I
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 11
locals = [ class com/zy/study10/MethodStructureTest, int, int ]
stack = [ class java/lang/Exception ]
frame_type = 4 /* same */
SourceFile: "MethodStructureTest.java"
5.2.4 運作時常量池
- 位元組碼中包含了常量池
- 方法中包含了運作時常量池,運作時常量池就是将位元組碼中的常量池加載到記憶體中
常量池:
- 常量池中的類型包括: 數量值,字元串值,類引用,字段引用,方法引用
- 常量池可以看作一張表,存儲了類名,方法名,字段名,參數類型等等資訊,虛拟機在執行代碼的時候根據指令找到對應的常量池索引,進而使用
- 常量池内部是嵌套的
運作時常量池:
- 運作時常量池是方法區的一部分
- 運作時常量池是在類和接口等類型加載到jvm的時候建立的,将常量池中的字面量和符号引用存儲到運作時常量池,并将符号引用轉換為直接引用
- 運作時常量池具有動态性,即可以在運作時動态添加
- 運作時常量池是通過索引通路的,常量池的容量要比實際存儲數量大1
- 當運作時常量池超過方法區的最大記憶體時,會OOM
5.3 方法區的演變過程
從jdk6,jdk7,jdk8看方法區的變化過程
jdk6 | 永久代,靜态變量/常量池存放在永久代中 |
jdk7 | 永久代,逐漸去除永久代,靜态變量/常量池移到了堆中 |
jdk8 | 元空間,但是靜态變量/常量池還是在堆中 |
為什麼要用元空間替代永久代?
oracle官網的解釋是,JRocket/J9都使用了元空間,并且Oracle已經收購了JRocket,是以就使用了元空間.
從調優上了解:
- 永久代大小難以設定,如果設定過大,比較浪費虛拟機記憶體,如果設定過小,又會觸發多次FullGC,影響性能
- 永久代難以調優,永久代正常情況下很少GC,難以控制調優
相比之下元空間使用本地記憶體,能用多大就用多大,也不用jvm考慮GC問題,提高了性能
從上述演變過程解釋一下為什麼StringTable要移動到堆?
因為開發過程中建立大量的字元串,這些字元串如果都放到永久代中,由于永久代的GC效率不是很高,隻要當老年代/永久代空間不足時才會觸發FullGC然後回收永久代中,這樣就導緻了大量的字元串不會被回收,可能會導緻永久代空間不足,移動到堆之後,就可以通過YGC快速回收.
注意: 靜态變量存放在堆中指的是變量引用本身,而不是對象本身,建立的對象都是存放在堆中的,而引用本身則是随着jdk版本的不同存放的地方也不同,jdk6中存放在永久代,7/8存放在堆中
5.4 方法區的垃圾回收
java虛拟機規範中并沒有強制要求jvm要對方法區進行回收,針對HotSpot來說,還是有回收的:
方法區的垃圾回收主要針對:
- 運作時常量池中不再使用的常量
- 不再使用的類型資訊
5.4.1 常量的回收規則
常量主要指的是字面量和符号引用
主要包括:
- 類和接口的全限定名(完整包名+類名)
- 字段的名稱和描述符
- 方法的名稱和描述符
在hotspot中,隻要沒有地方引用該常量,就将該常量回收,類似對堆中對象的回收
5.4.2 類型的回收
判斷一個類是否不再使用,需要同時滿足如下關系:
- 該類的所有執行個體都被回收,堆中不存在該類及其派生子類的執行個體
- 該類的類加載器被回收
- 該類的Class對象沒有任何地方引用,無法通過反射通路該對象
滿足上述條件後,才可以允許該類被回收,但是并不一定回收.
jvm參數: -Xnoclassgc控制是否回收類
jvm參數: -verbose:class -XX:+TraceClass-Loading -XX:TraceClassUnloading可以檢視類的加載和解除安裝資訊.
總結:
方法區的回收主要針對常量,對于類的回收條件比較苛刻,一般情況下不會回收類資訊,特殊情況下如果使用大量的動态生成,反射的情況下,需要考慮類的回收.