1. 程式人生 > 其它 >JVM_03 執行時資料區 [ 方法區]

JVM_03 執行時資料區 [ 方法區]

技術標籤:JVM上篇(記憶體與垃圾回收篇)

①. 方法區的概述

1>. 方法區的概述

  • ①. 方法區在JVM啟動的時候被建立,並且它的實際的實體記憶體空間和Java堆區一樣都可以是不連續的 | 關閉Jvm就會釋放這個區域的記憶體

  • ②. 方法區時邏輯上是堆的一個組成部分,但是在不同虛擬機器裡頭實現是不一樣的,最典型的就是永久代(PermGen space)和元空間(Metaspace)
    (注意:方法區時一種規範,而永久代和元空間是它的一種實現方式)

  • ③. 方法區的大小決定了系統可以儲存多少個類,如果系統定義了太多的類,導致方法區溢位,虛擬機器同樣會丟擲記憶體溢位錯誤:(java.lang.OutOfMemoryError:PermGen space、java.lang.OutOfMemoryError:Metaspace) 掌握

  1. 載入大量的第三方的jar包
  2. tomcat部署的工程過多(30-50個)
  3. 大量動態的生成反射類
  • ④. 對於HotspotJVM而言,方法區還有一個別名叫非堆(Non-heap),目的就是要和堆分開,方法區可以看成一塊獨立於Java堆的記憶體空間

②. 方法區的內部結構

2>. 方法區的內部結構

  • ①. 深入理解Java虛擬機器》書中對方法區儲存內容描述如下:它用於儲存已被虛擬機器載入的型別資訊、常量、靜態變數、即時編譯器編譯後的程式碼快取掌握
    在這裡插入圖片描述

  • ②. 型別資訊(對每個載入的型別( 類class、介面interface、列舉enum、註解annotation),JVM必 .須在方法區中儲存以下型別資訊:)

  1. 這個型別的完整有效名稱(全名=包名.類名)
  2. 這個型別直接父類的完整有效名(對於interface或是java. lang.Object,都沒有父類)
  3. 這個型別的修飾符(public, abstract, final的某個子集)
  4. 這個型別直接介面的一個有序列表
  • ③. 域資訊(成員變數)
  1. JVM必須在方法區中儲存型別的所有域的相關資訊以及域的宣告順序。
  2. 域的相關資訊包括:域名稱、 域型別、域修飾符(public, private, protected, static, final, volatile, transient的某個子集)
  • ④. 方法資訊:JVM必須儲存所有方法的以下資訊,同域資訊一樣包括宣告順序
  1. 方法名稱
  2. 方法的返回型別(或void)
  3. 方法引數的數量和型別(按順序)
  4. 方法的修飾符(public, private, protected, static, final,synchronized, native , abstract的一個子集)
  5. 方法的位元組碼(bytecodes)、運算元棧、區域性變量表及大小( abstract和native 方法除外)
  6. 異常表( abstract和native方法除外)
    每個異常處理的開始位置、結束位置、程式碼處理在程式計數器中的偏移地址、被捕獲的異常類的常量池索引
  • ⑤. non-final的類變數
    (Order.class位元組碼檔案,右鍵Open in Teminal開啟控制檯,使用javap -v -p Order.class > tst.txt 將位元組碼檔案反編譯並輸出為txt檔案,可以看到被宣告為static final的常量number在編譯的時候就被賦值了,這不同於沒有被final修飾的static變數count是在類載入的準備階段被賦值為預設的初始化值,在初始化的時候賦予正確的初始化值
 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

以下程式碼不會報空指標異常

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

③. 方法區的演進細節

3>. 方法區的演進細節 面試常問

  • ①. Jdk 1.6 及之前:有永久代,靜態變數、字串常量池1.6在方法區

  • ②. Jdk 1.7 :有永久代,但已經逐步 " 去永久代 ",字串常量池、靜態變數移除,儲存在堆中

  • ③. jdk 1.8 及之後: 無永久代,常量池1.8在元空間。但靜態變數、字串常量池仍在堆中
    在這裡插入圖片描述

  • ④. 為什麼要用元空間取代永久代 掌握

  1. 為永久代設定空間大小是很難確定的
    (①. 永久代引數設定過小,在某些場景下,如果動態載入的類過多,容易產生Perm區的OOM,比如某個實際Web工程中,因為功能點比較多,在執行過程中,要不斷動態載入很多類,經常出現致命錯誤
    ②. 永久代引數設定過大,導致空間浪費
    ③. 預設情況下,元空間的大小受本地記憶體限制)
  2. 對永久代進行調優是很困難的
    (方法區的垃圾收集主要回收兩部分:常量池中廢棄的常量和不再使用的型別,而不再使用的類或類的載入器回收比較複雜,full gc 的時間長)
  • ⑤. StringTable為什麼要調整 掌握
  1. jdk7中將StringTable放到了堆空間中。因為永久代的回收效率很低,在full gc的時候才能觸發。而full gc是老年代的空間不足、永久代不足才會觸發
  2. 這就導致StringTable回收效率不高,而我們開發中會有大量的字串被建立,回收效率低,導致永久代記憶體不足,放到堆裡,能及時回收記憶體

④. 設定方法區大小

4>. 設定方法區大小

  • ①. jdk7及以前:
  1. -XX:PermSize=100m(預設值是20.75M)
  2. -XX:MaxPermSize=100m(32位機器預設是64M,64位機器模式是82M)
  3. 圖解:
    在這裡插入圖片描述
  • ②. jdk1.8及以後
  1. -XX:MetaspaceSize=100m(windows下,預設約等於21M)
  2. -XX:MaxMetaspaceSize=100m(預設是-1,即沒有限制)

⑤. 常量池的理解

5>. 常量池的理解

  • ①. 常量池,可以看做是一張表,虛擬機器指令根據這張常量表找到要執行的類名,方法名,引數型別、字面量等資訊 掌握
Constant pool:
   #1 = Methodref          #7.#23         // java/lang/Object."<init>":()V
   #2 = Methodref          #24.#25        // com/xiaozhi/heap/Order.hello:()V
   #3 = Fieldref           #26.#27        // java/lang/System.out:Ljava/io/PrintStream;
   #4 = Fieldref           #24.#28        // com/xiaozhi/heap/Order.count:I
   #5 = Methodref          #29.#30        // java/io/PrintStream.println:(I)V
  • ②. 一個有效的位元組碼檔案中除了包含類的版本資訊、欄位、方法以及介面等描述資訊外,還包含一項資訊那就是常量池表(Constant Poo1 Table),包括各種字面量和對型別域和方法的符號引用。

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

  • ④. 比如如下程式碼,雖然只有 194 位元組,但是裡面卻使用了 string、System、Printstream 及 Object 等結構。這裡程式碼量其實已經很小了。如果程式碼多,引用到的結構會更多!

	Public class Simpleclass {
	public void sayhelloo() {
	    System.out.Println (hello) }
	}

⑥. 執行時常量池

6>. 執行時常量池 掌握

  • ①. 執行時常量池,常量池是 *.class 檔案中的,當該類被載入,它的常量池資訊就會放入執行時常量池,並把裡面的符號地址變為真實地址

  • ②. 執行時常量池( Runtime Constant Pool)是方法區的一部分。

  • ③. 常量池表(Constant Pool Table)是Class檔案的一部分,用於存放編譯期生成的各種字面量與符號引用,這部分內容將在類載入後存放到方法區的執行時常量池中。

  • ④. 執行時常量池中包含多種不同的常量,包括編譯期就已經明確的數值字面量,也包括到執行期解析後才能夠獲得的方法或者欄位引用。此時不再是常量池中的符號地址了,這裡換為真實地址。
    (方法區內常量池之中主要存放的兩大類常量:字面量和符號引用。
    字面量比較接近Java語言層次的常量概念,如文字字串、被宣告為final的常量值等。
    而符號引用則屬於編譯原理方面的概念,包括下面三類常量:
    1、類和介面的全限定名
    2、欄位的名稱和描述符
    3、方法的名稱和描述符) 掌握

⑦. 如何證明靜態變數存在哪

7>.如何證明靜態變數存在哪

/**
 * 《深入理解Java虛擬機器》中的案例:
 * staticObj、instanceObj、localObj存放在哪裡?
 */
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();
    }
}
  • staticObj隨著Test的型別資訊存放在方法區,instance0bj 隨著Test的物件例項存放在Java堆,localobject則是存放在foo()方法棧幀的區域性變量表中
hsdb>scanoops 0x00007f32c7800000 0x00007f32c7b50000 JHSDB_ _TestCase$Obj ectHolder
0x00007f32c7a7c458 JHSDB_ TestCase$Obj ectHolder
0x00007f32c7a7c480 JHSDB_ TestCase$Obj ectHolder
0x00007f32c7a7c490 JHSDB_ TestCase$Obj ectHolder
  • 測試發現:三個物件的資料在記憶體中的地址都落在Eden區範圍內,所以結論:只要是物件例項必然會在Java堆中分配

  • 接著,找到了一個引用該staticObj物件的地方,是在一個java. lang . Class的例項裡,並且給出了這個例項的地址,通過Inspector檢視該物件例項,可以清楚看到這確實是一個
    java.lang.Class型別的物件例項,裡面有一個名為staticObj的例項欄位:
    在這裡插入圖片描述

  • 從《Java 虛擬機器規範》所定義的概念模型來看,所有 C1ass 相關的資訊都應該存放在方法區之中,但方法區該如何實現,《Java 虛擬機器規範》並未做出規定,這就成了一件允許不同虛擬機器自己靈活把握的事情。JDK7 及其以後版本的 Hotspot 虛擬機器選擇把靜態變數與型別在 Java 語言一端的對映 C1ass 物件存放在一起,儲存於】ava 堆之中,從我們的實驗中也明確驗證了這一點.

⑧. 方法區的垃圾回收

8>. 方法區的垃圾回收

前言:
(1).有些人認為方法區(如Hotspot,虛擬機器中的元空間或者永久代)是沒有垃圾收集行為的,其實不然。《Java 虛擬機器規範》對方法區的約束是非常寬鬆的,提到過可以不要求虛擬機器在方法區中實現垃圾收集。事實上也確實有未實現或未能完整實現方法區型別解除安裝的收集器存在(如 JDK11 時期的 ZGC 收集器就不支援類解除安裝)
(2). 一般來說這個區域的回收效果比較難令人滿意,尤其是型別的解除安裝,條件相當苛刻。但是這部分割槽域的回收有時又確實是必要的。以前 Sun 公司的 Bug 列表中,曾出現過的若干個嚴重的 Bug 就是由於低版本的 Hotspot 虛擬機器對此區域未完全回收而導致記憶體洩漏。

  • ①. 方法區的垃圾收集主要回收兩部分內容:常量池中廢奔的常量和不再使用的型別

  • ②. 先來說說方法區內常量池之中主要存放的兩大類常量:字面量和符號引用。 字面量比較接近Java語言層次的常量概念,如文字字串、被宣告為final的常量值等。而符號引用則屬於編譯原理方面的概念,包括下面三類常量:

  1. 類和介面的全限定名
  2. 欄位的名稱和描述符
  3. 方法的名稱和描述符
  • ③. HotSpot虛擬機器對常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方引用,就可以被回收。回收廢棄常量與回收Java堆中的物件非常類似。

  • ④. 判定一個常量是否“廢棄”還是相對簡單,而要判定一個型別是否屬於“不再被使用的類”的條件就比較苛刻了。需要同時滿足下面三個條件:

  1. 該類所有的例項都已經被回收,也就是Java堆中不存在該類及其任何派生子類的例項。
  2. 載入該類的類載入器已經被回收,這個條件除非是經過精心設計的可替換類載入器的場景,如OSGi、JSP的重載入等,否則通常是很難達成的
  3. 該類對應的java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法
  • ⑤. Java虛擬機器被允許對滿足上述三個條件的無用類進行回收,這裡說的僅僅是“被允許”,而並不是和物件一樣,沒有引用了就必然會回收。關於是否要對型別進行回收,HotSpot虛擬機器提供了一Xnoclassgc 引數進行控制,還可以使用一verbose:class以及一XX: +TraceClass一Loading、一XX:+TraceClassUnLoading查 看類載入和解除安裝資訊

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