【面試】JVM記憶體結構
theme: smartblue
Java跨平臺執行
我們都知道Java語言一次編譯到處執行,可以在windows上執行也可以在Liunx上執行,屬於跨平臺語言,Java其實就是依賴JVM實現的跨平臺性,但是我們的JVM本身是不存在跨平臺的。
通過Javac.exe編譯.java原始碼檔案生成.class檔案,然後再通過類載入器,將class檔案載入到JVM中,交由JVM執行,最後輸出結果。
我們來記一張簡潔的執行圖:
JVM的組成
JVM由4大部分組成:ClassLoader(類載入器),Runtime Data Area(執行時資料區域),Execution Engine(執行引擎),Native Interface(本地介面)。
- ClassLoader: 負責載入位元組碼檔案,即是java編譯後的.class檔案。
- Runtime Date Area: 存放.class檔案和分配記憶體。
- Native Interface: 負責呼叫本地介面,即是呼叫不同的語言介面給java使用。
- Execution Engine: 當.class位元組碼檔案被載入後,會把指令和資料資訊存放在記憶體中,此時執行引擎負責把這些命令解釋給作業系統。
類載入器
1 類載入器的過程
- 載入:將位元組碼檔案載入到記憶體
- 校驗:檢驗位元組碼檔案的正確性
- 準備:給類的靜態變數分配記憶體,並賦予預設值
- 解析:類裝載器裝入類所引用的其他物件
- 初始化:對類的靜態變數初始化為指定值,執行靜態程式碼塊
2 類載入的種類
-
啟動類載入器:負責載入JRE的核心類庫
-
擴充套件類載入器:負責載入JRE擴充套件的ext中的JAR類包
-
系統類載入器:負責載入ClassPath路徑下的類包
-
使用者自定義載入器:負責載入使用者自定義路徑下的類包
3 類載入機制
-
全盤負責委託機制:當類載入器載入一個類時,除非顯示的是另一個載入器,該類鎖依賴的和應用的類也由這個類載入器載入
-
雙親委派機制:當一個類載入器收到了類載入的請求的時候,他不會直接去載入目標類,首先委派父類載入器去尋找目標類,只有父載入器無法載入這個類的時候,才會在自己路徑中查詢並載入目標類。
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)。
我們將上面的demo進行反彙編,翻譯成JVM虛擬機器的彙編程式碼:
java
javap -c MainDemo.class > MainDemo.txt
然後我們開啟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."
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 從方法中返回引用型別的資料
我們棧幀內部存放的是一些區域性變數,運算元棧,動態連結串列,方法出口。
這裡當我們的棧中的區域性變數是物件的時候,那麼此時我們儲存的是堆記憶體空間中物件的地址。
堆
Java虛擬機器啟動時建立,用於存放物件例項,幾乎所有的物件包括常量池都在堆上分配記憶體,當物件無法在記憶體申請記憶體時,就會丟擲OOM(OutOfMemoryError)異常。
所有的類都是在Eden Space(伊甸區)new出來的,當伊甸區空間用完了,程式又需要建立物件,JVM的垃圾回收器將對伊甸區進行垃圾回收(Minor GC),將伊甸區中不再被其它物件所引用的物件銷燬,然後被引用的剩餘物件移到倖存者0區,當0區空間不夠用,再次進行GC,然後移動到1區,如果1區也滿了,將會轉移到0區,倖存者0區和1區中反覆存在,經過多次GC,超過15次的存活物件,最後將會進入到老年區,如果老年區記憶體空間也滿了,將會產生MajorGC,進行老年區的記憶體清理,如果老年代執行了MajorGC之後,任然無法進行物件的儲存,也會產生OOM(OutOfMemoryError)異常。
總結
GC是垃圾回收機制,java中申請的記憶體可以被垃圾回收裝置進行回收,GC可以一定程度的避免記憶體洩漏,但是會引入一些額外的開銷。 GC中主要回收的是堆和方法區中的記憶體,棧中記憶體的釋放要等到執行緒結束或者是棧幀被銷燬,而程式計數器中儲存的是地址不需要進行釋放。