1. 程式人生 > 實用技巧 >JVM全面分析之方法區

JVM全面分析之方法區

目錄

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


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

方法區的演進

  • 在jdk7以及以前,習慣上把方法區,稱為永久代。jdk8開始,使用元空間取代了永久代,並且儲存在直接記憶體中
  • 本質上,方法區和永久代並不等價。僅是對hotspot而言的,《Java虛擬機器規範》對如何實現方法區,不做統一要求。例如JRockit / IBM J9中不存在永久代的概念。
      * 現在來看,當年使用永久代,不是好的idea。導致Java程式更容易OOM(超過-XX:MaxPermSize上線)。

方法區的理解

設定方法區大小與OOM

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

  • jdk7及以前:
      * 通過-XX:PermSize來設定永久代初始分配空間。預設值是20.75
      * -XX:MaxPermSize來設定永久代最大可分配空間。32位機器預設是64M。,64位機器模式是82M。
      * 當JVM載入的類資訊容量超過了這個值,會報異常OutOfMemoryError:PermGen space。

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

下面用命令檢視引數設定的大小:

public class JvmTest01 {
    public static void main(String[] args) {
        System.out.println("haha");
        try {
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("end");
    }
}

使用jps檢視程式的埠,如12345.使用jinfo -flag MetaspaceSize 12345.即可檢視。

如何解決這些OOM

  1. 要解決OOM異常或heap space的異常,一般的手段是首先通過記憶體映像分析工具(如Eclipse Memory Analyzer)對dump出來的堆轉儲快照進行分析,重點是確認記憶體中的物件是否是必要的,也就是要分清楚到底是出現了記憶體洩漏(Memory Leak)還是記憶體溢位(Memory Overflow).
  2. 如果是記憶體洩漏,可進一步通過工具檢視洩漏物件到GC Roots的引用鏈,於是就能找到洩漏物件是通過怎樣的路徑與GC Roots相關聯並導致垃圾收集器無法自動回收他們的。掌握了洩漏物件的型別資訊,以及GC Roots引用鏈的資訊,就可以比較準確的定位出洩漏程式碼的位置。
  3. 如果不存在記憶體洩漏,換句話說就是記憶體中的物件確實都還必須存活著,那就應當檢查虛擬機器的堆引數(-Xmx與-Xms),與機器屋裡記憶體對比看是否還可以調大,從程式碼上檢查是否存在某些物件宣告週期過長、持有狀態時間過長的情況,嘗試減少程式執行期的記憶體消耗。

方法區的內部結構

《深入理解Java虛擬機器》書中對方法區(Method Area)儲存內容描述如下:它用於儲存已被虛擬機器載入的 型別資訊、常量、靜態變數、即時編譯器編譯後的程式碼快取等

型別資訊

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

  1. 這個型別的完整有效名稱(全名=包名.類名)
  2. 這個型別直接父類的完整有效名(對於interface或是java.lang.Object,都沒有父類)
  3. 這個型別的修飾符(public,abstract,final的某個子集)
  4. 這個型別直接介面的一個有序列表

域(Field資訊)

  1. JVM必須在方法區中儲存型別的所有域的相關資訊以及域的宣告順序。
  2. 域的相關資訊包括:域名稱、域型別、域修飾符(public,private,protected,static,final,volatile,transient的某個子集)

方法(Method)資訊

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

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

全域性常量:static final
  被宣告為final的類變數的處理方法則不同,每個全域性常量在編譯的時候就會被分配了。可以通過javap -v -p *.class命令檢視。

public class JvmTest01 {
    public static int i = 10;

    public final static int j = 10;

    public final int k = 10;

    public int m = 10;
}

常量池表

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

執行時常量池

  • 執行時常量池(Runtime Constant Pool)是方法區的一部分。
  • 常量池表(Constant Pool Table)是Class檔案的一部分,用於存放編譯器生成的各種字面量與符號引用,這部分內容將在類載入後存放到方法區的執行時常量池中
  • 執行時常量池,在載入類和介面到虛擬機器後,就會建立對應的執行時常量池。
  • JVM為每個已載入的型別(類或者介面)都維護一個常量池。池中的資料項像陣列項一樣,是通過索引訪問的。
  • 執行時常量池中包含多種不同的常量,包括編譯器就已經明確的數值字面量,也包括到執行期解析後才能獲得的方法或者欄位引用。此時不再是常量池中的符號地址了,這裡換為真實地址。
      * 執行時常量池,相對於Class檔案常量池的另一個重要特徵是:具備動態性。
  • 執行時常量池類似於傳統程式語言中的符號表(symbol table),但是它所包含的資料卻比符號表要更加豐富一些。
  • 當建立類或介面的執行時常量池時,如果構造執行時常量池所需的記憶體空間超過了方法區所能提供的最大值。則JVM會丟擲OutOfMemoryError異常。

方法區使用舉例

public class JvmTest01 {
    public static void main(String[] args) {
        int x = 500;
        int y = 100;
        int a = x / y;
        int b = 50;
        System.out.println(a + b);
    }
}

















方法區的演進細節

Hotspot中方法區的變化:

  • jdk1.6之前 有永久代,靜態變數存放在永久代上
  • jdk1.7 有永久代,但已經逐步”去永久代“。字串常量池、靜態變數移除,儲存在堆中
  • jdk1.8及之後 無永久代,型別資訊、欄位、方法、常量儲存在本地記憶體的元空間,但字串常量池、靜態變數仍在堆中。



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

  1. 為永久代設定空間大小是很難確定的。
    & emsp; 在某些場景下,如果動態載入類過多,容易產生Perm 區的OOM。比如某個實際WEB工程中,因為功能點比較多,在執行過程中,要不斷動態載入很多類,經常出現致命錯誤。

    而元空間和永久代之間最大的區別在於:元空間並不在虛擬機器中,而是使用本地記憶體,因此,預設情況下僅受本地記憶體限制。
  2. 對永久代進行調優是很困難的。

StringTable為什麼要調整?

  jdk7中將StringTable放到了堆空間中。因為永久代的回收效率很低,在full gc的時候才會觸發。而full gc是老年代的空間不足、永久代不足時才會觸發。這就導致StringTable回收效率不高。而我們開發中會有大量的字串被建立,回收效率低,導致永久代記憶體不足。放到堆裡,能及時回收記憶體。

方法區的垃圾回收

  有些人認為方法區(入HotSpot虛擬機器中的元空間或者永久代)是沒有垃圾收集行為的,其實不然。《Java虛擬機器規範》對方法區的約束是非常寬鬆的,提到過可以不要求虛擬機器在方法區中實現垃圾收集。事實上也確實有未實現或未能完整實現方法區型別解除安裝的收集器存在(如JDK 11時期的ZGC收集器就不支援類解除安裝)

  一般來說這個區域的回收效果比較難令人滿意,尤其是型別的解除安裝,條件相當苛刻。但是這部分割槽域的回收有時又確實是必要的。

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

  • 先來說說方法區內常量池之中主要存放的兩大類常量:字面量和符號引用。字面量比較接近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虛擬機器具備型別解除安裝的能力,以保證不會對方法區造成過大的記憶體壓力。

總結