十九、JVM內存管理分析

一、JVM的運行過程

  • JVM虛擬機我們可以把它當做一臺虛擬出來的計算機,也有自己的內存管理如:堆、棧、方法區等。
  • JVM的作用是將字節碼翻譯成不同操作系統可以識別的機器碼執行。
    運行過程如下圖:


(1)我們寫的一個JAVA程序首先通過JDK的中JAVAC工具進行編譯,編譯成了.class文件,也就是我們說的字節碼。
(2)JAVA類加載器(ClassLoader)將字節碼載入到JVM的運行時數據區,也就是JVM中的一塊內存區域。
(3)通過執行引擎調用操作系統接口解釋執行或者JIT執行。

  • 解釋執行指的是JVM通過加載到的字節碼進行翻譯執行
  • JIT指的是將熱點代碼直接翻譯成機器碼保存下來,方便下次直接執行。
    區別是:前者啓動快,但是運行速度慢,因爲每次都要邊解釋邊執行。而後者啓動速度慢,但是執行速度快,因爲需要將熱點代碼翻譯成機器碼保存下來,但是下次執行到熱點代碼的時候直接通過執行之前保存的機器碼,因此速度比較快。
  • 一般虛擬機是通過兩種方式混合使用。

二、運行時數據區

字節碼通過類加載器加載到了JVM運行時數據區,而運行時數據區又將內存劃分了不同的區域。不同的身份將進入到不同的區域。如下圖:



運行時數據區主要分爲兩大塊:線程共享數據區和線程隔離數據區
其中右邊白色部分包括虛擬機棧、本地方法棧、程序計數器是線程隔離數據區。左邊灰色區域方法區、堆是線程共享數據區。

  • 線程隔離數據區
    指的是每一個線程都擁有自己的一塊內存區域,就比如有兩個線程A和B,他們各自擁有自己的虛擬機棧、本地方法棧、程序計數器。
  • 線程共享區
    指的是線程共享的區域,每個線程都共享了方法區和堆

1、線程共享數據區

1.1、程序計數器

指向了當前線程正在執行的字節碼指令地址,換句話說就是記錄當前字節碼指令執行的位置。因爲在操作系統中由於時間片輪轉機制,當前的線程可能指令還沒執行完就被切出去了,因此要通過程序計數器記錄正在執行的位置,當線程重新獲得時間片之後,在原來的位置繼續執行。

1.2、虛擬機棧

存儲當前線程運行方法所需要的數據、指令、返回地址。我們可以理解成一個線程就擁有一個虛擬機棧,而線程中的每一個方法就是一個棧幀。

1.2.1、棧幀

棧幀中又包含了:局部變量表、操作數棧、動態鏈接、 完成出口(返回地址)

如下圖


線程中一個方法的執行就是在這些身份相互配合執行。我們將線程隔離數據區進行更細的劃分如下圖



我們看到一個虛擬機棧會有多個棧幀,每一個棧幀入棧出棧的過程就是一個方法執行的過程。棧幀中的局部變量表、操作數棧、動態鏈接、完成出口存放了方法執行過程的數據。下面我們來演示一個方法的執行對應字節碼指令的執行。

  • JAVA源代碼
package com.it.test;

public class Test {
    public static void main(String[] args) {
        Test test = new Test();
        test.function();
    }

    public int function() {
        int a = 1;
        int b = 3;
        int c = (a + b) * 10;
        return c;
    }
}

  • 反彙編後的字節碼

D:\app_work_space\JavaHighSets\src\com\it\test>javap -c Test.class
Compiled from "Test.java"
public class com.it.test.Test {
  public com.it.test.Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class com/it/test/Test
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #4                  // Method function:()I
      12: pop
      13: return

  public int function();
    Code:
       0: iconst_1
       1: istore_1
       2: iconst_3
       3: istore_2
       4: iload_1
       5: iload_2
       6: iadd
       7: bipush        10
       9: imul
      10: istore_3
      11: iload_3
      12: ireturn
}


我們將源代碼和反彙編後的代碼合在一起進行對比,然後分析function方法的執行如何在虛擬機棧和程序計數器中體現的。

//源代碼
  public int function() {
        int a = 1;
        int b = 3;
        int c = (a + b) * 10;
        return c;
    }
//反彙編後的代碼
  public int function();
    Code:
       0: iconst_1
       1: istore_1
       2: iconst_3
       3: istore_2
       4: iload_1
       5: iload_2
       6: iadd
       7: bipush        10
       9: imul
      10: istore_3
      11: iload_3
      12: ireturn
}
  • code就是代碼的意思
  • 0、1、2...等等行號,我們可以認爲是字節碼指令執行到的位置,程序計數器存放的就是這些數據的地址,記錄當前方法執行到的位置,因此程序計數器存放的數據較小,不會因爲內存不足產生OOM異常。
    0: iconst_1
    將int 類型 1壓入到操作數棧
    1:istore_1
    將操作數棧棧頂出棧,存入局部變量表下標爲1的位置
    以上兩個步驟完成了int a =1;
    2: iconst_3
    將int類型 3壓入到操作數棧
    3:istore_2
    將操作數棧棧頂出棧,存入局部變量表下標爲2的位置
    以上兩個步驟完成了int b = 3;
    4:iload_1
    將局部變量表下標爲1的位置的數據壓入操作數棧
    5:iload_2、
    將局部變量表下標爲2的位置的數據壓入操作數棧
    6: iadd
    三部曲,
    (1)將棧頂的兩個數據出棧
    (2)相加
    (3)將結果壓入到操作數棧
    7: bipush
    將int類型10壓入到操作數棧
    9:
    imul三部曲
    (1)將棧頂兩個數據出棧
    (2)相乘
    (3)結果壓入到操作數棧
    10:istore_3
    將棧頂數據存入到局部變量表下標爲3的位置
    11: iload_3
    將局部變量表下標爲3的位置的數據存到操作數棧,作爲返回值
    以上就完成了int c = (a + b) * 10
    12: ireturn
    將操作數棧棧頂的數據出棧返回
    完成了rerturn z

最後方法執行完畢之後,棧幀就從Java虛擬機棧中出棧

1.3、Java虛擬機棧小結

Java虛擬機棧存放的是一個方法的執行過程、而每一個方法對應一個棧幀,每一個方法的執行過程就是棧幀入棧出棧的過程,而棧幀中又包含了局部變量表、操作數棧、動態鏈接、完成出口(返回地址)。這四個角色用於存放執行過程的數據。

(1)局部變量表

存方法內部的局部變量基本數據類型、以及局部對象類型變量的引用

(2)操作數棧

完成方法中數據的操作

(3)動態鏈接

Java語言中會有多肽,例如以下代碼,Student和Teacher都繼承了User,執行方法eat,在編譯期間是沒法知道是執行Student的eat方法還是Teacher的eat方法,所以在方法運行期間,Java虛擬機棧的棧幀中存放一個動態鏈接來確定執行誰的eat方法

public class User  {

    public void eat(){
    }
    public static void main(String[] args) {
        User user = new Student();
        user.eat();
        user = new Teacher();
        user.eat();
    }
}

(4) 完成出口(返回地址)

例如以下代碼, 在main方法中執行了function方法,當function放執行完畢之後,要回到main方法中繼續執行,那具體要回到的地方是哪裏呢?就是我們 System.out.println("你好。。。");這一行這裏作爲出口,所以這個出口就是存放在棧幀中的“完成出口”區域。

package com.it.test;

public class Test {
    public static void main(String[] args) {
        Test test = new Test();
        test.function();
        System.out.println("你好。。。");
        System.out.println("哈哈。。。");
    }

    public int function() {
        int a = 1;
        int b = 3;
        int c = (a + b) * 10;
        return c;
    }
}

1.4、本地方法棧

以上我們說到了線程隔離區中的程序計數器、虛擬機棧。下面我們來了解一下最後一個本地方法棧。
我們知道虛擬機棧存的是Java方法的執行過程所需的指令、數據、返回地址等,每一個方法的執行就是一個棧幀入棧出棧的過程。而本地方法的執行則對應了我們的本地方法棧。例如我們的hashCode方法就是本地方法。

   public native int hashCode();

當JVM創建的線程調用了native方法之後,JVM不會爲其在虛擬機棧中創建棧幀,而是簡單的動態鏈接並直接調用native方法。
一般的虛擬機,Java虛擬機棧和本地方法棧都是合在一塊區域。例如我們的HotSpot。

2、線程隔離數據區

2.1、方法區

2.2、堆

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