記憶體池與JVM記憶體模型

2020-08-12 18:17:46

基礎

在这里插入图片描述

class檔案
  • 硬碟上的.class檔案
class content
  • 類載入器將硬碟上的.class檔案讀入記憶體中的那一塊記憶體區域
Class物件

在java世界裏,一切皆物件。從某種意義上來說,java有兩種物件:範例物件和Class物件。每個類的執行時的型別資訊就是用Class物件表示的。它包含了與類有關的資訊。其實我們的範例物件就通過Class物件來建立的。Java使用Class物件執行其RTTI(執行時型別識別,Run-Time Type Identification),多型是基於RTTI實現的。

每一個類都有一個Class物件,每當編譯一個新類就產生一個Class物件,基本型別 (boolean, byte, char, short, int, long, float, and double)有Class物件,陣列有Class物件,就連關鍵字void也有Class物件(void.class)。Class物件對應着java.lang.Class類,如果說類是物件抽象和集合的話,那麼Class類就是對類的抽象和集合。

Class類沒有公共的構造方法,Class物件是在類載入的時候由Java虛擬機器以及通過呼叫類載入器中的 defineClass 方法自動構造的,因此不能顯式地宣告一個Class物件。一個類被載入到記憶體並供我們使用需要經歷如下三個階段:

載入,這是由類載入器(ClassLoader)執行的。通過一個類的全限定名來獲取其定義的二進制位元組流(Class位元組碼),將這個位元組流所代表的靜態儲存結構轉化爲方法去的執行時數據介面,根據位元組碼在java堆中生成一個代表這個類的java.lang.Class物件。

鏈接。在鏈接階段將驗證Class檔案中的位元組流包含的資訊是否符合當前虛擬機器的要求,爲靜態域分配儲存空間並設定類變數的初始值(預設的零值),並且如果必需的話,將常數池中的符號參照轉化爲直接參照。

初始化。到了此階段,才真正開始執行類中定義的java程式程式碼。用於執行該類的靜態初始器和靜態初始塊,如果該類有父類別的話,則優先對其父類別進行初始化。

所有的類都是在對其第一次使用時,動態載入到JVM中的(懶載入)。當程式建立第一個對類的靜態成員的參照時,就會載入這個類。使用new建立類物件的時候也會被當作對類的靜態成員的參照。因此java程式程式在它開始執行之前並非被完全載入,其各個類都是在必需時才載入的。這一點與許多傳統語言都不同。動態載入使能的行爲,在諸如C++這樣的靜態載入語言中是很難或者根本不可能複製的。

在類載入階段,類載入器首先檢查這個類的Class物件是否已經被載入。如果尚未載入,預設的類載入器就會根據類的全限定名查詢.class檔案。在這個類的位元組碼被載入時,它們會接受驗證,以確保其沒有被破壞,並且不包含不良java程式碼。一旦某個類的Class物件被載入記憶體,我們就可以它來建立這個類的所有物件。
有三種獲得Class物件的方式:

  1. Class.forName(「類的全限定名」)
  2. 範例物件.getClass()
  3. 類名.class (類字面常數)
物件

對類的範例化 Test_22 obj = new Test_22();

JVM記憶體模型

方法區

介紹
《Java虛擬機器規範》中明確說明:「儘管所有的方法區在邏輯上是屬於堆的一部分,但一些簡單的實現可能不會選擇去進行垃圾收集或者進行壓縮。」但對於HotSpotJVM而言,方法區還有一個別名叫做Non-Heap(非堆),目的就是要和堆分開。所以,方法區看作是一塊獨立於Java堆的記憶體空間。

  • 方法區(Method Area)與Java堆一樣,是各個執行緒共用的記憶體區域。
  • 方法區在JVM啓動的時候被建立,並且它的實際的實體記憶體空間中和Java堆區一樣都可以是不連續的。
  • 方法區的大小,跟堆空間一樣,可以選擇固定大小或者可延伸。
  • 方法區的大小決定了系統可以儲存多少個類,如果系統定義了太多的類,導致方法區溢位,虛擬機器同樣會拋出記憶體溢位錯誤:java.lang.OutofMemoryError:PermGen
    space (8前)或者 java.lang.OutofMemoryError:Metaspace(8以及以後)
  • 關閉JVM就會釋放這個區域的記憶體。
  • 元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代最大的區別在於:元空間不在虛擬機器設定的記憶體中,而是使用本地記憶體。

設定方法區記憶體大小

  • 元數據區大小可以使用參數-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設定爲一個相對較高的值。

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

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

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

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

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

4、靜態變數

  • non-final的類變數
    static靜態變數:載入時準備階段(賦預設值)、初始化階段賦給定值
  • 全域性常數
    ​ static final:編譯時(準備階段)賦給定值

5、執行時常數池

常數池
在这里插入图片描述

  • 一個有效的位元組碼檔案中除了包含類的版本資訊、欄位、方法以及介面等描述資訊外,還包含一項資訊那就是常數池表(Constant Pool
    Table),包括各種字面量和對型別、域和方法的符號參照。
  • 一個java原始檔中的類、介面,編譯後產生一個位元組碼檔案。而Java中的位元組碼需要數據支援,通常這種數據會很大以至於不能直接存到位元組碼裡,換另一種方式,可以存到常數池,這個位元組碼包含了指向常數池的參照。在動態鏈接的時候會用到執行時常數池

執行時常數池

  • 執行時常數池(Runtime Constant Pool)是方法區的一部分。
  • 常數池表(Constant Pool Table)是class檔案的一部分。
  • 執行時常數池,在載入類和介面到虛擬機器後,就會建立對應的執行時常數池。
  • JVM爲每個已載入的型別(類或介面)都維護一個常數池。
  • 執行時常數池中包含多種不同的常數,包括編譯期就已經明確的數值字面量,也包括到執行期解析後才能 纔能夠獲得的方法或者欄位參照。此時不再是常數池中的符號地址了,這裏換爲真實地址。
  • 執行時常數池,相對於class檔案常數池的另一重要特徵是:具備動態性。
  • 當建立類或介面的執行時常數池時,如果構造執行時常數池所需的記憶體空間超過了方法區所能提供的最大值,則JVM會拋OutOfMemoryError異常。
public class Test extends HashMap implements Serializable {
    private String name = "";
    private int x = 1;
    public Test(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        Test haha = new Test(null);
        int nameLength = haha.getNameLength();
        System.out.println(nameLength);
    }

    public int getNameLength() {
        int y = 0;
        try {
            y = name.length();
        } catch (NullPointerException e) {
            System.out.println("空指針異常");
            e.printStackTrace();
        }
        return y;
    }
}  
Classfile /D:/ideaFiles/Algorithm/out/production/Algorithm/com/lx/mySort/Test.class
  Last modified 2020-8-12; size 1145 bytes
  MD5 checksum 8f9825153f3fa6f2042785c0df59703b
  Compiled from "Test.java"
//類資訊
public class com.lx.mySort.Test extends java.util.HashMap implements java.io.Serializable
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #15.#44        // java/util/HashMap."<init>":()V
   #2 = String             #45            //
 ...
{
//域資訊
  private java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE

  private int x;
    descriptor: I
    flags: ACC_PRIVATE

//方法資訊
  ...
  public int getNameLength();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: aload_0
         3: getfield      #3                  // Field name:Ljava/lang/String;
         6: invokevirtual #10                 // Method java/lang/String.length:()I
         9: istore_1
        10: goto          26
        13: astore_2
        14: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        17: ldc           #12                 // String 空指針異常
        19: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        22: aload_2
        23: invokevirtual #14                 // Method java/lang/NullPointerException.printStackTrace:()V
        26: iload_1
        27: ireturn
//異常表
      Exception table:
         from    to  target type
             2    10    13   Class java/lang/NullPointerException
      LineNumberTable:
        line 26: 0
        line 28: 2
        line 32: 10
        line 29: 13
        line 30: 14
        line 31: 22
        line 33: 26
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           14      12     2     e   Ljava/lang/NullPointerException;
            0      28     0  this   Lcom/lx/mySort/Test;
            2      26     1     y   I
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 13
          locals = [ class com/lx/mySort/Test, int ]
          stack = [ class java/lang/NullPointerException ]
        frame_type = 12 /* same */
}
SourceFile: "Test.java"

演進過程
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

虛擬機器棧
  1. Java虛擬機器棧也是執行緒私有的,它的生命週期與執行緒相同(隨執行緒而生,隨執行緒而滅)
  2. 如果執行緒請求的棧深度大於虛擬機器所允許的深度,將拋出StackOverflowError異常;如果虛擬機器棧可以動態擴充套件,如果擴充套件時無法申請到足夠的記憶體,就會拋出OutOfMemoryError異常;當前大部分JVM都可以動態擴充套件,只不過JVM規範也允許固定長度的虛擬機器棧)
  3. Java虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法執行的同時會建立一個棧幀。

棧針

  • 棧幀(Stack Frame)是用於支援虛擬機器進行方法呼叫和方法執行的數據結構。它是虛擬機器執行時數據區中的java虛擬機器棧的棧元素。棧幀儲存了方法的區域性變數表、運算元棧、動態連線和方法返回地址等資訊。每一個方法從呼叫開始至執行完成的過程,都對應着一個棧幀在虛擬機器裏面從入棧到出棧的過程

區域性變數表

  • 區域性變數表(Local Variable Table)是一組變數值儲存空間,用於存放方法參數和方法內部定義的區域性變數。並且在Java編譯爲Class檔案時,就已經確定了該方法所需要分配的區域性變數表的最大容量。
  • 區域性變數表存放了編譯期可知的各種基本數據型別(boolean、byte、char、short、int、float、long、double)「String是參照型別」,物件參照(reference型別) 和 returnAddress型別(它指向了一條位元組碼指令的地址)
  • 注意:
      很多人說:基本數據和物件參照儲存在棧中。
      當然這種說法雖然是正確的,但是很不嚴謹,只能說這種說法針對的是區域性變數。
      區域性變數儲存在區域性變數表中,隨着執行緒而生,執行緒而滅。並且執行緒間數據不共用。
      但是,如果是成員變數,或者定義在方法外物件的參照,它們儲存在堆中。
      因爲在堆中,是執行緒共用數據的,並且棧幀裡的命名就已經清楚的劃分了界限 : 區域性變數表!
      
    運算元棧
  • 運算元棧也常被稱爲操作棧,它是一個後入先出(Last In First Out,LIFO)
    棧。同區域性變數表一樣,運算元棧的最大深度也在編譯的時候被寫入到Code屬性的max_stacks數據項之中。運算元棧的每一個元素可以是任意的Java數據型別,包括long和double。32位元數據型別所佔的棧容量爲l,64位元數據型別所佔的棧容量爲2在方法執行的任何時候,運算元棧的深度都不會超過在max_stacks數據項中設定的最大值。
  • 當一個方法剛剛開始執行的時候,這個方法的運算元棧是空的,在方法的執行過程中,會有各種位元組碼指令向運算元棧中寫入和提取內容,也就是入棧出棧操作。例如,在做算術運算的時候是通過運算元棧來進行的,又或者在呼叫其他方法的時候是通過運算元棧來進行參數傳遞的。
  • 另外,在概念模型中,兩個棧幀作爲虛擬機器棧的元素,相互之間是完全獨立的。但是大多數虛擬機器的實現裡都會做一些優化處理,令兩個棧幀出現一部分重登。讓下面 下麪棧幀的部分運算元棧與上面棧幀的部分區域性變數表重疊在一起,這樣在進行方法呼叫時就可以共用一部分數據,而無須進行額外的參數複製傳遞了。

動態連線 直接地址

  • 每個棧幀都包含一個指向執行時常數池中該棧幀所屬方法的參照,持有這個參照是爲了支援方法呼叫過程中的動態連線。Class檔案的常數池中存有大最的符號參照,位元組碼中的方法呼叫指令就以常最池中指向方法的符號參照爲參數。這些符號參照一部分會在類載入階段或第一次使用的時候轉化爲直接參照,這種轉化稱爲靜態解析。另外一部分將在每一次的執行期間轉化爲直接參照,這部分稱爲動態連線。

返回地址 回覆 回復現場

  • 當一個方法被執行後,有兩種方式退出這個方法。第一種方式是執行引擎遇到任意一個方法返回的位元組碼指令,這時候可能會有返回值傳遞給上層的方法呼叫者,是否有返回值和返回值的型別將根據遇到何種方法返回指令來決定,這種退出方法的方式稱爲正常完成出口(Normal
    Method Invocation Completion)。

  • 另外一種退出方式是,在方法執行過程中遇到了異常,並且這個異常沒有在方法體內得到處理,無論是Java虛擬機器內部產生的異常,還是程式碼中使用athrow位元組碼指令產生的異常,只要在本方法的異常表中沒有搜尋到匹配的例外處理器,就會導致方法退出,這種退出方法的方式稱爲異常完成出口(Abrupt
    Method Invocation Completion) 。一個方法使用異常完成出口的方式退出,是不會給它的上層呼叫者產生任何返回值的。

  • 無論採用何種退出方式,在方法退出之後,都需要返回到方法被呼叫的位置,程式才能 纔能繼續執行,方法返回時可能需要在棧幀中儲存一些資訊,用來幫助恢復它的上層方法的執行狀態。一般來說,方法正常退出時,呼叫者的PC計數器的值就可以作爲返回地址,棧幀中很可能會儲存這個計數器值。而方法異常退出時,返回地址是要通過例外處理器表來確定的,棧幀中一般不會儲存這部分資訊。

  • 方法退出的過程實際上等同於把當前棧幀出棧,因此退出時可能執行的操作有:恢復上層方法的區域性變數表和運算元棧,把返回值(如果有的話)壓入呼叫者棧幀的運算元棧中,調整PC計數器的值以指向方法呼叫指令後面的一條指令等。

在这里插入图片描述
JVM執行main方法,內部是如何執行的
1、建立執行main方法需要的棧幀
2、將main方法的運算元棧指針賦值給執行緒的屬性:運算元棧
3、將main方法的區域性表指針賦值給給執行緒的屬性:區域性表指針
VM執行add方法,內部是怎麼做的
1、建立add的方法的棧幀
2、在add方法的棧幀中儲存main方法的位元組碼的下一行程式計數器(18)
3、執行緒的區域性表開始指針(main的)儲存至add方法的棧幀
4、執行緒的運算元棧開始指針(main的)儲存至add方法的棧幀
5、將add方法的區域性表指針賦值給執行緒的區域性表指針
6、將add方法的運算元棧指針賦值給執行緒的運算元棧指針

程式計數器

在这里插入图片描述

堆區
本地方法棧

this指針

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

在这里插入图片描述

虛擬機器棧與方法區關係

虛擬機器棧與堆區關係

方法區與堆區關係