【JVM學習】——方法區

1、概述

1.1 方法區理解

《Java虛擬機規範》中明確說明:「儘管全部的方法區在邏輯上是屬於堆的一部分,但一些簡單的實現可能不會選擇去進行垃圾收集或者進行壓縮。」但對於HotSpot JVM而言,方法區還有一個別名叫作Non-Heap(非堆),目的就是要和堆分開。html

因此,方法區看做是一塊獨立於Java堆的內存空間java

方法區主要存放的是 Class,而堆中主要存放的是 實例化的對象編程

  • 方法區(Method Area)與Java堆同樣,是各個線程共享的內存區域。
  • 方法區在JVM啓動的時候被建立,而且它的實際的物理內存空間中和Java堆區同樣均可以是不連續的。
  • 方法區的大小,跟堆空間同樣,能夠選擇固定大小或者可擴展。
  • 方法區的大小決定了系統能夠保存多少個類,若是系統定義了太多的類,致使方法區溢出,虛擬機一樣會拋出內存溢出錯誤:java.lang.OutofMemoryError:PermGen space 或者java.lang.OutOfMemoryError:Metaspace數組

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

1.2 方法區的演進

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

  • JDK 1.8後,元空間存放在堆外內存中。

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

永久代更容易致使Java程序更容易OOM(超過-XX:MaxPermsize上限)

而到了JDK8,終於徹底廢棄了永久代的概念,改用與JRockit、J9同樣在本地內存中實現的元空間(Metaspace)來代替。服務器

元空間的本質和永久代相似,都是對JVM規範中方法區的實現。不過元空間與永久代最大的區別在於:元空間不在虛擬機設置的內存中,而是使用本地內存框架

永久代、元空間兩者並不僅是名字變了,內部結構也調整了。根據《Java虛擬機規範》的規定,若是方法區沒法知足新的內存分配需求時,將拋出OOM異常。編程語言

1.3 設置方法區大小

方法區的大小沒必要是固定的,JVM能夠根據應用的須要動態調整。 工具

jdk7及之前

  • 經過-XX:Permsize來設置永久代初始分配空間。默認值是20.75M。
  • -XX:MaxPermsize來設定永久代最大可分配空間。32位機器默認是64M,64位機器模式是82M。
  • 當JVM加載的類信息容量超過了這個值,會報異常OutofMemoryError:PermGen space

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時,適當提升該值。若是釋放空間過多,則適當下降該值。

若是初始化的高水位線設置太低,上述高水位線調整狀況會發生不少次。經過垃圾回收器的日誌能夠觀察到FullGC屢次調用。爲了不頻繁地GC,建議將-XX:MetaspaceSize設置爲一個相對較高的值。

如何解決OOM

  • 要解決OOM異常或heap space的異常,通常的手段是首先經過內存映像分析工具(如Eclipse Memory Analyzer)對dump出來的堆轉儲快照進行分析,重點是確認內存中的對象是不是必要的,也就是要先分清楚究竟是出現了內存泄漏(Memory Leak)仍是內存溢出(Memory Overflow)

    • 內存泄漏就是 有大量的引用指向某些對象,可是這些對象之後不會使用了,可是由於它們還和GC ROOT有關聯,因此致使之後這些對象也不會被回收,這就是內存泄漏的問題
  • 若是是內存泄漏,可進一步經過工具查看泄漏對象到GC Roots的引用鏈。因而就能找到泄漏對象是經過怎樣的路徑與GCRoots相關聯並致使垃圾收集器沒法自動回收它們的。掌握了泄漏對象的類型信息,以及GCRoots引用鏈的信息,就能夠比較準確地定位出泄漏代碼的位置。
  • 若是不存在內存泄漏,換句話說就是內存中的對象確實都還必須存活着,那就應當檢查虛擬機的堆參數(-Xmx與-Xms),與機器物理內存對比看是否還能夠調大,從代碼上檢查是否存在某些對象生命週期過長、持有狀態時間過長的狀況,嘗試減小程序運行期的內存消耗。

2、方法區的內部結構

《深刻理解Java虛擬機》書中對方法區(Method Area)存儲內容描述以下:它用於存儲已被虛擬機加載的類型信息常量靜態變量即時編譯器編譯後的代碼緩存等。

2.1 類型信息

對每一個加載的類型(類class、接口interface、枚舉enum、註解annotation),JVm必須在方法區中存儲如下類型信息:

  • 這個類型的完整有效名稱(全名=包名.類名)
  • 這個類型直接父類的完整有效名(對於interface或是java.lang.object,都沒有父類)
  • 這個類型的修飾符(public,abstract,final的某個子集)
  • 這個類型直接接口的一個有序列表

2.2 域信息

JVM必須在方法區中保存類型的全部域的相關信息以及域的聲明順序。

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

2.3 方法信息

JVM必須保存全部方法的如下信息,同域信息同樣包括聲明順序:

  • 方法名稱
  • 方法的返回類型(或void)
  • 方法參數的數量和類型(按順序)
  • 方法的修飾符(public,private,protected,static,final,synchronized,native,abstract的一個子集)
  • 方法的字節碼(bytecodes)、操做數棧、局部變量表及大小(abstract和native方法除外)
  • 異常表(abstract和native方法除外)
每一個異常處理的開始位置、結束位置、代碼處理在程序計數器中的偏移地址、被捕獲的異常類的常量池索引。

2.4 non-final的類變量

靜態變量和類關聯在一塊兒,隨着類的加載而加載,他們成爲類數據在邏輯上的一部分,類變量被類的全部實例共享,即便沒有類實例時,也能夠訪問它。

public class MethodAreaTest {
    public static void main(String[] args) {
        Order order = new Order();
        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!");
    }
}

如上代碼所示,即便咱們把order設置爲null,也不會出現空指針異常。

2.5 運行時常量池

(1)常量池

Java中的常量池,實際上分爲兩種形態:靜態常量池運行時常量池

所謂靜態常量池,即*.class文件中的常量池,class文件中的常量池不只僅包含字符串(數字)字面量,還包含類、方法的信息,佔用class文件絕大部分空間。這種常量池主要用於存放兩大類常量:字面量(Literal)和符號引用量(Symbolic References),字面量至關於Java語言層面常量的概念,如文本字符串,聲明爲final的常量值等,符號引用則屬於編譯原理方面的概念,包括了以下三種類型的常量:

  • 類和接口的全限定名
  • 字段名稱和描述符
  • 方法名稱和描述符

爲何須要常量池

一個java源文件中的類、接口,編譯後產生一個字節碼文件。而Java中的字節碼須要數據支持,一般這種數據會很大以致於不能直接存到字節碼裏,換另外一種方式,能夠存到常量池,這個字節碼包含了指向常量池的引用。

好比:

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

雖然上述代碼只有194字節,可是裏面卻使用了String、System、PrintStream及Object等結構。這裏的代碼量其實不多了,若是代碼多的話,引用的結構將會更多,這裏就須要用到常量池了。

常量池中有什麼

  • 數量值
  • 字符串值
  • 類引用
  • 字段引用
  • 方法引用

例以下面這段代碼:

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

將會被翻譯成以下字節碼:

new #2  
dup
invokespecial

小結

常量池、能夠看作是一張表,虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量等類型。

(2)運行時常量池

運行時常量池(Runtime Constant Pool)是方法區的一部分。

常量池表(Constant Pool Table)是Class文件的一部分,用於存放編譯期生成的各類字面量與符號引用,這部份內容將在類加載後存放到方法區的運行時常量池中

運行時常量池,在加載類和接口到虛擬機後,就會建立對應的運行時常量池。

JVM爲每一個已加載的類型(類或接口)都維護一個常量池。池中的數據項像數組項同樣,是經過索引訪問的。

運行時常量池中包含多種不一樣的常量,包括編譯期就已經明確的數值字面量,也包括到運行期解析後纔可以得到的方法或者字段引用。此時再也不是常量池中的符號地址了,這裏換爲真實地址。

運行時常量池相似於傳統編程語言中的符號表(symboltable),可是它所包含的數據卻比符號表要更加豐富一些。當建立類或接口的運行時常量池時,若是構造運行時常量池所需的內存空間超過了方法區所能提供的最大值,則JVM會拋OutOfMemoryError異常。

(3)圖解實例

以下代碼

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

字節碼執行過程展現:

首先現將操做數500放入到操做數棧中:

而後再存儲到局部變量表中:

而後重複一次,把100放入局部變量表中,最後再將變量表中的500 和 100 取出,進行操做:

將500 和 100 進行一個除法運算,結果入棧:

最後就是輸出流,須要調用運行時常量池的常量:

最後調用invokevirtual(虛方法調用),而後返回:

返回:

程序計數器始終計算的都是當前代碼運行的位置,目的是爲了方便記錄 方法調用後可以正常返回,或者是進行了CPU切換後,也能回來到原來的代碼進行執行。

2.6 方法區的演進細節

只有Hotspot纔有永久代。BEA JRockit、IBM J9等來講,是不存在永久代的概念的。原則上如何實現方法區屬於虛擬機實現細節,不受《Java虛擬機規範》管束,並不要求統一。

Hotspot中方法區的變化:

版本 主要變化
JDK1.6及之前 有永久代(permanent generation),靜態變量存儲在永久代上
JDK1.7 有永久代,但已經逐步 「去永久代」,字符串常量池,靜態變量移除,保存在堆中
JDK1.8 無永久代,類型信息,字段,方法,常量保存在本地內存的元空間,但字符串常量池、靜態變量仍然在堆中。

JDK6的時候:

JDK7的時候:

JDK8的時候,元空間大小隻受物理內存影響:

爲何永久代要被元空間替代?

JRockit是和HotSpot融合後的結果,由於JRockit沒有永久代,因此HotSpot不須要配置永久代。隨着Java8的到來,HotSpot VM中再也見不到永久代了。可是這並不意味着類的元數據信息也消失了。這些數據被移到了一個與堆不相連的本地內存區域,這個區域叫作元空間(Metaspace)

因爲類的元數據分配在本地內存中,元空間的最大可分配空間就是系統可用內存空間,這項改動是頗有必要的,緣由有:

  • 爲永久代設置空間大小是很難肯定的

在某些場景下,若是動態加載類過多,容易產生Perm區的OOM。好比某個實際Web工程中,由於功能點比較多,在運行過程當中,要不斷動態加載不少類,常常出現致命錯誤。

「Exception in thread‘dubbo client x.x connector'java.lang.OutOfMemoryError:PermGen space」

而元空間和永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。所以,默認狀況下,元空間的大小僅受本地內存限制。

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

    • 主要是爲了下降Full GC

StringTable爲何要調整位置?

jdk7中將StringTable放到了堆空間中。由於永久代的回收效率很低,在Full GC的時候纔會觸發。而Full GC是老年代的空間不足、永久代不足時纔會觸發。

這就致使StringTable回收效率不高。而咱們開發中會有大量的字符串被建立,回收效率低,致使永久代內存不足。放到堆裏,能及時回收內存。

靜態變量存放在那裏?

靜態引用對應的對象實體始終都存在堆空間,只要是對象實例必然會在Java堆中分配

從《Java虛擬機規範》所定義的概念模型來看,全部Class相關的信息都應該存放在方法區之中,但方法區該如何實現,《Java虛擬機規範》並未作出規定,這就成了一件容許不一樣虛擬機本身靈活把握的事情。JDK7及其之後版本的HotSpot虛擬機選擇把靜態變量與類型在Java語言一端的映射class對象存放在一塊兒,存儲於Java堆之中。

3、方法區的垃圾回收

有些人認爲方法區(如HotSpot虛擬機中的元空間或者永久代)是沒有垃圾收集行爲的,其實否則。《Java虛擬機規範》對方法區的約束是很是寬鬆的,提到過能夠不要求虛擬機在方法區中實現垃圾收集。事實上也確實有未實現或未能完整實現方法區類型卸載的收集器存在(如JDK11時期的zGC收集器就不支持類卸載)。

通常來講這個區域的回收效果比較難使人滿意,尤爲是類型的卸載,條件至關苛刻。可是這部分區域的回收有時又確實是必要的。之前Sun公司的Bug列表中,曾出現過的若干個嚴重的Bug就是因爲低版本的HotSpot虛擬機對此區域未徹底回收而致使內存泄漏。

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

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

  • 類和接口的全限定名
  • 字段的名稱和描述符
  • 方法的名稱和描述符

HotSpot虛擬機對常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方引用,就能夠被回收。

回收廢棄常量與回收Java堆中的對象很是相似。(關於常量的回收比較簡單,重點是類的回收)

斷定一個常量是否「廢棄」仍是相對簡單,而要斷定一個類型是否屬於「再也不被使用的類」的條件就比較苛刻了。須要同時知足下面三個條件:

  • 該類全部的實例都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實例。
    加載該類的類加載器已經被回收,這個條件除非是通過精心設計的可替換類加載器的場景,如osGi、JSP的重加載等,不然一般是很難達成的。
  • 該類對應的java.lang.C1ass對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。I Java虛擬機被容許對知足上述三個條件的無用類進行回收,這裏說的僅僅是「被容許」,而並非和對象同樣,沒有引用了就必然會回收。關因而否要對類型進行回收,HotSpot虛擬機提供了-Xnoclassgc參數進行控制,還可使用-verbose:class 以及 -XX:+TraceClass-Loading-XX:+TraceClassUnLoading查看類加載和卸載信息
  • 在大量使用反射、動態代理、CGLib等字節碼框架,動態生成JSP以及oSGi這類頻繁自定義類加載器的場景中,一般都須要Java虛擬機具有類型卸載的能力,以保證不會對方法區形成過大的內存壓力。

參考

深刻理解Java虛擬機:JVM高級特性與最佳實踐(第3版)

觸摸java常量池

相關文章
相關標籤/搜索