天天看點

<JVM上篇:記憶體與垃圾回收篇>07-方法區

⭐️ ⭐️上篇文章-​​<JVM上篇:記憶體與垃圾回收篇>05-本地方法接口和本地方法棧 | 06-堆​​

<JVM上篇:記憶體與垃圾回收篇>07-方法區

文章目錄

  • ​​7. 方法區​​
  • ​​7.1. 棧、堆、方法區的互動關系​​
  • ​​7.2. 方法區的了解​​
  • ​​7.2.1. 方法區在哪裡?​​
  • ​​7.2.2. 方法區的基本了解​​
  • ​​7.2.3. HotSpot 中方法區的演進​​
  • ​​7.3. 設定方法區大小與 OOM​​
  • ​​7.3.1. 設定方法區記憶體的大小​​
  • ​​7.3.2. 如何解決這些 OOM​​
  • ​​7.4. 方法區的内部結構​​
  • ​​7.4.1. 方法區(Method Area)存儲什麼?​​
  • ​​7.4.2. 方法區的内部結構​​
  • ​​類型資訊​​
  • ​​域(Field)資訊​​
  • ​​方法(Method)資訊​​
  • ​​non-final 的類變量​​
  • ​​補充說明:全局常量(static final)​​
  • ​​7.4.3. 運作時常量池 VS 常量池​​
  • ​​為什麼需要常量池?​​
  • ​​常量池中有什麼?​​
  • ​​小結​​
  • ​​7.4.4. 運作時常量池​​
  • ​​7.5. 方法區使用舉例​​
  • ​​7.6. 方法區的演進細節​​
  • ​​7.6.1. 為什麼永久代要被元空間替代?​​
  • ​​7.6.2. StringTable 為什麼要調整位置?​​
  • ​​7.6.3. 靜态變量存放在那裡?​​
  • ​​7.7. 方法區的垃圾回收​​
  • ​​總結​​
  • ​​常見面試題​​

7. 方法區

<JVM上篇:記憶體與垃圾回收篇>07-方法區

從線程共享與否的角度來看

<JVM上篇:記憶體與垃圾回收篇>07-方法區

7.1. 棧、堆、方法區的互動關系

<JVM上篇:記憶體與垃圾回收篇>07-方法區

7.2. 方法區的了解

官方文檔:​​Chapter 2. The Structure of the Java Virtual Machine (oracle.com)​​

<JVM上篇:記憶體與垃圾回收篇>07-方法區

7.2.1. 方法區在哪裡?

《Java 虛拟機規範》中明确說明:“盡管所有的方法區在邏輯上是屬于堆的一部分,但一些簡單的實作可能不會選擇去進行垃圾收集或者進行壓縮。”但對于 HotSpotJVM 而言,方法區還有一個别名叫做 Non-Heap(非堆),目的就是要和堆分開。

<JVM上篇:記憶體與垃圾回收篇>07-方法區

是以,方法區看作是一塊獨立于 Java 堆的記憶體空間。

代碼層面也可以進行示範:

<JVM上篇:記憶體與垃圾回收篇>07-方法區

7.2.2. 方法區的基本了解

  • 方法區(Method Area)與 Java 堆一樣,是各個線程共享的記憶體區域。(當多個線程都需要調用某個類時,并且該類沒有被加載,隻需要其中一個線程加載即可)
  • 方法區在 JVM 啟動的時候被建立,并且它的實際的實體記憶體空間中和 Java 堆區一樣都可以是不連續的。
  • 方法區的大小,跟堆空間一樣,可以選擇固定大小或者可擴充。
  • 方法區的大小決定了系統可以儲存多少個類,如果系統定義了太多的類,導緻方法區溢出,虛拟機同樣會抛出記憶體溢出錯誤:​

    ​java.lang.OutOfMemoryError: PermGen space​

    ​​ 或者​

    ​java.lang.OutOfMemoryError: Metaspace​

  • 加載大量的第三方的 jar 包;Tomcat 部署的工程過多(30~50 個);大量動态的生成反射類
  • 關閉 JVM 就會釋放這個區域的記憶體。

7.2.3. HotSpot 中方法區的演進

在 jdk7 及以前,習慣上把方法區,稱為永久代。jdk8 開始,使用元空間取代了永久代。

<JVM上篇:記憶體與垃圾回收篇>07-方法區

本質上,方法區和永久代并不等價。僅是對 hotspot 而言的。《Java 虛拟機規範》對如何實作方法區,不做統一要求。例如:BEA JRockit / IBM J9 中不存在永久代的概念。

補充:

  • 可以把方法區看成接口,永久代或者元空間看成其實作
  • 元空間是方法區的一種實作. 方法區是一種規範,永久代和元空間是兩種不同的實作方式
  • 元空間不在虛拟機設定的記憶體中,而是使用了本地記憶體

現在來看,當年使用永久代,不是好的 idea。導緻 Java 程式更容易 OOM(超過​

​-XX:MaxPermsize​

​上限)

<JVM上篇:記憶體與垃圾回收篇>07-方法區

而到了 JDK8,終于完全廢棄了永久代的概念,改用與 JRockit、J9 一樣在本地記憶體中實作的元空間(Metaspace)來代替

<JVM上篇:記憶體與垃圾回收篇>07-方法區

元空間的本質和永久代類似,都是對 JVM 規範中方法區的實作。不過元空間與永久代最大的差別在于:元空間不在虛拟機設定的記憶體中,而是使用本地記憶體 (可以達到幾個G…更不容易出現OOM)

永久代、元空間二者并不隻是名字變了,内部結構也調整了

根據《Java 虛拟機規範》的規定,如果方法區無法滿足新的記憶體配置設定需求時,将抛出 OOM 異常

7.3. 設定方法區大小與 OOM

7.3.1. 設定方法區記憶體的大小

方法區的大小不必是固定的,JVM 可以根據應用的需要動态調整。

jdk7 及以前

  • 通過​

    ​-XX:Permsize​

    ​來設定永久代初始配置設定空間。預設值是 20.75M
  • 通過​

    ​-XX:MaxPermsize​

    ​來設定永久代最大可配置設定空間。32 位機器預設是 64M,64 位機器模式是 82M
  • 當 JVM 加載的類資訊容量超過了這個值,會報異常​

    ​OutOfMemoryError:PermGen space​

    ​。
<JVM上篇:記憶體與垃圾回收篇>07-方法區

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上篇:記憶體與垃圾回收篇>07-方法區

舉例 1:《深入了解 Java 虛拟機》的例子

<JVM上篇:記憶體與垃圾回收篇>07-方法區

舉例 2

/**
 * 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上篇:記憶體與垃圾回收篇>07-方法區
<JVM上篇:記憶體與垃圾回收篇>07-方法區

7.3.2. 如何解決這些 OOM

  1. 要解決 OOM 異常或 heap space 的異常,一般的手段是首先通過記憶體映像分析工具(如 Eclipse Memory Analyzer)對 dump 出來的堆轉儲快照進行分析,重點是确認記憶體中的對象是否是必要的,也就是要先厘清楚到底是出現了記憶體洩漏(Memory Leak)還是記憶體溢出(Memory Overflow)
  2. 如果是記憶體洩漏,可進一步通過工具檢視洩漏對象到 GC Roots 的引用鍊。于是就能找到洩漏對象是通過怎樣的路徑與 GCRoots 相關聯并導緻垃圾收集器無法自動回收它們的。掌握了洩漏對象的類型資訊,以及 GCRoots 引用鍊的資訊,就可以比較準确地定位出洩漏代碼的位置。

    一句話: 記憶體洩漏就是有大量的引用指向某些對象,但是這些對象以後不會使用了,但是因為它們還和GC ROOT有關聯,是以導緻以後這些對象也不會被回收,這就是記憶體洩漏的問題

  3. 如果不存在記憶體洩漏,換句話說就是記憶體中的對象确實都還必須存活着,那就應當檢查虛拟機的堆參數(​

    ​-Xmx​

    ​與​

    ​-Xms​

    ​),與機器實體記憶體對比看是否還可以調大,從代碼上檢查是否存在某些對象生命周期過長、持有狀态時間過長的情況,嘗試減少程式運作期的記憶體消耗。

7.4. 方法區的内部結構

<JVM上篇:記憶體與垃圾回收篇>07-方法區

7.4.1. 方法區(Method Area)存儲什麼?

《深入了解 Java 虛拟機》書中對方法區(Method Area)存儲内容描述如下:

它用于存儲已被虛拟機加載的類型資訊、常量、靜态變量、即時編譯器編譯後的代碼緩存等。
<JVM上篇:記憶體與垃圾回收篇>07-方法區

7.4.2. 方法區的内部結構

類型資訊

對每個加載的類型(類 class、接口 interface、枚舉 enum、注解 annotation),JVM 必須在方法區中存儲以下類型資訊:

  1. 這個類型的完整有效名稱(全名=包名.類名)
  2. 這個類型直接父類的完整有效名(對于 interface 或是 java.lang.object,都沒有父類)
  3. 這個類型的修飾符(public,abstract,final 的某個子集)
  4. 這個類型直接接口的一個有序清單
<JVM上篇:記憶體與垃圾回收篇>07-方法區

域(Field)資訊

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

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

<JVM上篇:記憶體與垃圾回收篇>07-方法區

問題:方法區包含域資訊,也就是包含成員變量?

隻是存放了成員變量的資訊,具體的值會和對象一起放在堆中。類變量會存放在方法區

方法(Method)資訊

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

  1. 方法名稱
  2. 方法的傳回類型(或 void)
  3. 方法參數的數量和類型(按順序)
  4. 方法的修飾符(public,private,protected,static,final,synchronized,native,abstract 的一個子集)
  5. 方法的位元組碼(bytecodes)、操作數棧、局部變量表及大小(abstract 和 native 方法除外)
  6. 異常表(abstract 和 native 方法除外)
  • 每個異常處理的開始位置、結束位置、代碼處理在程式計數器中的偏移位址、被捕獲的異常類的常量池索引
<JVM上篇:記憶體與垃圾回收篇>07-方法區

**備注:**以上代碼皆來源于​

​MethodInnerStrucTest​

​​ 類的位元組碼檔案, 可以通過 ​

​javap -v -p Met hodInnerStrucTest.class > test.txt​

​ 指令,将位元組碼導出到檔案中,便于觀察.

  • 位元組碼檔案中存放着類檔案的類型詳細資訊.(如上所示)
  • 類加載器将位元組碼檔案加載到方法區後. 方法區中除了記錄着類的詳細資訊,還包括該類是被哪個類加載器加載的等, 同時類加載器(也是類)也會記錄加載了哪些類.

non-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!");
    }
}      

執行以上的main方法,并不會報錯,amazing!

補充說明:全局常量(static final)

被聲明為 final 的類變量的處理方法則不同,每個全局常量在編譯的時候就會被配置設定了。

通過位元組碼檢視

代碼同上面.....      
  • 對比final修飾的變量指派操作
<JVM上篇:記憶體與垃圾回收篇>07-方法區
  • 檢視靜态變量指派操作
<JVM上篇:記憶體與垃圾回收篇>07-方法區

7.4.3. 運作時常量池 VS 常量池

<JVM上篇:記憶體與垃圾回收篇>07-方法區
  • 方法區,内部包含了運作時常量池
  • 位元組碼檔案,内部包含了常量池
  • 要弄清楚方法區,需要了解清楚 ClassFile,因為加載類的資訊都在方法區。
  • 要弄清楚方法區的運作時常量池,需要了解清楚 ClassFile 中的常量池。

官方文檔:​​https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html​​

<JVM上篇:記憶體與垃圾回收篇>07-方法區

一個有效的位元組碼檔案中除了包含類的版本資訊、字段、方法以及接口等描述符資訊外,還包含一項資訊就是常量池表(Constant Pool Table),包括各種字面量和對類型、域和方法的符号引用. 也就是說一個方法的具體實作細節都藏在常量池中

<JVM上篇:記憶體與垃圾回收篇>07-方法區

舉例:

常量池中的資訊相當于炒菜的基本原料,每個方法相當于一道道菜.每道菜都可能會用到那些基本原材料.對應到代碼中,方法的位元組碼會用到常量池中的内容

為什麼需要常量池?

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

比如:如下的代碼:

public class SimpleClass {
    public void sayHello() {
        System.out.println("hello");
    }
}      

雖然隻有 194 位元組,但是裡面卻使用了 String、System、PrintStream 及 Object 等結構。這裡的代碼量其實很少了,如果代碼多的話,引用的結構将會更多,這裡就需要用到常量池了。

<JVM上篇:記憶體與垃圾回收篇>07-方法區

常量池中有什麼?

擊中常量池記憶體儲的資料類型包括:

  • 數量值
  • 字元串值
  • 類引用
  • 字段引用
  • 方法引用
<JVM上篇:記憶體與垃圾回收篇>07-方法區

例如下面這段代碼:

public class MethodAreaTest2 {
    public static void main(String args[]) {
        Object obj = new Object();
    }
}      

​Object obj = new Object();​

​将會被翻譯成如下位元組碼:

0: new #2  // Class java/lang/Object
1: dup
2: invokespecial // Method java/lang/Object "<init>"() V      

小結

常量池、可以看做是一張表,虛拟機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量等類型

7.4.4. 運作時常量池

  • 運作時常量池(Runtime Constant Pool)是方法區的一部分。
  • 常量池表(Constant Pool Table)是 Class 檔案的一部分,用于存放編譯期生成的各種字面量與符号引用,這部分内容将在類加載後存放到方法區的運作時常量池中。
  • 位元組碼中的常量池存放的都是符号引用,連結解析階段将符号引用轉化為直接引用.是以方法區的運作區常量池裡面存放的都是直接引用.
  • 運作時常量池,在加載類和接口到虛拟機後,就會建立對應的運作時常量池。
  • JVM 為每個已加載的類型(類或接口)都維護一個常量池。池中的資料項像數組項一樣,是通過索引通路的。
  • 運作時常量池中包含多種不同的常量,包括編譯期就已經明确的數值字面量,也包括到運作期解析後才能夠獲得的方法或者字段引用。此時不再是常量池中的符号位址了,這裡換為真實位址。
  • 運作時常量池,相對于 Class 檔案常量池的另一重要特征是:具備動态性。
  • 以String.intern()為例,編譯器會将字元串添加到常量池中(StringTable維護),并傳回指向該常量的引用
  • 運作時常量池類似于傳統程式設計語言中的符号表(symboltable),但是它所包含的資料卻比符号表要更加豐富一些。
  • 當建立類或接口的運作時常量池時,如果構造運作時常量池所需的記憶體空間超過了方法區所能提供的最大值,則 JVM 會抛 OutOfMemoryError 異常。

深入解析動态性:

  • 動态性是運作時常量池可以動态的往裡面添加本來沒有的資訊
  • 而常量池,隻能放代碼中存在的資訊,在編譯期間,就确定了,不會再得到更改
  • 運作時常量池,則可以通過代碼動态的往裡面塞資訊。

7.5. 方法區使用舉例

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);
    }
}      
  • 五個本地變量,是以本地變量表尾5. args存在下标為0的位置上
<JVM上篇:記憶體與垃圾回收篇>07-方法區
  • 将500放入操作數棧,然後指派給變量x中,并将x存放到局部變量表中.
<JVM上篇:記憶體與垃圾回收篇>07-方法區
<JVM上篇:記憶體與垃圾回收篇>07-方法區
  • 将100放入操作數棧,然後指派給變量y中,并将y存放到局部變量表中.
<JVM上篇:記憶體與垃圾回收篇>07-方法區
<JVM上篇:記憶體與垃圾回收篇>07-方法區
  • 讀取本地變量表下标為1、2的變量,将其放入到操作數棧中(等待運算)
<JVM上篇:記憶體與垃圾回收篇>07-方法區
<JVM上篇:記憶體與垃圾回收篇>07-方法區
  • 進行除法運算,并将結果存放到操作數棧中。之後将結果指派給a,并存放到局部變量表中。
<JVM上篇:記憶體與垃圾回收篇>07-方法區
  • 将50放入操作數棧,然後指派給變量b中,并将b存放到局部變量表中.
<JVM上篇:記憶體與垃圾回收篇>07-方法區
<JVM上篇:記憶體與垃圾回收篇>07-方法區
  • 擷取#2(System.out.printlen)的值,并将其放入操作數棧中.
<JVM上篇:記憶體與垃圾回收篇>07-方法區
  • 将本地變量表下标為3、4的加入到操作數棧中,并執行加操作運算。
<JVM上篇:記憶體與垃圾回收篇>07-方法區
<JVM上篇:記憶體與垃圾回收篇>07-方法區
<JVM上篇:記憶體與垃圾回收篇>07-方法區
  • 彈出操作數棧中的參數,傳入 # 2對應的方法(System.out.println) ,進行列印操作。
<JVM上篇:記憶體與垃圾回收篇>07-方法區
  • 執行return指令,結束方法.
<JVM上篇:記憶體與垃圾回收篇>07-方法區

圖解糾錯

程式計數器裡面應該儲存的是目前執行指令的下一條指令位址.

分析:PC計數器儲存的是目前指令的下一條指令位址。目前指令執行完畢,CPU切換到其他線程,執行另外一個線程的指令。 當CPU再次切換回來時,從PC計數器拿到下一條要執行的指令繼續進行執行。

(CPU不會執行一半就去執行其他線程的指令)

7.6. 方法區的演進細節

  1. 首先明确:隻有 Hotspot 才有永久代。BEA JRockit、IBMJ9 等來說,是不存在永久代的概念的。原則上如何實作方法區屬于虛拟機實作細節,不受《Java 虛拟機規範》管束,并不要求統一
  2. Hotspot 中方法區的變化:
JDK1.6 及之前 有永久代(permanet),靜态變量存儲在永久代上
JDK1.7 有永久代,但已經逐漸 “去永久代”,字元串常量池,靜态變量移除,儲存在堆中
JDK1.8 無永久代,類型資訊,字段,方法,常量儲存在本地記憶體的元空間,但字元串常量池、靜态變量仍然在堆中。
<JVM上篇:記憶體與垃圾回收篇>07-方法區

7.6.1. 為什麼永久代要被元空間替代?

官網位址:​​JEP 122: Remove the Permanent Generation (java.net)​​

<JVM上篇:記憶體與垃圾回收篇>07-方法區

随着 Java8 的到來,HotSpot VM 中再也見不到永久代了。但是這并不意味着類的中繼資料資訊也消失了。這些資料被移到了一個與堆不相連的本地記憶體區域,這個區域叫做元空間(Metaspace)。

由于類的中繼資料配置設定在本地記憶體中,元空間的最大可配置設定空間就是系統可用記憶體空間。

這項改動是很有必要的,原因有:

  1. 為永久代設定空間大小是很難确定的。在某些場景下,如果動态加載類過多,容易産生 Perm 區的 oom。比如某個實際 Web 工 程中,因為功能點比較多,在運作過程中,要不斷動态加載很多類,經常出現緻命錯誤。
"Exception in thread 'dubbo client x.x connector' java.lang.OutOfMemoryError:PermGen space"      

而元空間和永久代之間最大的差別在于:元空間并不在虛拟機中,而是使用本地記憶體。 是以,預設情況下,元空間的大小僅受本地記憶體限制。

  1. 對永久代進行調優是很困難的。

有些人認為方法區(如 HotSpot 虛拟機中的元空間或者永久代)是沒有垃圾收集行為的,其實不然。《Java 虛拟機規範》對方法區的限制是非常寬松的,提到過可以不要求虛拟機在方法區中實作垃圾收集。事實上也确實有未實作或未能完整實作方法區類型解除安裝的收集器存在(如 JDK 11 時期的 ZGC 收集器就不支援類解除安裝)。 一般來說這個區域的回收效果比較難令人滿意,尤其是類型的解除安裝,條件相當苛刻。但是這部分區域的回收有時又确實是必要的。以前 Sun 公司的 Bug 清單中,曾出現過的若幹個嚴重的 Bug 就是由于低版本的 HotSpot 虛拟機對此區域未完全回收而導緻記憶體洩漏

方法區的垃圾收集主要回收兩部分内容:常量池中廢棄的常量和不再使用的類型

7.6.2. StringTable 為什麼要調整位置?

jdk7 中将 StringTable 放到了堆空間中。因為永久代的回收效率很低,在 full gc 的時候才會觸發。而 full gc 是老年代的空間不足、永久代不足時才會觸發。

這就導緻 StringTable 回收效率不高。而我們開發中會有大量的字元串被建立,回收效率低,導緻永久代記憶體不足。放到堆裡,能及時回收記憶體。

7.6.3. 靜态變量存放在那裡?

  • 示例1
/**
 * 靜态引用對應的對象實體始終都存在堆空間
 * jdk7:
 * -Xms200m -Xmx200m -XX:PermSize=300m -XX:MaxPermSize=300m -XX:+PrintGCDetails
 * jdk8:
 * -Xms200m -Xmx200m-XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails
 */
public class StaticFieldTest {
    private static byte[] arr = new byte[1024 * 1024 * 100];
    public static void main(String[] args) {
        System.out.println(StaticFieldTest.arr);

        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}      
<JVM上篇:記憶體與垃圾回收篇>07-方法區
<JVM上篇:記憶體與垃圾回收篇>07-方法區

結論:靜态引用對應的對象實體始終都存在堆空間

  • 示例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 工具進行分析,這裡細節略掉

<JVM上篇:記憶體與垃圾回收篇>07-方法區

**三個變量的引用:**staticobj 随着 Test 的類型資訊存放在方法區,instanceobj 随着 Test 的對象執行個體存放在 Java 堆,localobject 則是存放在 foo()方法棧幀的局部變量表中。

<JVM上篇:記憶體與垃圾回收篇>07-方法區

測試發現:三個對象的資料在記憶體中的位址都落在 Eden 區範圍内,是以結論:隻要是對象執行個體必然會在 Java 堆中配置設定。

接着,找到了一個引用該 staticobj 對象的地方,是在一個 java.lang.Class 的執行個體裡,并且給出了這個執行個體的位址,通過 Inspector 檢視該對象執行個體,可以清楚看到這确實是一個 java.lang.Class 類型的對象執行個體,裡面有一個名為 staticobj 的執行個體字段:

從《Java 虛拟機規範》所定義的概念模型來看,所有 Class 相關的資訊都應該存放在方法區之中,但方法區該如何實作,《Java 虛拟機規範》并未做出規定,這就成了一件允許不同虛拟機自己靈活把握的事情。JDK7 及其以後版本的 HotSpot 虛拟機選擇把靜态變量與類型在 Java 語言一端的映射 class 對象存放在一起,存儲于 Java 堆之中,從我們的實驗中也明确驗證了這一點

備注:前面已經說過了,沒有棧上配置設定這碼事,如果未逃逸,做标量替換,把一個對象分解為多個成員變量,也還是存在堆上。

7.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:+TraceClassLoading​

​​、​

​-XX:+TraceClassUnLoading​

​檢視類加載和解除安裝資訊

在大量使用反射、動态代理、CGLib 等位元組碼架構,動态生成 JSP 以及 OSGi 這類頻繁自定義類加載器的場景中,通常都需要 Java 虛拟機具備類型解除安裝的能力,以保證不會對方法區造成過大的記憶體壓力。

總結

<JVM上篇:記憶體與垃圾回收篇>07-方法區
  • 關于MiniorGC和Full GC的差別. MinorGC是老年代的GC,FullGC是整個堆的GC
  • 棧幀中的動态連結指向的是方法區中的方法,代表目前操作的是哪個方法

常見面試題

百度:

說一下 JVM 記憶體模型吧,有哪些區?分别幹什麼的?

螞蟻金服:

Java8 的記憶體分代改進 JVM 記憶體分哪幾個區,每個區的作用是什麼?

一面:JVM 記憶體分布/記憶體結構?棧和堆的差別?堆的結構?為什麼兩個 survivor 區?

二面:Eden 和 survior 的比例配置設定

小米:

jvm 記憶體分區,為什麼要有新生代和老年代

位元組跳動:

二面:Java 的記憶體分區

二面:講講 vm 運作時資料庫區 什麼時候對象會進入老年代?

京東:

JVM 的記憶體結構,Eden 和 Survivor 比例。

JVM 記憶體為什麼要分成新生代,老年代,持久代。

新生代中為什麼要分為 Eden 和 survivor。

天貓:

一面:Jvm 記憶體模型以及分區,需要詳細到每個區放什麼。

一面:JVM 的記憶體模型,Java8 做了什麼改

拼多多:

JVM 記憶體分哪幾個區,每個區的作用是什麼?

美團: