談談對 JVM 的理解

談談對 JVM 的理解

JVM 的體系結構概述

在這裏插入圖片描述
在這裏插入圖片描述

什麼是類加載器?

    負責加載 class 文件,class 文件在 文件開頭有特定的文件標示,將 class 文件字節碼內容加載到內存中,並將這些內容轉換成方法區中的運行時數據結構並且 ClassLoader 只負責 class 文件的加載,至於它是否可以運行,則由 Execution Engine 決定
在這裏插入圖片描述

三大類加載器

  • 根加載器(Bootstrap)
  • 擴展類加載器(Extension)
  • 應用程序類加載器(AppClassLoader)
    在這裏插入圖片描述
package jvm;

/**
 * @author Woo_home
 * @create by 2020/3/18
 */
public class HelloDemo {
    public static void main(String[] args) {
        Object obj = new Object();
        HelloDemo hello = new HelloDemo();
        // 獲取當前類加載器
        System.out.println(obj.getClass().getClassLoader());
        // 獲取當前類加載器
        System.out.println(hello.getClass().getClassLoader());
        // 獲取當前類加載器的父加載器
        System.out.println(hello.getClass().getClassLoader().getParent());
    }
}

輸出爲:
null
    Object 爲什麼是 null 呢?其實這裏的 null 指的就是 Bootstrap(啓動類加載器),而 Object 又是 Java 自帶的,所以這裏輸出爲 null 是因爲 Object 無法獲取本身(類加載器)

    而 HelloDemo 是自己創建的,所以獲取到的類加載器是 AppClassLoader,因爲 AppClassLoader 的父加載器是 ExtentionClassLoader,所以 hello.getClass().getClassLoader().getParent() 的時候就會輸出 ExtClassLoader

sun.misc.Launcher

    我們從輸出的結果可以發現類加載器前面帶着一個 sun.misc.Launcher,這是個什麼東東呢?我們都知道 Java 的 main 方法是一切程序的入口方法,而 Launcher 則是一個 Java 虛擬機的入口應用

雙親委派

    當一個類收到了類加載請求,它首先不會嘗試自己去加載這個類,而是把這個請求委派給自己的父類去完成,每一個層次類加載器都是如此,因此所有的加載請求都應該傳送到啓動類加載器中,只有當父類加載器反饋自己無法完成這個請求的時候(在它的加載路徑下沒有找到所需加載的 Class),子類加載器纔會嘗試自己去加載

採用雙親委派有什麼好處?

    採用雙親委派的一個好處是比如加載位於 rt.jar 包中的類 java.lang.Object 時,不管是哪個加載器加載這個類,最終都是委託給頂層的啓動類加載器進行加載,這樣就保證了使用不同的類加載器最終得到的都是同樣一個 Object 對象

沙箱安全機制

我們建一個 java.lang 這麼一個包,然後在這個包下新建一個 String 類,代碼如下:

package java.lang;

/**
 * @author Woo_home
 * @create by 2020/3/18
 */
public class String {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

這個代碼很簡單,運行該程序
在這裏插入圖片描述
從輸出結果可以發現,報錯說我們沒有編寫 main 方法,但是我們的代碼明明編寫了 main 方法,爲什麼呢?

    說明我們雖然編寫了 String 這麼個類,但是程序並沒有運行這個 String 類。按道理來說我們自定義的的類是由應用程序類加載器(AppClassLoader)去加載的,而我們剛剛學過雙親委派機制,當一個類收到了類加載請求,它首先不會嘗試自己去加載這個類,而是把這個請求委派給自己的父類去完成

    所以運行 String 這個類的時候首先就是由啓動類加載器去加載(Bootstrap),而啓動類加載器(Bootstrap)會先去加載 rt.jar 下的 java.lang.String 這個類,這個類是 Java 自帶的,爲了不讓我們去修改這個源代碼,所以就設置了一個沙箱安全機制(保證對代碼的有效隔離,防止對本地系統造成破壞

Execution Engine

如執行命令

javac Hello.java
java Hello

Native Interface 本地接口

    本地接口的作用是融合不同的編程語言爲 Java 所用,它的初衷是融合 C/C++ 程序,Java 誕生的時候是 C/C++ 橫行的時候,要想立足,必須要調用 C/C++ 程序,於是就在內存中專門開闢了一塊區域處理標記爲 native 代碼,它的具體做法是 Native Method Stack 中登記 native 方法,在 Execution Engine 執行是加載 native libraies
    目前該方法使用的越來越少了,除非是與硬件有關的應用,比如通過 Java 程序驅動打印機或者 Java 系統管理生產設備,在企業級應用中已經比較少見。因爲現在的異構領域間的通信很發達,比如可以使用 Socket 通信,也可以使用 Web Service 等等

Native Method Stack 本地方法棧

    它的具體做法是 Native Method Stack 中登記 native 方法,在 Execution Engine 執行時加載本地方法庫

Method Area 方法區

    供各線程共享的運行時內存區域。它存儲了每一個類的結構信息,例如運行時的常量池(Runtime Constant Pool)、字段和方法數據、構造函數和普通方法的字節碼內容。方法區是個規範,方法區在不同的虛擬機裏實現是不一樣的,最典型的就是永久代(PermGen space)和元空間(Metaspace)

實例變量存在堆內存中,和方法區無關

程序計數器

    每個線程都有一個程序計數器,是線程私有的,就是一個指針,指向方法區中的方法字節碼(用來存儲指向下一條指令的地址,也即將要執行的指令代碼),由執行引擎讀取下一條指令,是一個非常小的內存空間

    這塊內存區域很小,它是當前線程所執行的字節碼的行號指示器,字節碼解釋器通過改變這個計數器的值來選取下一條需要執行的字節碼指令。

    如果執行的是一個 Native 方法,那這個計數器是空的

    用以完成分支、循環、跳轉、異常處理、線程恢復等基礎功能。不會發生內存溢出(OutOfMemory=OOM)錯誤

Java 棧

    棧也叫棧內存,主管 Java 程序的運行,是在線程創建時創建,它的生命週期是跟隨線程的聲明週期,線程結束棧內存也就釋放了,對於棧來說不存在垃圾回收問題,只要線程一結束該棧就 Over,生命週期和線程一致,是線程私有的。8 種基本類型的變量 + 對象的應用變量 + 實例方法都是在函數的棧內存中分配

棧存儲什麼?

棧幀中主要保存 3 類數據:

  • 本地變量(Local Variables):輸入參數和輸出參數以及方法內的變量
  • 棧操作(Operand Stack):記錄出棧、入棧的操作
  • 棧幀數據(Frame Data):包括類文件、方法等

Heap 堆

    一個 JVM 實例只存在一個堆內存,堆內存的大小是可以調節的。類加載器讀取了類文件之後,需要把類、方法、常變量放到堆內存中,保存所有引用類型的真實信息,以方便執行器執行,堆內存分爲三個部分

  • Young Generation Space 新生區 Young / New
  • Tenure Generation Space 老年區 Old / Tenure
  • Permanent Space 永久區 Perm

在 Java 8 中永久區改爲了元空間

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