JVM運行時數據區--方法區


運行時數據區結構圖(溫習):

 

 

 堆、棧、方法區的交互關系

 

 

 

方法區的理解

  • 方法區(Method Area)與Java堆一樣,是各個線程共享的內存區域
  • 方法區在JVM啟動時就會被創建,並且它的實際的物理內存空間中和Java堆區一樣都可以是不連續的
  • 方法區的大小,跟堆空間一樣,可以選擇固定大小或者可拓展
  • 方法區的大小決定了系統可以保存多少個類,如果系統定義了太多的類,導致方法區溢出,虛擬機同樣會拋出內存溢出錯誤:java.lang.OutOfMemoryError:PermGen space 或者 java.lang,OutOfMemoryError:Metaspace,比如:
    • 加載大量的第三方jar包;
    • Tomcat部署的工程過多;
    • 大量動態生成反射類;
  • 關閉JVM就會釋放這個區域的內存。

例,使用jvisualvm查看加載類的個數:下面是一個demo演示

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

 

 HotSpot虛擬機中方法區的演進

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

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

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

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

  • 根據《Java虛擬機規范》的規定,如果方法區無法滿足新的內存分配需求時,將拋出OOM異常.。

設置方法區大小的參數

方法區的大小不必是固定的,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時,適當提高該值。如果釋放空間過多,則適當降低該值。
  • 如果初始化的高水位線設置過低,.上 述高水位線調整情況會發生很多次。通過垃圾回收器的日志可以觀察到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:(內存泄漏、內存溢出)

  • 1、要解決00M異常或heap space的異常,一般的手段是首先通過內存映像分析工具(如Eclipse Memory Analyzer) 對dump出來的堆轉儲快照進行分析,重點是確認內存中的對象是否是必要的,也就是要先分清楚到底是出現了內存泄漏(Memory Leak)還是內存溢出(Memory 0verflow) 。
  • 2、如果是內存泄漏,可進一步通過工具查看泄漏對象到GC Roots 的引用鏈(堆當中的閑置對象由於引用鏈的引用關系無法被回收,雖然它已經屬於閑置的資源)。於是就能找到泄漏對象是通過怎樣的路徑與GCRoots相關聯並導致垃圾收集器無法自動回收它們的。掌握了泄漏對象的類型信息,以及GC Roots引用鏈的信息,就可以比較准確地定位出泄漏代碼的位置。
  • 3、如果不存在內存泄漏,換句話說就是內存中的對象確實都還必須存活着,那就應當檢查虛擬機的堆參數(一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);
        }
    }
}

方法區的內部結構

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

類型信息

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

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

域信息(成員變量)

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

方法信息(method)

JVM必須保存所有方法的以下信息,同域信息一樣包括聲明順序:

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

non-final的類變量(非聲明為final的static靜態變量)

  • 靜態變量和類關聯在一起,隨着類的加載而加載,他們成為類數據在邏輯上的一部分
  • 類變量被類的所有實例所共享,即使沒有類實例你也可以訪問它。

以下代碼不會報空指針異常:

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是在類加載的准備階段才被賦值(還記得clinit嗎)。
 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

 復習:

1.clinit()即“class or interface initialization method”,注意他並不是指構造器init()

2.此方法不需要定義,是javac編譯器自動收集類中的所有類變量的賦值動作和靜態代碼塊中的語句合並而來。 

3.如果沒有靜態變量,那么字節碼文件中就不會有clinit方法

常量池

  • 一個有效的字節碼文件中除了包含類的版本信息、字段、方法以及接口等描述信息外,還包含一項信息那就是常量池表(Constant Poo1 Table),包括各種字面量和對類型域和方法的符號引用。
  • 一個 java 源文件中的類、接口,編譯后產生一個字節碼文件。而 Java 中的字節碼需要數據支持,通常這種數據會很大以至於不能直接存到字節碼里,換另一種方式,可以存到常量池;而這個字節碼包含了指向常量池的引用。在動態鏈接的時候會用到運行時常量池.
  • 比如如下代碼,雖然只有 194 字節,但是里面卻使用了 string、System、Printstream 及 Object 等結構。這里代碼量其實已經很小了。如果代碼多,引用到的結構會更多!
Public class Simpleclass {
public void sayhelloo() {
    System.out.Println (hello) }
}

小結:字節碼當中的常量池結構(constant pool),可以看做是一張表,虛擬機指令根據這張常量表找到要執行的類名,方法名,參數類型、字面量等信息。

運行時常量池

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

方法區的演進細節

首先明確:只有HotSpot才有永久代。 BEA JRockit、IBM J9等來說,是不存在永久代的概念的。原則上如何實現方法區屬於虛擬機實現細節,不受《Java虛擬機規范》管束,並不要求統一。

Hotspot中 方法區的變化:

  • jdk1.6及之前:有永久代(permanent generation) ,靜態變量存放在 永久代上。
  • jdk1.7:有永久代,但已經逐步“去永久代”,字符串常量池、靜態變量移除,保存在堆中。注意:
  • jdk1.8及之后: 無永久代,類型信息、字段、方法、常量保存在本地內存的元空間,但字符串常量池、靜態變量仍留在堆空間.

 

 

注意:

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()方法棧幀的局部變量表中。

我的理解:至2020/7/17日

就是實例變量(也就是成員變量)的生命周期是跟隨對象的,屬於對象的內容。而對象實例化之后,存放在堆中,所有成員變量也會存在堆中。

而類中的其他變量,如靜態變量,也叫類變量,屬於類型的信息,是存放在方法區中的。但是、但是!!JDK7以上版本,靜態域存儲於定義類型的Class對象中,Class對象如同堆中其他對象一樣,存在於GC堆中。后面證明!!!

局部變量是屬於方法(對應棧幀)的,也就存在棧中,即棧幀當中的局部變量表當中。

誤區:

只要是對象的引用都存在於棧中,誰教你的?,又是誰教我的?wtnmd...

記着:

存在棧中的變量是方法中定義的局部變量。

對象中定義的實例變量存儲在堆中。

上述代碼的地址測試(JHSDB這個工具在jdk9及以上才有):

hsdb>scanoops 0x00007f32c7800000 0x00007f32c7b50000 JHSDB_ _TestCase$Obj ectHolder
0x00007f32c7a7c458 JHSDB_ TestCase$Obj ectHolder
0x00007f32c7a7c480 JHSDB_ TestCase$Obj ectHolder
0x00007f32c7a7c490 JHSDB_ TestCase$Obj ectHolder

實例本身存放位置

測試發現:三個對象的數據在內存中的地址都落在Eden區范圍內(scanoops 0x00007f32c7800000 0x00007f32c7b50000 JHSDB_ _TestCase$Obj ectHolder),所以結論:只要是對象實例必然會在Java堆中分配

接着,找到了一個引用該staticObj對象的地方,是在一個java.lang.Class的實例里,並且給出了這個實例的地址。

通過Inspector查看該對象實例,可以清楚看到這確實是一個 java.lang.Class類型的對象實例里面有一個名為staticObj的實例字段:

  • 從《Java 虛擬機規范》所定義的概念模型來看,所有 Class 相關的信息都應該存放在方法區之中,但方法區該如何實現,《Java 虛擬機規范》並未做出規定,這就成了一件允許不同虛擬機自己靈活把握的事情。
  • JDK7 及其以后版本的 Hotspot 虛擬機選擇把   靜態變量與類型在 Java 語言一端的映射 Class 對象    存放在一起,存儲於java 堆之中,從我們的實驗中也明確驗證了這一點.。
  • 到此長出一口氣...🤭

Class對象是存放在堆區的,不是方法區,這點很多人容易犯錯。

java.lang.Class對象,它們從來都是“普通”Java對象,跟其它Java對象一樣存在普通的Java堆(GC堆的一部分)里。

類的元數據(元數據並不是類的Class對象!Class對象是加載的最終產品,類的方法代碼,變量名,方法名,訪問權限,返回值等等都是在方法區的)才是存在方法區的。

方法區的垃圾回收

  有些人認為方法區(如Hotspot,虛擬機中的元空間或者永久代)是沒有垃圾收集行為的,其實不然。《Java 虛擬機規范》對方法區的約束是非常寬松的,提到過可以不要求虛擬機在方法區中實現垃圾收集。事實上也確實有未實現或未能完整實現方法區類型卸載的收集器存在(如 JDK11 時期的ZGC 收集器就不支持類卸載)。

       一般來說這個區域的回收效果比較難令人滿意,尤其是類型的卸載,條件相當苛刻。但是這部分區域的回收有時又確實是必要的。以前 Sun 公司的 Bug 列表中,曾出現過的若干個嚴重的 Bug 就是由於低版本的 Hotspot 虛擬機對此區域未完全回收而導致內存泄漏。
方法區的垃圾收集主要回收兩部分內容:常量池中廢奔的常量不再使用的類型。

常量池中廢奔的常量

  • 先來說說方法區內常量池之中主要存放的兩大類常量:字面量和符號引用。 字面量比較接近Java語言層次的常量概念,如文本字符串、被聲明為final的常量值等。而符號引用則屬於編譯原理方面的概念。
  • 常量池中包括下面三類常量:
    • 1、類和接口的全限定名
    • 2、字段的名稱和描述符
    • 3、方法的名稱和描述符
  • HotSpot虛擬機對常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方引用,就可以被回收。回收廢棄常量與回收Java堆中的對象非常類似。

常量池中不再使用的類型

  • 判定一個常量是否“廢棄”還是相對簡單,而要判定一個類型是否屬於“不再被使用的類”的條件就比較苛刻了。需要同時滿足下面三個條件:
    • 該類所有的實例都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實例。
    • 加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,如OSGi、JSP的重加載等,否則通常是很難達成的。
    • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

 

  • Java虛擬機被允許對滿足上述三個條件的無用類進行回收,這里說的僅僅是“被允許”,而並不是和對象一樣,沒有引用了就必然會回收。關於是否要對類型進行回收,HotSpot虛擬機提供了一Xnoclassgc 參數進行控制,還可以使用一verbose:class以及一XX: +TraceClass一Loading、一XX:+TraceClassUnLoading查 看類加載和卸載信息
  • 在大量使用反射、動態代理、CGLib等字節碼框架,動態生成JSP以及oSGi這類頻繁自定義類加載器的場景中,通常都需要Java虛擬機具備類型卸載的能力,以保證不會對方法區造成過大的內存壓力。

運行時數據區的總結:

至此JVM運行時數據區部分基本講解完畢...

預知JVM前置內容講解,本人博客往前翻

預知JVM后置內容講解,本人博客往后翻

面試題補充

百度

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

螞蟻金服:

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

小米:

jvm內存分區,為什么要有新生代和老年代

字節跳動:

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

京東:

JVM的內存結構,Eden和Survivor比例 。
JVM內存為什么要分成新生代,老年代,持久代。新生代中為什么要分為Eden和Survivor。

天貓:

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

拼多多:

JVM內存分哪幾個區,每個區的作用是什么?

美團:

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM