JVM中有哪些記憶體區域,分別是用來幹什麼的
前言
之前我們探討過一個.class檔案是如何被載入到jvm中的。但是jvm內又是如何劃分記憶體的呢?這個內被載入到了那一塊記憶體中?jvm記憶體劃分也是面試當中必被問到的一個面試題。
什麼是jvm記憶體區域劃分?
其實這個問題非常簡單,JVM在執行我們寫好的程式碼時,他是必須使用多塊記憶體空間的,不同的記憶體空間用來放不同的資料,然後配合我們寫的程式碼流程,才能讓我們的系統執行起來。
舉個最簡單的例子,比如咱們現在知道了JVM會載入類到記憶體裡來供後續執行,那麼我問問大家,這些類載入到記憶體以後,放到哪兒去了呢?想過這個問題嗎?
所以JVM裡就必須有一塊記憶體區域,用來存放我們寫的那些類。
包括我們定義的成員變數,類變數,方法,區域性變數等等,都在jvm記憶體中對應著一塊記憶體來記錄儲存。
存放類的方法區
在JDK1.8之前的版本里,代表JVM的一塊區域。在1.8版本以後,這塊區域的名字改了,叫做“Matespace”,可以認為是“元資料空間”這樣的意思,當然這裡主要存放的還是我們自己寫的各種類的相關資訊。
舉個栗子。有如下兩個類,People類沒有成員變數,而Student類有一個name的類變數。
public class Student{ private static String name = "lisi"; } public class People{ public static void main(){ Student student = new Student(); } }
這兩個類被載入到JVM,就會存放在這個方法區裡面(注意:如果讀過我之前的章節,就會明白這裡的載入代表的是:載入->驗證->準備->解析->初始化,類的所有類變數都會被賦值)。如下圖
執行程式碼指令的程式計數器
我們知道,被載入到jvm的類物件是我們寫的.java檔案被編譯之後的.class檔案。
在編譯過後會將我們的程式碼編譯成計算機能讀懂的位元組碼。而這個.calss檔案就是,就是我們程式碼編譯好的位元組碼了。
載入到記憶體以後,位元組碼執行引擎就開始工作了。去執行我們編譯出來的程式碼指令,如下圖
此時問題來了,我們是不是需要一塊記憶體空間來記錄我們位元組碼執行引擎目前執行到了哪行程式碼?這一塊特殊的記憶體區域就是“程式計數器”
這個程式計數器就是用來記錄當前執行的位元組碼指令的位置。
如下圖:
到這裡我相信會有人產生疑惑,就按照當前的程式碼順序執行就行了,為什麼要記錄執行到哪裡了?
因為我們寫好的程式碼可能會開啟多個執行緒併發的執行不同的程式碼。可能當前執行緒這段程式碼還沒有執行完畢,就上下文切換到另一段程式碼中。
當執行緒再次上下文切換到之前的程式碼時,就需要一個專門記錄當前執行緒執行到了哪一條位元組碼。所以,每一個執行緒都有這自己的程式計數器。
如下圖:
java虛擬機器棧
java程式碼在執行的時候,一定是某個執行緒來執行某個方法中的程式碼。
當執行緒執行到某個方法的時候,如果這個方法有區域性變數,那麼就需要一塊區域來存放區域性變數的資料資訊。這個區域就叫做java虛擬機器棧。
每一個執行緒都有一個自己的java虛擬機器棧,比如說當執行main方法的時候就會有一個main執行緒,用來存放main方法中定義的區域性變數
public static void main(){
People people = new People();
int i = 9;
}
比如上面的main()方法中,其實就有一個"people"的區域性變數,他是引用一個People的例項物件的,這個物件我們先不管他。然後有一個"i"的區域性變數。
如下圖:
我想大家應該都知道棧的資料結構,後進先出。當方法執行完畢以後,這個棧楨就會出棧,裡面的區域性變數資訊就會從記憶體刪除。所以區域性變數是執行緒安全的。因為只有當前執行緒能獲取到這個值。
為什麼要用後進先出的資料結構?
假設a方法當中同步呼叫b方法,此時a方法的棧楨先入棧,然後再是b方法的棧楨入棧。b方法執行完畢後,b方法的棧楨出棧,繼續執行a方法。所以使用一個後進先出的棧結構是非常完美的。
此時jvm的記憶體模型圖如下:
java堆記憶體
這一塊記憶體是非常非常重要的。
我們例項化的所有物件都是存放在這個記憶體中。這個例項化的物件裡面會包含一些資料,我們用上面的程式碼來做栗子。
public class Student{
private String name = "lisi";
public String getNmae(){
return name;
}
}
public class People{
public static void main(){
Student student = new Student();
student.getName();
}
}
還是這個程式碼,當main執行緒執行main()方法的時候,首先在堆記憶體中例項化Student物件,然後在區域性變數中建立student,student存的是例項化Student物件的記憶體地址。然後執行Student物件的getName()方法。
如下圖:
由上圖可以看出來,棧空間是封閉的,是執行緒安全的,而堆記憶體中是我們主要發生執行緒不安全的地方,因為堆記憶體的空間所有的執行緒其實都是能共享的。
此時jvm的記憶體劃分的最終模型為:
其他記憶體區域
很多java程式猿對這一塊區域的接觸是非常少的。
其實在JDK的很多底層程式碼API中,比如NIO。
如果你去看原始碼會發現很多地方的程式碼不是java寫的,而是走的native方法去呼叫本地作業系統裡面的一些方法,可能呼叫的都是c語言寫的方法。
比如說:public native int hashCode();
在呼叫這種native方法的時候,就會有執行緒對應的本地方法棧,這個其實類似於java虛擬機器棧。也是存放各種native方法的區域性變量表之類的資訊。
還有一塊區域,是不是jvm的,通過NIO中的allocateDirect這種API,可以在jva堆外分配記憶體空間,然後通過java虛擬機器棧裡的DirectByteBuffer來引用和操作堆外記憶體空間。
總結
基本上jvm的核心記憶體區域的功能都解釋清楚了,面試能回答到這一個地步應該也能順利通過了。
我們需要重點關注的是方法區,程式計數器,java虛擬機器棧和java堆記憶體這些記憶體區域的作用