【面試】JVM記憶體結構

語言: CN / TW / HK

theme: smartblue

Java跨平臺執行

我們都知道Java語言一次編譯到處執行,可以在windows上執行也可以在Liunx上執行,屬於跨平臺語言,Java其實就是依賴JVM實現的跨平臺性,但是我們的JVM本身是不存在跨平臺的。

通過Javac.exe編譯.java原始碼檔案生成.class檔案,然後再通過類載入器,將class檔案載入到JVM中,交由JVM執行,最後輸出結果。

我們來記一張簡潔的執行圖:

image.png

JVM的組成

JVM由4大部分組成:ClassLoader(類載入器),Runtime Data Area(執行時資料區域),Execution Engine(執行引擎),Native Interface(本地介面)。

image.png

  • ClassLoader: 負責載入位元組碼檔案,即是java編譯後的.class檔案。
  • Runtime Date Area: 存放.class檔案和分配記憶體。
  • Native Interface: 負責呼叫本地介面,即是呼叫不同的語言介面給java使用。
  • Execution Engine: 當.class位元組碼檔案被載入後,會把指令和資料資訊存放在記憶體中,此時執行引擎負責把這些命令解釋給作業系統。

類載入器

1 類載入器的過程

image.png

  • 載入:將位元組碼檔案載入到記憶體
  • 校驗:檢驗位元組碼檔案的正確性
  • 準備:給類的靜態變數分配記憶體,並賦予預設值
  • 解析:類裝載器裝入類所引用的其他物件
  • 初始化:對類的靜態變數初始化為指定值,執行靜態程式碼塊

2 類載入的種類

  1. 啟動類載入器:負責載入JRE的核心類庫

  2. 擴充套件類載入器:負責載入JRE擴充套件的ext中的JAR類包

  3. 系統類載入器:負責載入ClassPath路徑下的類包

  4. 使用者自定義載入器:負責載入使用者自定義路徑下的類包

3 類載入機制

  1. 全盤負責委託機制:當類載入器載入一個類時,除非顯示的是另一個載入器,該類鎖依賴的和應用的類也由這個類載入器載入

  2. 雙親委派機制:當一個類載入器收到了類載入的請求的時候,他不會直接去載入目標類,首先委派父類載入器去尋找目標類,只有父載入器無法載入這個類的時候,才會在自己路徑中查詢並載入目標類。

Java虛擬機器採用的是雙親委派模式,雙親委派機制的優勢:避免類的重複載入,保護程式安全,防止核心API被隨意篡改

執行時資料區域

執行時資料區域總共分為五部分:分別是Java虛擬機器棧、本地方法棧、程式計數器、堆、方法區。

方法區: 負責儲存.class檔案,並且這塊有一個執行常量池,就是儲存一些變數或者常量資訊的。

堆: 分配記憶體給物件,比如我們new的物件,就存在堆裡面。

java虛擬機器棧: 也可稱為執行緒棧,每個執行緒獨享的記憶體空間。

本地方法棧: 本地native方法獨享的記憶體空間。

程式計數器: 記錄執行緒執行的位置,方便執行緒切換後再次執行。

Java虛擬機器棧

比如我們的main方法,呼叫sum函式,執行一個和的運算,此時我們的Java虛擬機器棧就會為期分配棧幀記憶體區域。

````java public class MainDemo {

// 一個方法對應一塊棧幀記憶體區域
public static Integer sum() {
    int a = 1;
    int b = 2;
    return a + b;
}

// main方法也對應一塊棧幀記憶體區域
public static void main(String[] args) {
    Integer sum = sum();
    System.out.println(sum);
}

} ```` 首先我們來看一下他的執行順序是怎樣的?首先是先呼叫main函式,隨後再去呼叫sum函式,sum運算結束之後,再銷燬棧記憶體,其次再返回到main函式,等到main結束之後,再銷燬main的棧記憶體空間,這個過程main先執行了,卻是最後退出,即棧幀內部的資料結構即是先進後出(FILO)。

image.png

我們將上面的demo進行反彙編,翻譯成JVM虛擬機器的彙編程式碼:

java javap -c MainDemo.class > MainDemo.txt

image.png

然後我們開啟MainDemo.txt檔案,裡面就是一堆的JVM執行的彙編程式碼。

````java Compiled from "MainDemo.java" public class com.dt.thread.java.MainDemo { public com.dt.thread.java.MainDemo(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return

public static java.lang.Integer sum(); Code: 0: iconst_1 1: istore_0 2: iconst_2 3: istore_1 4: iload_0 5: iload_1 6: iadd 7: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 10: areturn

public static void main(java.lang.String[]); Code: 0: invokestatic #3 // Method sum:()Ljava/lang/Integer; 3: astore_1 4: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 7: aload_1 8: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 11: return } ```` 這一堆的程式碼,怎麼來解讀呢?其實Oracle官方有專門的指令碼文件來解讀。這裡我們就來簡單來解讀一下

iconst_1 將int型別常量1壓入棧

istore_0 將int型別值存入區域性變數0

iconst_2 將int型別常量2壓入棧

istore_1 將int型別值存入區域性變數1

iload_0 從區域性變數0中裝載int型別值

iload_1 從區域性變數1中裝載int型別值

iadd 執行int型別的加法

invokestatic 呼叫類(靜態)方法

areturn 從方法中返回引用型別的資料

我們棧幀內部存放的是一些區域性變數,運算元棧,動態連結串列,方法出口。

image.png

這裡當我們的棧中的區域性變數是物件的時候,那麼此時我們儲存的是堆記憶體空間中物件的地址。

image.png

Java虛擬機器啟動時建立,用於存放物件例項,幾乎所有的物件包括常量池都在堆上分配記憶體,當物件無法在記憶體申請記憶體時,就會丟擲OOM(OutOfMemoryError)異常。

image.png

所有的類都是在Eden Space(伊甸區)new出來的,當伊甸區空間用完了,程式又需要建立物件,JVM的垃圾回收器將對伊甸區進行垃圾回收(Minor GC),將伊甸區中不再被其它物件所引用的物件銷燬,然後被引用的剩餘物件移到倖存者0區,當0區空間不夠用,再次進行GC,然後移動到1區,如果1區也滿了,將會轉移到0區,倖存者0區和1區中反覆存在,經過多次GC,超過15次的存活物件,最後將會進入到老年區,如果老年區記憶體空間也滿了,將會產生MajorGC,進行老年區的記憶體清理,如果老年代執行了MajorGC之後,任然無法進行物件的儲存,也會產生OOM(OutOfMemoryError)異常。

總結

GC是垃圾回收機制,java中申請的記憶體可以被垃圾回收裝置進行回收,GC可以一定程度的避免記憶體洩漏,但是會引入一些額外的開銷。 GC中主要回收的是堆和方法區中的記憶體,棧中記憶體的釋放要等到執行緒結束或者是棧幀被銷燬,而程式計數器中儲存的是地址不需要進行釋放。