天天看點

【JVM入門食用指南】JVM記憶體管理

📑即将學會

JVM的記憶體管理的相關知識點,JVM對記憶體管理進行了哪些規範

Java從編譯到執行

.java檔案經過javac編譯成.class檔案 .class檔案通過類加載器(ClassLoader)加載到方法區 jvm執行引擎執行 把位元組碼翻譯成機器碼

解釋執行與JIT執行

解釋執行

JVM 是C++ 寫的 需要通過C++ 解釋器進行解釋

解釋執行優缺點

通過JVM解釋 速度相對慢一些

JIT (just-in-time compilation 即時編譯)(hotspot)

方法、一段代碼 循環到一定次數 後端1萬多 代碼會走hotspot編譯 JIT執行(hotspot)(JIT) java代碼 直接翻譯成(不經解釋器) 彙編碼 機器碼

JIT執行優缺點

速度快 但是編譯需要一定時間

JVM是一種規範

JVM兩種特性 跨平台 語言無關性

  • 跨平台
    • 相同的代碼在不同的平台有相同的執行效果
  • JVM語言無關性
    • 隻識别.class檔案 隻要把相關的語言檔案編譯成.class檔案 就可以通過JVM執行
      • 像groove、kotlin、java語言 本質上和語言沒有關系,是以,隻要符合JVM規範,就可以執行 語言層面上 隻是将.java .kt等檔案編譯成.class檔案

是以 JVM是一種規範

JVM 記憶體管理規範

運作時資料區域

Java虛拟機在執行Java程式的過程中會把它所管理的記憶體 劃分為若幹個不同的資料區域 資料劃分 而資料劃分這塊 依據線程私有 和 線程共享這兩種進行劃分

  • 線程共享區
    • 線程共享區 分為方法區 和 堆
  • 方法區 (永久代(JDk1.7 前) 元空間(JDK1.8) hotspot實作下稱呼 )
    • 在JVM規範中,統稱為方法區
      • 隻是

        hotSpot VM 

        這塊産品用得比較多 。

        hotSpot

        利用永久代 或者 元空間 實作method區 Hotspot不同的版本實作而已

堆 幾乎所有對象都會在這裡配置設定

線程私有區 每啟動一個線程劃分的一個區域

直接記憶體 堆外記憶體

  • JVM會在運作時把管理的區域進行虛拟化 new 對象()

通過JVM記憶體管理,将對象放入堆中,使用的時候隻需要找到對象的引用 就可以直接使用,比直接通過配置設定記憶體,位址尋址 計算偏移量,偏移長度更友善。

  • 而這塊資料沒有經過記憶體虛拟化 (運作時外的記憶體 記憶體8G JVM 占用5G 堆外記憶體3G)

可以通過某種方法 進行申請、使用、釋放。不過比較麻煩,涉及配置設定記憶體、配置設定位址等

java方法的運作與虛拟機棧

虛拟機棧

棧的結構 存儲目前線程運作Java方法所需要的資料、指令、傳回位址。

public static void main(String[] args) {
    A();
}
private static void A() {
    B();
}
private static void B() {
    C();
}
private static void C() {
    
}      

比如以上代碼,當我們運作main方法時,會啟動一個線程,這個時候,JVM會在運作時資料區建立一個虛拟機棧。 在棧中 運作方法 每運作一個方法 ,會壓入一個棧幀

  • 虛拟機棧大小限制 Xss參數指定
-Xsssize
設定線程堆棧大小(以位元組為機關)。k或k表示KB, m或m表示MB, g或g表示GB。預設值取決于虛拟記憶體。
下面的示例以不同的機關将線程堆棧大小設定為1024kb:
-Xss1m
-Xss1024k
-Xss1048576
這個選項相當于-XX:ThreadStackSize。      

棧幀

棧幀内主要包含

  • 局部變量表
  • 操作數棧
  • 動态連接配接
  • 完成出口

棧幀對記憶體區域的影響

以以下代碼為例

public class Apple {
    public int grow() throws Exception {
        int x = 1;
        int y = 2;
        int z = (x + y) * 10;
        return z;
    }
    public static void main(String[] args) throws Exception {
        Apple apple = new Apple();
        apple.grow();
        apple.hashCode();
    }
}      

因為JVM識别的.class檔案,而不是.java檔案。是以,我們需要拿到其位元組碼,可以通過ASM plugin插件 右鍵擷取 或者通過 javap -v xxx.class 擷取 (本文通過javap 方式擷取) 其位元組碼如下

Classfile /XXX/build/classes/java/mainXXX/Apple.class
  Last modified 2021-8-11; size 668 bytes
  MD5 checksum d10da1235fad7eba906f5455db2c5d8b
  Compiled from "Apple.java"
public class Apple
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#29         // java/lang/Object."<init>":()V
   #2 = Class              #30            // Apple
   #3 = Methodref          #2.#29         // Apple."<init>":()V
   #4 = Methodref          #2.#31         // Apple.grow:()I
   #5 = Methodref          #6.#32         // java/lang/Object.hashCode:()I
   #6 = Class              #33            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Apple;
  #14 = Utf8               grow
  #15 = Utf8               ()I
  #16 = Utf8               x
  #17 = Utf8               I
  #18 = Utf8               y
  #19 = Utf8               z
  #20 = Utf8               Exceptions
  #21 = Class              #34            // java/lang/Exception
  #22 = Utf8               main
  #23 = Utf8               ([Ljava/lang/String;)V
  #24 = Utf8               args
  #25 = Utf8               [Ljava/lang/String;
  #26 = Utf8               apple
  #27 = Utf8               SourceFile
  #28 = Utf8               Apple.java
  #29 = NameAndType        #7:#8          // "<init>":()V
  #30 = Utf8               Apple
  #31 = NameAndType        #14:#15        // grow:()I
  #32 = NameAndType        #35:#15        // hashCode:()I
  #33 = Utf8               java/lang/Object
  #34 = Utf8               java/lang/Exception
  #35 = Utf8               hashCode
{
  public Apple();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Apple;
  public int grow() throws java.lang.Exception;
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: bipush        10
         9: imul
        10: istore_3
        11: iload_3
        12: ireturn
      LineNumberTable:
        line 5: 0
        line 6: 2
        line 7: 4
        line 8: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  this   Apple;
            2      11     1     x   I
            4       9     2     y   I
           11       2     3     z   I
    Exceptions:
      throws java.lang.Exception
  public static void main(java.lang.String[]) throws java.lang.Exception;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class Apple
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method grow:()I
        12: pop
        13: aload_1
        14: invokevirtual #5                  // Method java/lang/Object.hashCode:()I
        17: pop
        18: return
      LineNumberTable:
        line 11: 0
        line 12: 8
        line 13: 13
        line 14: 18
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      19     0  args   [Ljava/lang/String;
            8      11     1 apple   Lcom/enjoy/ann/Apple;
    Exceptions:
      throws java.lang.Exception
}
SourceFile: "Apple.java"      

從其位元組碼中 我們可以看到這麼一段

public Apple();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Apple;      

這是Apple的構造方法,雖然我們沒有寫,但是預設有無參構造方法實作

回到正文 下面我們對

grow()

方法做解析

public int grow() throws java.lang.Exception;
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: bipush        10
         9: imul
        10: istore_3
        11: iload_3
        12: ireturn
      LineNumberTable:
        line 5: 0
        line 6: 2
        line 7: 4
        line 8: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  this   Apple;
            2      11     1     x   I
            4       9     2     y   I
           11       2     3     z   I
    Exceptions:
      throws java.lang.Exception      

我們可以看到其

code

代碼區域,有 0 1 2 3 既

這是grow棧幀中的位元組碼位址(相對于改方法的偏移量)表,當程式運作的時候,程式計數器中的數會被調換為運作這個方法的位元組碼的行号 0 1 2 3 [位元組碼行号] 而位元組碼的行号 對應JVM 位元組碼指令助記符 下面對位元組碼位址表中涉及的位元組碼行号 進行了解

0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: bipush        10
         9: imul
        10: istore_3
        11: iload_3
        12: ireturn      

首先 進入

grow

方法 記錄進入方法時所在

main()

中的行号 作為完成出口,如main方法中grow方法位元組碼位址為3 方法完成後,接着執行完成出口的下一行位元組碼位址,所有的操作都在操作數棧中完成

進入

grow()

棧幀中。程式計數器将計數器置為0,如果該類是靜态方法,則局部變量表不變,如果該類不是靜态方法,則在局部變量量中加入該對象執行個體this。類似下圖

  • 0: iconst_1
    • i 表示int const 表示常量 後面的1 表示值 ,這裡表示建立int常量 1 ,放入操作數棧。

然後code代碼運作下一行

  • 1: istore_1
    • 這裡将程式計數器count值改為1,然後 i 表示 int ,store表示 存儲, 1 表示存儲下标 存儲到局部變量表中1的位置 ,我們這裡将操作數中值為1的int出棧放到局部變量中 存儲。

上面兩條位元組碼 對應 int X = 1

i_const_1 對應右邊 定義1

i_store_1 對應左邊 用一個變量X存儲 1

int y = 2

參考上面分析

下面我們來看看

int z = (x + y) * 10;

x 和 y都在本地布局變量中有存儲,是以,執行這條代碼的時候,我們不需要上述步驟了,我們可以通過4: iload_1,将布局變量中1位置的資料加載到操作數棧中

下面執行code中 6: iadd,将操作數棧中的資料彈出兩個操作數,再将結果存入棧頂,這個時候結果僅僅保留在操作數棧

這個時候我們已經完成了

(X + y)

這步 ,接下來看

* 10

這步,這個時候我們跳到

7: bipush 10

這個值也是常量值,但是比較大 操作指令有點不一樣,0-5用const,其它值JVM采用bipush指令将常量壓入棧中。 10對應要壓入的值 。

然後我們跳到下一行 9: imul .這是一個加法操作指令。我們可以看到操作号直接從7變成了9.這是因為bipush指令過大,占用了2個操作指令長度。

這個時候我們已經得到了計算結果,還需要将其指派局部變量z進行變量存儲.

此時,我們已經完成了

z = (x + y) * 10

的操作了。 此時執行最後一行

return z;

首先 ,取出z,将其load進操作數棧,然後利用ireturn傳回結果。該方法結束。這個時候,完成出口存儲的上一方法中的程式計數器的值,回到上一方法中正确的位置。

補充

0 1 2 3 4 7 9 位元組碼偏移量 針對本方法的偏移

程式計數器隻存儲自己這個方法的值

動态連接配接 確定多線程執行程式的正确性

本地方法棧

本地方法棧(Native Method Stacks)與虛拟機棧所發揮的作用是非常相似的,其差別不過是虛拟機棧為JVM執行 Java 方法(也就是位元組碼)服務,而本地方法棧則是為JVM使用到的 Native方法服務。在hotSpot中,本地方法棧與虛拟機棧是一體

在本地方法棧中,程式計數器為null,因為,本地方法棧中運作的形式不是位元組碼

下面還是來一段代碼

public class Learn {
    static int NUMBER = 18; //靜态變量 基本資料類型
    static final int SEX = 1; //常量 基本資料類型
    static final Learn LERARN = new Learn(); //成員變量 指向 對象
    private boolean isYou = true; //成員變量
    public static void main(String[] args) {
        int x = 18;//局部變量
        long y = 1;
        Learn learn = new Learn(); //局部變量 對象
        learn.isYou = false;//局部變量 改變值
        learn.hashCode(); //局部變量調用native 方法
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(128 * 1024 * 1024);//配置設定128M直接記憶體
    }
}      

類加載過程中

Learn 加載到方法區

類中的 靜态變量、常量加載到方法區。

方法區

是可供各條線程共享的運作時記憶體區域。它存儲了每一個類的結構資訊,

我們申請的幾乎所有的對象,都是在這裡存儲的。我們常說的垃圾回收,操作的對象就是堆。 随着對象的頻繁建立,堆空間占用的越來越多,就需要不定期的對不再使用的對象進行回收。這個在 Java 中,就叫作 GC(Garbage Collection)。

建立的時候,到底是在堆上配置設定,還是在棧上配置設定呢?

這和兩個方面有關:對象和在 Java 類中存在的位置。 對于普通對象來說,JVM 會首先在堆上建立對象,然後在其他地方使用的其實是它的引用。比如,把這個引用儲存在虛拟機棧的局部變量表中。 對于基本資料類型來說(byte、short、int、long、float、double、char),有兩種情況。 當你在方法體内聲明了基本資料類型的對象,它就會在棧上直接配置設定。其他情況,都是在堆上配置設定。

JVM記憶體處理

先來一段代碼進行後續分析

public class JVMObject {
    public final static String MAN_TYPE = "man"; // 常量
    public static String WOMAN_TYPE = "woman";  // 靜态變量
    public static void  main(String[] args)throws Exception {
        Teacher T1 = new Teacher();
        T1.setName("A");
        T1.setSexType(MAN_TYPE);
        T1.setAge(36);
        for(int i =0 ;i < 15 ;i++){
            //每觸發一次gc(),age+1 記錄age的字段是4位 最大1111 對應15 
            System.gc();//主動觸發GC 垃圾回收 15次--- T1存活  T1要進入老年代
        }
        Teacher T2 = new Teacher();
        T2.setName("B");
        T2.setSexType(MAN_TYPE);
        T2.setAge(18);
        Thread.sleep(Integer.MAX_VALUE);//線程休眠 後續進行觀察   T2還是在新生代
    }
}
class Teacher{
    String name;
    String sexType;
    int age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getSexType() {
        return sexType;
    }
    public void setSexType(String sexType) {
        this.sexType = sexType;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}      
  1. JVM申請記憶體
  2. 初始化運作時資料區
  3. 類加載
  • 執行方法(加載後運作main方法)

4.建立對象

流程

JVM 啟動,申請記憶體,先進行運作時資料區的初始化,然後把類加載到方法區,最後執行方法。方法的執行和退出過程在記憶體上的展現上就是虛拟機棧中棧幀的入棧和出棧。 同時在方法的執行過程中建立的對象一般情況下都是放在堆中,堆中的對象最後通過垃圾回收處理。

  • 堆空間分代劃分

通過HSDB檢視堆空間劃分 及記憶體配置設定

  • 先運作相關類
  • CMD指令行 運作jps 檢視相關程序
  • 找到JDK安裝目錄 java8u292\bin bin 目錄下點選HSDB.exe運作程式
  • 通過File下 點選 下圖 進行 程序綁定
    • 将之前通過jps擷取的程序号輸入 輸入框
    • 綁定後界面為 該程序下程序資訊
  • 通過

    Tools

    欄下的heap parameter 可以觀察堆配置設定情況
    • 我們可以看到堆分區的劃分和之前的是類似的,這樣可以直覺的看到堆的位址,也可以讓我們對JVM将記憶體虛拟化有更直覺的認知。
  • 對象的位址配置設定
    • 我們也可以通過object histogram檢視對象的配置設定情況
    • 進入後界面如下所示
    • 我們可以通過全類名搜尋相關類
    • 找到自己想要的檢視的類後,可以看到 第一行表示這個類所有對象的size ,count 數量是多少個。比如标紅的表示,

      Teacher

      類所有對象占用48,一共兩個對象。輕按兩下這一欄,進入詳細頁面
    • 點選對應條目,點選下方insperctor檢視詳細資訊,将其與之前堆記憶體位址配置設定對比,發現一個主動調用gc()從新生代慢慢進入老年代,這個A已經進入老年代了,而另一個B還在新生代。

通過HSDB檢視棧

可以在HSDB綁定程序時,檢視所有列出的線程資訊,點選想要檢視的線程,如main線程。點選浮窗菜單欄上的第二個 我們可以檢視主線程的棧記憶體情況 ,如下圖所示。

有興趣的朋友可以去玩玩 這個工具

記憶體溢出

棧溢出 StackOverflowError

方法調用方法 遞歸

堆溢出

OOM 申請配置設定記憶體空間 超出堆最大記憶體空間

可以通過設定運作設定 進行模拟

可以通過設定 VM options進行設定JVM 相關參數配置參考相關連結第一個 可以通過 調大 -Xms,-Xmx參數避免棧溢出

方法區溢出

(1) 運作時常量池溢出

(2)方法區中儲存的Class對象沒有被及時回收掉或者Class資訊占用的記憶體超過了我們配置。

class回收條件
  • 該類所有的執行個體都已經被回收,堆中不存在該類的任何執行個體
  • 加載該類的ClassLoader已經被回收
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射通路該類的方法。

直接記憶體溢出

直接記憶體的容量可以通過MaxDirectMemorySize來設定(預設與堆記憶體最大值一樣)

相關連結

java (oracle.com) 可以直覺檢視其中涉及的參數資訊