JVM_06 運行時數據區3-方法區

完整JVM學習筆記請戳

1. 堆、棧、方法區的交互關係

運行時數據區結構圖

堆、棧、方法區的交互關係
html

2. 方法區的理解

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

  • 方法區(Method Area)與Java堆同樣,是各個線程共享的內存區域
  • 方法區在JVM啓動時就會被建立,而且它的實際的物理內存空間中和Java堆區同樣均可以是不連續的
  • 方法區的大小,跟堆空間同樣,能夠選擇固定大小或者可拓展
  • 方法區的大小決定了系統能夠保存多少個類,若是系統定義了太多的類,致使方法區溢出,虛擬機一樣會拋出內存溢出錯誤:java.lang.OutOfMemoryError:PermGen space 或者 java.lang,OutOfMemoryError:Metaspace,好比:
    • 加載大量的第三方jar包;
    • Tomcat部署的工程過多;
    • 大量動態生成反射類;
  • 關閉JVM就會釋放這個區域的內存
    例,使用jvisualvm查看加載類的個數
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...");
    }
}
複製代碼
  • 在jdk7及之前,習慣上把方法區稱爲永久代。jdk8開始,使用元空間取代了永久代
  • 本質上,方法區和永久代並不等價。僅是對hotSpot而言的。《java虛擬機規範》對如何實現方法區,不作統一要求。例如:BEA JRockit/IBM J9中不存在永久代的概念
    • 如今看來,當年使用永久代,不是好的idea。致使Java程序更容易OOM(超過-XX:MaxPermSize上限)

方法區在jdk7及jdk8的落地實現 git

  • 在jdk8中,終於徹底廢棄了永久代的概念,改用與JRockit、J9同樣在本地內存中實現的元空間(Metaspace)來代替
    github

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

  • 永久代、元空間並不僅是名字變了。內部結構也調整了算法

  • 根據《Java虛擬機規範》得規定,若是方法區沒法知足新的內存分配需求時,將拋出OOM異常.數據庫

3.設置方法區大小與OOM

方法區的大小沒必要是固定的,jvm能夠根據應用的須要動態調整。
jdk7及之前:編程

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

jdk8及之後:windows

  • 元數據區大小可使用參數一XX:MetaspaceSize和一XX :MaxMetaspaceSize指定,替代上述原有的兩個參數。
  • 默認值依賴於平臺。windows下,一XX:MetaspaceSize是21M,一 XX:MaxMetaspaceSize的值是一1, 即沒有限制。| I
  • 與永久代不一樣,若是不指定大小,默認狀況下,虛擬機會耗盡全部的可用系統內存。 若是元數據區發生溢出,虛擬機同樣會拋出異常OutOfMemoryError: Metaspace
  • -XX:MetaspaceSize: 設置初始的元空間大小。對於一個64位的服務器端JVM來講, 其默認的一XX :MetaspaceSize值爲21MB.這就是初始的高水位線,一旦觸及這個水位線,Full GC將會被觸發並卸載沒用的類(即這些類對應的類加載器再也不存活),而後這個高水位線將會重置。新的高水位線的值取決於GC後釋放了多少元空間。若是釋放的空間不足,那麼在不超過MaxMetaspaceSize時,適當提升該值。若是釋放空間過多,則適當下降該值。
  • 若是初始化的高水位線設置太低,.上 述高水位線調整狀況會發生不少次。經過垃圾回收器的日誌能夠觀察到Full GC屢次調用。爲了不頻繁地GC,建議將- XX :MetaspaceSize設置爲一個相對較高的值。
*  jdk7及之前:
 *  查詢 jps  -> jinfo -flag PermSize [進程id]
 *  -XX:PermSize=100m -XX:MaxPermSize=100m
 *
 *  jdk8及之後:
 *  查詢 jps  -> jinfo -flag MetaspaceSize [進程id]
 *  -XX:MetaspaceSize=100m  -XX:MaxMetaspaceSize=100m
複製代碼

方法區OOM

  • 一、要解決00M異常或heap space的異常,通常的手段是首先經過內存映像分析工具(如Eclipse Memory Analyzer) 對dump出來的堆轉儲快照進行分析,重點是確認內存中的對象是不是必要的,也就是要先分清楚究竟是出現了內存泄漏(Memory Leak)仍是內存溢出(Memory 0verflow) 。
  • 二、若是是內存泄漏,可進一步經過工具查看泄漏對象到GC Roots 的引用鏈。因而就能找到泄漏對象是經過怎樣的路徑與GCRoots相關聯並致使垃圾收集器沒法自動回收它們的。掌握了泄漏對象的類型信息,以及GC Roots引用鏈的信息,就能夠比較準確地定位出泄漏代碼的位置。
  • 三、若是不存在內存泄漏,換句話說就是內存中的對象確實都還必須存活着,那就應當檢查虛擬機的堆參數(一Xmx與一Xms) ,與機器物理內存對比看是否還能夠調大,從代碼_上檢查是否存在某些對象生命週期過長、持有狀態時間過長的狀況,嘗試減小程序運行期的內存消耗。

如下代碼在JDK8環境下會報 Exception in thread "main" java.lang.OutOfMemoryError: Compressed class space 錯誤數組

/**
 * jdk6/7中:
 * -XX:PermSize=10m -XX:MaxPermSize=10m
 *
 * jdk8中:
 * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
 *
 */
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);
        }
    }
}
複製代碼

4.方法區的內部結構

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

類型信息

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

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

域信息(成員變量)

  • JVM必須在方法區中保存類型的全部域的相關信息以及域的聲明順序。
  • 域的相關信息包括:域名稱、 域類型、域修飾符(public, private, protected, static, final, volatile, transient的某個子集)

方法信息

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

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

non-final的類變量

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

如下代碼不會報空指針異常

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!");
    }
}
複製代碼

全局常量 static final 被聲明爲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
複製代碼

運行時常量池

常量池

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.3
  • 一個有效的字節碼文件中除了包含類的版本信息、字段、方法以及接口等描述信息外,還包含一項信息那就是常量池表(Constant Poo1 Table),包括各類字面量和對類型域和方法的符號引用。
  • 一個 java 源文件中的類、接口,編譯後產生一個字節碼文件。而 Java 中的字節碼須要數據支持,一般這種數據會很大以致於不能直接存到字節碼裏,換另外一種方式,能夠存到常量池這個字節碼包含了指向常量池的引用。在動態連接的時候會用到運行時常量池.
  • 好比以下代碼,雖然只有 194 字節,可是裏面卻使用了 string、System、Printstream 及 Object 等結構。這裏代碼量其實已經很小了。若是代碼多,引用到的結構會更多!
Public class Simpleclass {
public void sayhelloo() {
    System.out.Println (hello) }
}
複製代碼

幾種在常量池內存儲的數據類型包括:

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

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

運行時常量池

  • 運行時常量池( Runtime Constant Pool)是方法區的一部分。
  • 常量池表(Constant Pool Table)是Class文件的一部分,==用於存放編譯期生成的各類字面量與符號引用==,這部份內容將在類加載後存放到方法區的運行時常量池中。
  • 運行時常量池,在加載類和接口到虛擬機後,就會建立對應的運行時常量池。
  • JVM爲每一個已加載的類型(類或接口)都維護一個常量池。池中的數據項像數組項同樣,是經過索引訪問的。
  • 運行時常量池中包含多種不一樣的常量,包括編譯期就已經明確的數值字面量,也包括到運行期解析後纔可以得到的方法或者字段引用。此時再也不是常量池中的符號地址了,這裏換爲真實地址。
    • 運行時常量池,相對於Class文件常量池的另外一重要特徵是:==具有動態性==。
      • String.intern()
  • 運行時常量池相似於傳統編程語言中的符號表(symbol table) ,可是它所包含的數據卻比符號表要更加豐富一些。
  • 當建立類或接口的運行時常量池時,若是構造運行時常量池所需的內存空間超過了方法區所能提供的最大值,則JVM會拋OutOfMemoryError異常。

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);
    }
}
複製代碼

main方法的字節碼指令

0 sipush 500
 3 istore_1
 4 bipush 100
 6 istore_2
 7 iload_1
 8 iload_2
 9 idiv
10 istore_3
11 bipush 50
13 istore 4
15 getstatic #2 <java/lang/System.out>
18 iload_3
19 iload 4
21 iadd
22 invokevirtual #3 <java/io/PrintStream.println>
25 return
複製代碼

6.方法區的演進細節

  1. 首先明確:只有HotSpot纔有永久代。 BEA JRockit、IBM J9等來講,是不存在永久代的概念的。原則上如何實現方法區屬於虛擬機實現細節,不受《Java虛擬機規範》管束,並不要求統一。
  2. Hotspot中 方法區的變化:
  • jdk1.6及以前:有永久代(permanent generation) ,靜態變量存放在 永久代上
  • jdk1.7:有永久代,但已經逐步「去永久代」,字符串常量池、靜態變量移除,保存在堆中
  • jdk1.8及以後: 無永久代,類型信息、字段、方法、常量保存在本地內存的元空間,但字符串常量池、靜態變量仍在堆

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

  • 隨着Java8的到來,HotSpot VM中再也見不到永久代了。可是這並不意味着類.的元數據信息也消失了。這些數據被移到了一個與堆不相連的本地內存區域,這個區域叫作元空間( Metaspace )。
  • 因爲類的元數據分配在本地內存中,元空間的最大可分配空間就是系統可用內存空間。
  • 這項改動是頗有必要的,緣由有:
    • 1)爲永久代設置空間大小是很難肯定的。 在某些場景下,若是動態加載類過多,容易產生Perm區的O0M。好比某個實際Web工程中,由於功能點比較多,在運行過程當中,要不斷動態加載不少類,常常出現致命錯誤。 "Exception in thread' dubbo client x.x connector’java.lang.OutOfMemoryError: PermGenspace" 而元空間和永久代之間最大的區別在於:==元空間並不在虛擬機中,而是使用本地內存。所以,默認狀況下,元空間的大小僅受本地內存限制==。
    • 2)對永久代進行調優是很困難的。

StringTable 爲何要調整

jdk7中將StringTable放到了堆空間中。由於永久代的回收效率很低,在full gc的時候纔會觸發。而full GC 是老年代的空間不足、永久代不足時纔會觸發。這就致使了StringTable回收效率不高。而咱們開發中會有大量的字符串被建立,回收效率低,致使永久代內存不足。放到堆裏,能及時回收內存.

如何證實靜態變量存在哪

/**
 * 《深刻理解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 堆之中,從咱們的實驗中也明確驗證了這一點.

7.方法區的垃圾回收

  有些人認爲方法區(如Hotspot,虛擬機中的元空間或者永久代)是沒有垃圾收集行爲的,其實否則。《Java 虛擬機規範》對方法區的約束是很是寬鬆的,提到過能夠不要求虛擬機在方法區中實現垃圾收集。事實上也確實有未實現或未能完整實現方法區類型卸載的收集器存在(如 JDK11 時期的 2GC 收集器就不支持類卸載)。
  通常來講這個區域的回收效果比較難使人滿意,尤爲是類型的卸載,條件至關苛刻。可是這部分區域的回收有時又確實是必要的。之前 Sun 公司的 Bug 列表中,曾出現過的若干個嚴重的 Bug 就是因爲低版本的 Hotspot 虛擬機對此區域未徹底回收而致使內存泄漏。
  方法區的垃圾收集主要回收兩部份內容:常量池中廢奔的常量和再也不使用的類型

  • 先來講說方法區內常量池之中主要存放的兩大類常量:字面量和符號引用。 字面量比較接近Java語言層次的常量概念,如文本字符串、被聲明爲final的常量值等。而符號引用則屬於編譯原理方面的概念,包括下面三類常量:
    • 一、類和接口的全限定名
    • 二、字段的名稱和描述符
    • 三、方法的名稱和描述符
  • HotSpot虛擬機對常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方引用,就能夠被回收。
  • 回收廢棄常量與回收Java堆中的對象很是相似。
  • ·斷定一個常量是否「廢棄」仍是相對簡單,而要斷定一個類型是否屬於「再也不被使用的類」的條件就比較苛刻了。須要同時知足下面三個條件:
    • 該類全部的實例都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實例。
    • 加載該類的類加載器已經被回收,這個條件除非是通過精心設計的可替換類加載器的場景,如OSGi、JSP的重加載等,不然一般是很難達成的。|】
    • 該類對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。
  • Java虛擬機被容許對知足上述三個條件的無用類進行回收,這裏說的僅僅是「被容許」,而並非和對象同樣,沒有引用了就必然會回收。關因而否要對類型進行回收,HotSpot虛擬機提供了一Xnoclassgc 參數進行控制,還可使用一verbose:class以及一XX: +TraceClass一Loading、一XX:+TraceClassUnLoading查 看類加載和卸載信息
  • 在大量使用反射、動態代理、CGLib等字節碼框架,動態生成JSP以及oSGi這類頻繁自定義類加載器的場景中,一般都須要Java虛擬機具有類型卸載的能力,以保證不會對方法區形成過大的內存壓力。

8. 總結

面試題:

百度

三面:說一下JVM內存模型吧,有哪些區?分別幹什麼的?

螞蟻金服:

Java8的內存分代改進
JVM內存分哪幾個區,每一個區的做用是什麼?
一面: JVM內存分佈/內存結構?棧和堆的區別?堆的結構?爲何兩個survivor區?
二面: Eden和Survior的比例分配

小米:

jvm內存分區,爲何要有新生代和老年代

字節跳動:

二面: Java的內存分區
二面:講講jvm運行時數據庫區
何時對象會進入老年代?

京東:

JVM的內存結構,Eden和Survivor比例 。
JVM內存爲何要分紅新生代,老年代,持久代。新生代中爲何要分爲Eden和Survivor。

天貓:

一面: Jvm內存模型以及分區,須要詳細到每一個區放什麼。
一面: JVM的內存模型,Java8作了什麼修改

拼多多:

JVM內存分哪幾個區,每一個區的做用是什麼?

美團:

java內存分配
jvm的永久代中會發生垃圾回收嗎?
一面: jvm內存分區,爲何要有新生代和老年代?



JVM學習代碼及筆記(陸續更新中...)

【代碼】
github.com/willShuhuan…
【筆記】
JVM_01 簡介
JVM_02 類加載子系統
JVM_03 運行時數據區1- [程序計數器+虛擬機棧+本地方法棧]
JVM_04 本地方法接口
JVM_05 運行時數據區2-堆
JVM_06 運行時數據區3-方法區
JVM_07 運行時數據區4-對象的實例化內存佈局與訪問定位+直接內存
JVM_08 執行引擎(Execution Engine)
JVM_09 字符串常量池StringTable
JVM_10 垃圾回收1-概述+相關算法
JVM_11 垃圾回收2-垃圾回收相關概念
JVM_12 垃圾回收3-垃圾回收器

相關文章
相關標籤/搜索