1. 程式人生 > 實用技巧 >JVM--方法區(元空間、永久代)

JVM--方法區(元空間、永久代)

目錄

五、方法區

執行時資料區結構

堆、棧、方法去的互動關係

1、介紹:

《Java虛擬機器規範》中明確說明:“儘管所有的方法區在邏輯上是屬於堆的一部分,但一些簡單的實現可能不會選擇去進行垃圾收集或者進行壓縮。”但對於HotSpotJVM而言,方法區還有一個別名叫做Non-Heap(非堆),目的就是要和堆分開。所以,方法區看作是一塊獨立於Java堆的記憶體空間

  • 方法區(Method Area)與Java堆一樣,是各個執行緒共享的記憶體區域。
  • 方法區在JVM啟動的時候被建立,並且它的實際的實體記憶體空間中和Java堆區一樣都可以是不連續的。
  • 方法區的大小,跟堆空間一樣,可以選擇固定大小或者可擴充套件
  • 方法區的大小決定了系統可以儲存多少個類,如果系統定義了太多的類,導致方法區溢位,虛擬機器同樣會丟擲記憶體溢位錯誤:java.lang.OutofMemoryError:PermGen space (8前)或者 java.lang.OutofMemoryError:Metaspace(8以及以後)
    • 載入過多第三方jar包;Tomcat部署專案過多;大量動態的生成反射類
  • 關閉JVM就會釋放這個區域的記憶體。

別稱:jdk7及以前(永久代),jdk8及以後(元空間)

演變

元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代最大的區別在於:元空間不在虛擬機器設定的記憶體中,而是使用本地記憶體

永久代、元空間二者並不只是名字變了,內部結構也調整了。

2、設定方法區記憶體大小

  • 元資料區大小可以使用引數-XX:MetaspaceSize-XX:MaxMetaspaceSize指定,替代上述原有的兩個引數。
  • 預設值依賴於平臺。windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,即沒有限制
  • 與永久代不同,如果不指定大小,預設情況下,虛擬機器會耗盡所有的可用系統記憶體。如果元資料區發生溢位,虛擬機器一樣會丟擲異常OutOfMemoryError:Metaspace
  • -XX:MetaspaceSize:設定初始的元空間大小。對於一個64位的伺服器端JVM來說,其預設的XX:MetaspaceSize值為21MB。這就是初始的高水位線,一旦觸及這個水位線,Full GC將會被觸發並解除安裝沒用的類(即這些類對應的類載入器不再存活)然後這個高水位線將會重置。新的高水位線的值取決於GC後釋放了多少元空間。如果釋放的空間不足,那麼在不超過MaxMetaspaceSize時,適當提高該值。如果釋放空間過多,則適當降低該值。
  • 如果初始化的高水位線設定過低,上述高水位線調整情況會發生很多次。通過垃圾回收器的日誌可以觀察到Fu11 GC多次呼叫。為了避免頻繁地GC,建議將-XX:MetaspaceSize設定為一個相對較高的值。

如何解決這些OOM?

1、要解決OOM異常或heap space的異常,一般的手段是首先通過記憶體映像分析工具(如Eclipse Memory Analyzer)對dump出來的堆轉儲快照進行分析,重點是確認記憶體中的物件是否是必要的,也就是要先分清楚到底是出現了記憶體洩漏(MemoryLeak)還是記憶體溢位(Memory Overflow)。

2、如果是記憶體洩漏,可進一步通過工具檢視洩漏物件到GC Roots的引用鏈。於是就能找到洩漏物件是通過怎樣的路徑與GC Roots相關聯並導致垃圾收集器無法自動回收它們的。掌握了洩漏物件的型別資訊,以及GC Roots引用鏈的資訊,就可以比較準確地定位出洩漏程式碼的位置。

3、如果不存在記憶體洩漏,換句話說就是記憶體中的物件確實都還必須存活著,那就應當檢查虛擬機器的堆引數(-Xmx與-Xms),與機器實體記憶體對比看是否還可以調大,從程式碼上檢查是否存在某些物件生命週期過長、持有狀態時間過長的情況,嘗試減少程式執行期的記憶體消耗

3、方法區記憶體結構

3.1、方法區所儲存的內容:

1、型別資訊

對每個載入的型別(類class、介面interface、列舉enum、註解annotation),JVM在方法區中儲存以下型別資訊:
①這個型別的完整有效名稱(全名=包名.類名
②這個型別直接父類的完整有效名(對於interface或是java.lang.object,都沒有父類)
③這個型別的修飾符(public,abstract,final的某個子集)
④這個型別直接介面的一個有序列表

2、域資訊

JVM必須在方法區中儲存型別的所有域的相關資訊以及域的宣告順序。

域的相關資訊包括:域名稱、域型別、域修飾符(public,private,protected, static, final, volatile, transient的某個子集)

3、方法資訊

JVM必須儲存所有方法的以下資訊,同域資訊一樣包括宣告順序:

  1. 方法名稱
  2. 方法的返回型別(或void)
  3. 方法引數的數量和型別(按順序)
  4. 方法的修飾符(public,private,protected,static,final,synchronized,native,abstract的一個子集)
  5. 方法的位元組碼(bytecodes)、運算元棧、區域性變量表及大小(abstract和native方法除外)
  6. 異常表(abstract和native方法除外):每個異常處理的開始位置、結束位置、程式碼處理在程式計數器中的偏移地址、被捕獲的異常類的常量池索引

檢視 命令列輸入 javap -v -p(包含private許可權) xxx.class > xxx.txt

示例:

public class Test extends HashMap implements Serializable {
    private String name = "";
    private int x = 1;
    public Test(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        Test haha = new Test(null);
        int nameLength = haha.getNameLength();
        System.out.println(nameLength);
    }

    public int getNameLength() {
        int y = 0;
        try {
            y = name.length();
        } catch (NullPointerException e) {
            System.out.println("空指標異常");
            e.printStackTrace();
        }
        return y;
    }
}
Classfile /D:/ideaFiles/Algorithm/out/production/Algorithm/com/lx/mySort/Test.class
  Last modified 2020-7-29; size 1145 bytes
  MD5 checksum 8f9825153f3fa6f2042785c0df59703b
  Compiled from "Test.java"
//類資訊
public class com.lx.mySort.Test extends java.util.HashMap implements java.io.Serializable
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #15.#44        // java/util/HashMap."<init>":()V
   #2 = String             #45            //
 ...
{
//域資訊
  private java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE

  private int x;
    descriptor: I
    flags: ACC_PRIVATE

//方法資訊
  ...
  public int getNameLength();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: aload_0
         3: getfield      #3                  // Field name:Ljava/lang/String;
         6: invokevirtual #10                 // Method java/lang/String.length:()I
         9: istore_1
        10: goto          26
        13: astore_2
        14: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        17: ldc           #12                 // String 空指標異常
        19: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        22: aload_2
        23: invokevirtual #14                 // Method java/lang/NullPointerException.printStackTrace:()V
        26: iload_1
        27: ireturn
//異常表
      Exception table:
         from    to  target type
             2    10    13   Class java/lang/NullPointerException
      LineNumberTable:
        line 26: 0
        line 28: 2
        line 32: 10
        line 29: 13
        line 30: 14
        line 31: 22
        line 33: 26
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           14      12     2     e   Ljava/lang/NullPointerException;
            0      28     0  this   Lcom/lx/mySort/Test;
            2      26     1     y   I
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 13
          locals = [ class com/lx/mySort/Test, int ]
          stack = [ class java/lang/NullPointerException ]
        frame_type = 12 /* same */
}
SourceFile: "Test.java"
4、靜態變數
  • non-final的類變數

    static靜態變數:載入時準備階段(賦預設值)、初始化階段賦給定值

  • 全域性常量

​ static final:編譯時(準備階段)賦給定值

5、執行時常量池
常量池

常量池所在位置

一個有效的位元組碼檔案中除了包含類的版本資訊、欄位、方法以及介面等描述資訊外,還包含一項資訊那就是常量池表(Constant Pool Table),包括各種字面量和對型別方法的符號引用。

常量池的作用:

一個java原始檔中的類、介面,編譯後產生一個位元組碼檔案。而Java中的位元組碼需要資料支援,通常這種資料會很大以至於不能直接存到位元組碼裡,換另一種方式,可以存到常量池,這個位元組碼包含了指向常量池的引用。在動態連結的時候會用到執行時常量池,之前有介紹。

常量池儲存的資料

  1. 數量值
  2. 字串值
  3. 類引用
  4. 欄位引用
  5. 方法引用
執行時常量池
  • 執行時常量池(Runtime Constant Pool)是方法區的一部分
  • 常量池表(Constant Pool Table)是class檔案的一部分
  • 執行時常量池,在載入類和介面到虛擬機器後,就會建立對應的執行時常量池。
  • JVM為每個已載入的型別(類或介面)都維護一個常量池。
  • 執行時常量池中包含多種不同的常量,包括編譯期就已經明確的數值字面量,也包括到執行期解析後才能夠獲得的方法或者欄位引用。此時不再是常量池中的符號地址了,這裡換為真實地址
    • 執行時常量池,相對於class檔案常量池的另一重要特徵是:具備動態性
  • 當建立類或介面的執行時常量池時,如果構造執行時常量池所需的記憶體空間超過了方法區所能提供的最大值,則JVM會拋OutOfMemoryError異常。

4、演進過程

永久代為什麼會被元空間替換

  • 永久代空間大小很難確定,太小容易GC/OOM異常,太大佔用記憶體(元空間並不在虛擬機器中、而是使用本地記憶體,大小僅受本地記憶體限制)
  • 永久代調優困難

靜態變數放到哪裡?

堆空間裡的永久代(8及後是元空間)

5、方法區的垃圾回收

主要回收:

​ 1、常量池中廢棄的常量:字面量和符號引用...(沒用被引用,則可以進行回收)

​ 2、不再使用的型別(同時滿足以下三個條件的類可被允許回收):

​ 1)該類的所有例項都被回收了,即Java堆中不存在該類及其任何派生的子類的例項

​ 2)該類的類載入器已經被回收了(除非精心設計,否則很難實現,如OSGI,JSP的重載入等)

​ 3)該類物件對應的java.lang.Class物件沒有在任何地方引用、無法在任何地方通過反射訪問到該類的方法

關於是否要對型別進行回收,HotSpot虛擬機器提供了-Xnoclassgc引數進行控制,還可以使用-verbose:class以及
-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading檢視類載入和解除安裝資訊

5、小結

6、物件例項化

初始化:

  1. 預設初始化
  2. 顯示初始化 / 程式碼塊初始化 / 構造器初始化

7、物件的記憶體佈局:

示例:

8、物件的訪問定位

8.1、訪問物件的方式:

  1. 控制代碼訪問

    reference中儲存穩定控制代碼地址,物件被移動(垃圾回收時常見)時只會改變控制代碼池中到物件示例資料的指標即可,reference不用修改

  1. 直接指標(HotSpot採用)

    記憶體相對於較小

9、直接記憶體

  • 不是虛擬機器執行時資料區的一部分,也不是《Java虛擬機器規範》中定義的記憶體區域。
  • 直接記憶體是在Java堆外的、直接向系統申請的記憶體區間。
  • 來源於NIO,通過存在堆中的DirectByteBuffer操作Native記憶體
  • 通常,訪問直接記憶體的速度會優於Java堆。即讀寫效能高。
    • 因此出於效能考慮,讀寫頻繁的場合可能會考慮使用直接記憶體。
    • Java的NIO庫允許Java程式使用直接記憶體,用於資料緩衝區