JVM之方法區

返回主博客

返回上一層

 

方法區

9.1 棧,堆,方法區交互關係

 

9.2 方法區的理解

The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods  used in class and instance initialization and interface initialization.

The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.

A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the method area, as well as, in the case of a varying-size method area, control over the maximum and minimum method area size.

The following exceptional condition is associated with the method area:

  • If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an OutOfMemoryError.

Java虛擬機有一個方法區域,在所有Java虛擬機線程之間共享。方法區域類似於常規語言編譯代碼的存儲區域,或者類似於操作系統進程中的“文本”段。它存儲每類結構,如運行時常量池、字段和方法數據,以及方法和構造函數的代碼,包括在類和實例初始化以及接口初始化中使用的特殊方法。

方法區域是在虛擬機啓動時創建的。儘管方法區域在邏輯上是堆的一部分,但是簡單的實現可以選擇不對其進行垃圾收集或壓縮。此規範不強制指定用於管理已編譯代碼的方法區域或策略的位置。方法區域可以是固定大小的,或者可以根據計算的需要進行擴展,並且如果不需要更大的方法區域,則可以收縮。方法區域的內存不需要是連續的。

Java虛擬機實現可以提供程序員或用戶對方法區域的初始大小的控制,並且,在大小不同的方法區域的情況下,還可以提供對最大和最小方法區域大小的控制。

 

如果類定義太多,方法區則會OOM

java.lang.OutOfMemoryError: PermGen space

java.lang.OutOfMemoryError: Metaspace

現在來看,當年使用永久代,不是好的idea,導致容易OOM(-XX:MaxPermSize)

到JDK8,廢棄永久代,改用與Jrockit,J9,一樣,在本地內存中實現的元空間代替。但是也是可以設置上限,也會OOM

元空間不像永久代,元空間不使用java內存,而是本地內存。

 

9.3 方法區大小設置和OOM

方法區的大小可以不必是固定的

1.7之前:

-XX:PermSize 設置初始大小 默認20.75M

-XX:MaxPerSize來設置最大配置內存 32位機型默認64M,64位機型默認82M

1.8之後

-XX:MetaspaceSize 默認和平臺相關, 推薦大一些,

-XX:MaxMetaspaceSize   -1 不限制,推薦不限制

對於64位的的服務器端的JVM來說,其默認的-XX:MetaSpaceSize是21M,這是初始高水平線,一旦觸及,Full GC將會觸發,並卸載無用的類。並且調高或調低“高水平線”。

 

代碼:可以通過CGLib或者ASM不斷建類,並在1.8的jre環境下配置JVM參數(-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m)使方法區溢出。

package com.jack.ascp.purchase.app.test.vm.methodarea;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
/**
 * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
 * @author : 江鵬亮
 * @date : 2020-06-27 11:21
 **/
public class MethodAreaOOMTest extends ClassLoader{
    public static void main(String[] args) {
        MethodAreaOOMTest oomTest = new MethodAreaOOMTest();
        ClassWriter classWriter = new ClassWriter(0);
        int num = 0;
        try {
            for (int i = 0; i < 10000; i++) {
                classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                byte[] code = classWriter.toByteArray();
                oomTest.defineClass("Class" + i, code, 0, code.length);
                num ++;
            }
        } catch (Error e) {
            e.printStackTrace();
        }
        System.out.println(num);
    }
}

結果:

 

如何解決OOM(這個得看經驗的,比如本來之前內存肯定夠用的,這次發佈突然就OOM了,肯定和這次發佈有關了)

1、要解決OOM異常或者heap space的異常,一般手段是通過內存映像分析工具(如Eclipsing Memory Analyzer)對dump出來的堆轉儲快照進行分析。重點是確認內存中的對象是否是必要的,也就是要分析是內存泄漏還是內存溢出。

2、如果內存泄漏,可以進一步通過工具查看泄漏對象到GC Root的引用鏈,於是就能找到內存泄漏對象是通過怎麼樣的路徑與GCRoot相關聯的,並導致垃圾回收器無法自動回收。然後並將其斷開

3、不存在內存泄漏,先排查某些對象是否有必要生命週期過長,並嘗試修改他,或者擴容。

 

9.4 方法區中的存儲數據

方法區用於存儲已經被虛擬機加載的類型信息,常量,靜態變量(其實字符串常量已經不在了),即時編譯器編譯後的代碼緩存等。

 

1、存儲類型信息:

對於class ,interface,enum, annotation 存儲以下信息

      1、全限定名,2、父類、3、修飾符 4 接口列表

2、Fileds信息 以及他們的順序

     域名稱,域類型,域修飾符(public,private,final,volatile,transient等)。

3、方法信息

      名稱,返回類型,參數數量和類型,方法修飾符,異常表。

 

4、加載的ClassLoader

5、靜態變量

6、全局靜態變量 final static修飾的在編譯期間決定,在準備階段賦值

 

9.5 運行時常量池VS 常量池

9.6 方法區演進過程

  只有HotSpot VM纔會有永久代

 

爲什麼將永久代替換成本地內存管理的元空間?

http://openjdk.java.net/jeps/122

Motivation

This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.

Description

Move part of the contents of the permanent generation in Hotspot to the Java heap and the remainder to native memory.

Hotspot's representation of Java classes (referred to here as class meta-data) is currently stored in a portion of the Java heap referred to as the permanent generation. In addition, interned Strings and class static variables are stored in the permanent generation. The permanent generation is managed by Hotspot and must have enough room for all the class meta-data, interned Strings and class statics used by the Java application. Class metadata and statics are allocated in the permanent generation when a class is loaded and are garbage collected from the permanent generation when the class is unloaded. Interned Strings are also garbage collected when the permanent generation is GC'ed.

The proposed implementation will allocate class meta-data in native memory and move interned(拘留,禁閉,關押) Strings and class statics to the Java heap. Hotspot will explicitly allocate and free the native memory for the class meta-data. Allocation of new class meta-data would be limited by the amount of available native memory rather than fixed by the value of -XX:MaxPermSize, whether the default or specified on the command line.

Allocation of native memory for class meta-data will be done in blocks of a size large enough to fit multiple pieces of class meta-data. Each block will be associated with a class loader and all class meta-data loaded by that class loader will be allocated by Hotspot from the block for that class loader. Additional blocks will be allocated for a class loader as needed. The block sizes will vary depending on the behavior of the application. The sizes will be chosen so as to limit internal and external fragmentation. Freeing the space for the class meta-data would be done when the class loader dies by freeing all the blocks associated with the class loader. Class meta-data will not be moved during the life of the class.

意思就是jrockit用元空間,hotspot就用元空間了。

1、爲永久代設置空間大小很難確定的。

     在某些情況下,如果動態加載類過多,容易Perm區OOM

2、對永久代調優時很困難的 而且FULL  GC效率低。

  VM判斷類是否廢棄是要經過一系列很多判斷的,而且也不能100%保證類需要廢棄。 判斷類和常量是否不被使用是很困難的,而且類不用了,我們爲什麼不直接在代碼中刪了?既然不刪,說明都是有可能會用的。(當然也有可能是某些框架啓動前期需要用到某些類,比如Spring 啓動用到AntPathMacher 等很多工具類,以及它定義的很多常量,後面有很多基本就不用了,這種容器框架啓動時用到了很多類以及對象,其實後面不用了,確實需要優化,或者有時候我們代碼中經常會用到第三方或者java提供的工具類,你用一個,我用一個之類的情況)。

 

StringTable爲什麼要調整

jdk1.7 將StringTable 放到了堆空間。因爲永久代回收效率低,在full GC 的時候纔會觸發,而Full GC是因爲老年代空間不足,永久代不足時纔會觸發,這就會導致StringTable的回收效率不高,我們在開發中會有大量字符串被回收,回收效率低,導致你永久代內存不足,放到堆中,能及時回收。

 

如何證明靜態變量放到哪裏。

分別使用一下JVM參數,在1.6 1.7 1.8運行 

/**
 * @author : 江鵬亮
 * -Xms600m -Xmx600m -XX:SurvivorRatio=8 -XX:PermSize=300m -XX:MaxPermSize=300m -XX:+PrintGCDetails
 * -Xms600m -Xmx600m -XX:SurvivorRatio=8 -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails
 * @date : 2020-06-27 15:37
 **/
public class StaticFiledTest {
    /**
     *  200M的arr 堆比值: 160 : 20 : 20 : 400,
     *  爲了是結果清晰,讓這個arr大小大於160, 從而直接觸發GC,晉身老年代,就可以看見老年代的佔用爲204800K
     */
    private static byte[] arr = new byte[1024 * 1024 * 200];
    public static void main(String[] args) {
        System.out.println(arr);
    }
}

靜態變量的實體對象從始至終都在堆中(1.6-1.8)但是靜態變量的引用在1.7之後隨着java.lang.Class 對象實例存放在堆中


/**
 * -Xms600m -Xmx600m -XX:SurvivorRatio=8 -XX:PermSize=300m -XX:MaxPermSize=300m -XX:+PrintGCDetails
 * -Xms600m -Xmx600m -XX:SurvivorRatio=8 -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails
 * @author : 江鵬亮
 * @date : 2020-06-27 16:06
 **/
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");
         }
    }
    static class ObjectHolder {
    }
    public static void main(String[] args) {
        Test test = new Test();
        test.foo();
    }
}

案例中

staticObj 引用隨着java.lang.Class類型的對象實例,一起放到堆中(jdk1.7以後)

instanceObj 引用隨着 ObjectHolder類型的對象實例,一起放到堆中

localObj 引用反正操作數棧和局部變量表上。

 

方法區的垃圾收集(GarbageCollect

java虛擬機規範中說:方法區不要求進行垃圾回收和壓縮。

(事實上確實有未實現或未能完整實現方法區類型卸載的收集器存在,如ZGC)

一般來說,這個區域的回收效果比較難令人滿意,尤其類型的卸載,條件相當苛刻。但是有時又由必要。

sun公司的bug列表中,曾出現若干嚴重問題的bug就是由於低版本的hotSpot虛擬機未完整回收。導致內存泄漏。

 

主要回收兩個部分:常量池中廢棄的常量和不再使用的類型。

1、hotSpot對常量池的回收策略時很明確的,只要常量池中的常量沒有任何地方引用就可以被回收。

2、要想判斷一個類是否可以被回收滿足下面三個條件:

    1、所有類(以及其派生的類的)的實例已經被回收。

    2、類加載器已經被回收。

    3、class 對象不再被任何地方引用

就算滿足也不一定就可以被回收。

運行時數據區總結:

 

 

 

 

 

10、對象的實例化,內存佈局域訪問定位

對象創建對應字節碼:

public class ObjectTest {
    int id;
    String name;
    public ObjectTest(int id, String name) {
        this.id = id;
        this.name = name;
    }
    public static void main(String[] args) {
        int id = 0;
        String name = "aaaa";
        ObjectTest objectTest = new ObjectTest(id, name);
    }
}

 0: iconst_0     常量池中加載0
 1: istore_1     將0存到局部變量表1的位置
 2: ldc   #4     常量池中加載“aaaa”
 4: astore_2     將字符串“aaaa” 存儲到局部變量表 2 的位置
 5: new   #5     new ObjectTest並對屬性默認初始化(如int默認0,boolean默認false), 並將其返回的引用放到棧頂
 8: dup          將棧頂元素(也就是剛剛new出來的對象引用)dup 一份,這樣棧頂的兩個元素都是這個new出來的對象引用
 9: iload_1      加載局部變量表1的元素,即 0
10: aload_2      加載局部變量表2的元素,即 "aaaa"
11: invokespecial #6 //Method "<init>":(ILjava/lang/String;)V   調用init方法,因爲調用完,對象引用就出棧了,所以前面dup了一份
14: astore_3     將對象引用存到局部變量表3的位置
15: return

重點說明:

 1、new的時候對象的大小就固定了,我猜:byte,short,char,boolean,float,int佔32位,地址引用佔32位,long和double佔兩個32

 2、之所以dup一份是因爲調用構造方法後,棧頂的一個對象引用就pop了,所以要先dup一個,用於後面的astore。

 

對象的內存佈局

 

對象訪問定位

句柄訪問:

 

使用直接指針(hotsport使用)

比較:

句柄訪問需要再次開闢空間,還得間接訪問一次。

但是當對象發生移動時,句柄訪問就只需要修改句柄池部分就可以了。而直接指針方式就得修改棧中的指針。

 

11、直接內存(本地內存)

不是JVM運行時數據區的一部分,不在java虛擬機規範中

元空間使用的就是本地內存

是C++直接向系統直接申請的內存。

來源於NIO,通過堆中的directByteBuffer操作的native內存。

什麼是NIO ?對比IO

 

IO (New IO/ IO)
blocking Non-blocking
使用byte[]或者char[]進行傳輸。 使用Buffer進行傳輸。
基於 Stream  基於 Channel
   

 

代碼案例:

public class BufferTest {
    private static final int  BUFFER = 1024 * 1024 * 1024;
    public static void main(String[] args) {
        //直接分配比迪內存空間
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
        System.out.println("直接內存分配完畢");
        Scanner scanner = new Scanner(System.in);
        scanner.next();
        System.out.println("直接內存開始釋放");
        byteBuffer = null;
        System.gc();
        scanner.next();
    }
}

 

查看任務管理器,該進程佔用了1G多內存

通常我們使用直接內存的速度優於使用堆內存,讀寫性能高。

     基於性能考慮,讀寫頻繁場合可以考慮使用直接內存。

     Java的NIO庫允許java程序使用直接內存。

 

直接內存也有可能OOM (java.lang.OutOfMemoryError: Direct buffer memory)

由於直接內存在堆外,他的大小不會直接受限於-Xms。但是系統內存是有限的,java堆和直接內存的總和依然受限於操作系統能給出的最大內存。

缺點

   分配回收成本高

   不受JVM內存回收管理

可以用-XX:MaxDirectMemorySize 設置。

如果不指定默認於-Xmx一致。

他的大小使用jvisualvm或者jprofiler是查看不到的。可以用任務管理器查看。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章