在一篇《初步瞭解JVM第一篇》中,我們已經瞭解了: 類載入器:負責載入*.class文件,將位元組碼內容載入到記憶體中。其中類載入器的類型有如下: 啟動類載入器(Bootstrap) 擴展類載入器(Extension) 應用程式類載入器(AppClassLoader) 用戶自定義載入器(User-Def ...
在一篇《初步瞭解JVM第一篇》中,我們已經瞭解了:
- 類載入器:負責載入*.class文件,將位元組碼內容載入到記憶體中。其中類載入器的類型有如下:
- 啟動類載入器(Bootstrap)
- 擴展類載入器(Extension)
- 應用程式類載入器(AppClassLoader)
- 用戶自定義載入器(User-Defined)
- 執行引擎:負責解釋命令,提交給操作系統執行。
- 本地介面:目的是為了融合不同的編程語言提供給Java所用,但是企業中已經很少會用到了。
- 本地方法棧:將本地介面的方法在本地方法棧中登記,在執行引擎執行的時候載入本地方法庫
- PC寄存器:是線程私有的,記錄方法的執行順序,用以完成分支、迴圈、跳轉、異常處理、線程恢復等基礎功能。
那在這一篇中我們來聊一聊方法區、棧和堆。
5.方法區
在JVM的架構圖中,Java棧、本地方法棧、程式計數器都是線程私有的。而方法區跟堆一樣,是一個記憶體共用的區域,他的主要作用就是存儲每一個類的結構信息,例如運行時常量池(Runtime Constant Pool)、欄位和方法數據、構造函數和普通方法的位元組碼內容。
再簡單來說方法區就是一個類的模板,在上一篇我們已經說了ClassLoader將class文件載入完成之後會把類的位元組碼內容放到方法區中,就像把Car.class文件通過類載入器載入後,會把car這個類的結構信息存放在方法區中。當你要實例化的時候再通過這個模板去new出你想要的car1,car2,car2,而你創建出來這些類對象是存放在堆(heap)中的。
圖一是方法區中存放的內容
圖一
方法區的實現:
方法區只是一個定義、一個規範。在不同的虛擬機裡頭實現是不一樣的。這裡我們主要介紹的是JDK7和JDK8的實現方式
- JDK7:永久代(PermGen space)
- JDK8:元空間(Metaspace)
永久代
在JDK7中方法區的實現方式叫永久代,但是它存儲的部分數據是存放在JVM的一塊地方的,這會造成一個問題:
當類載入太多了,可能會導致記憶體棧溢出:java.lang.OutOfMemoryError: PermGen,這樣一來就不夠靈活,為了提高靈活性(這隻是其中一個原因)就有了元空間
元空間:
在JDK8中,JVM的開發者就把永久代移除了,移至元空間中。其實作用是差不多的,只是元空間不再使用JVM的記憶體了,而是直接使用本地堆記憶體(native heap),說白了就是直接使用系統的記憶體,這樣就幾乎不會發生記憶體溢出的情況,提高了靈活性。
所以為什麼在網上會看到關於方法區很多不同的說法就是因為方法區的實現方式在不同的JVM中是不同,最典型的就是永久代和元空間。
以上我們總結出:
- 方法區:類似一個模板,存儲一個類的結構信息。
- 實現方法:
- 永久代:使用JVM的記憶體。
- 元空間:使用系統記憶體。
以上就是方法區的介紹,在介紹堆的時候還會提及。
6.Stack棧
棧是一個線程私有的,主要用來管理Java程式的運行。是線上程創建的時候創建的,它的生命周期跟隨這線程的結束而結束,當線程結束了棧的記憶體也就釋放了,對於棧來說,不會存在垃圾回收問題,因為只要線程一結束該棧就結束了。
棧中主要存儲的內容:
- 8種基本數據類型
- 對象的引用變數
- 實例方法
棧就類似一個子彈夾,它的特點就是“後進先出,先進後出”,在Java中需要實現很多方法,而這些方法就是一個一個被壓進棧中的,然後再依次調用。在平常中,我們所說的Java中的方法在棧其實有一個專有名詞叫棧幀,棧幀主要存放三類數據:
- 本地變數(Local Variables):輸入參數和輸出參數以及方法內的變數。
- 棧操作(Operand Stack):記錄出棧、入棧的操作。
- 棧幀數據(Frame Data):包括類文件、方法等等。
棧運行原理:
Java中的方法存放在棧中,但是這些方法到底是怎麼執行的呢?
接下來我們就用一個例子來說明一下:
package testJVM;
public class TestStack {
public static void method_one(){
System.out.println("This is the method_one");
}
public static void method_two(){
System.out.println("This is the method_two");
}
public static void main(String[] args) {
System.out.println("This is the main method");
//調用方法一
method_one();
//調用方法二
method_two();
//輸出程式結束
System.out.println("The program is finish");
}
}
以上的運行結果為:
這樣的輸出結果,相信已經在大家的預料之中,但是這些方法在棧中是怎麼運行的呢?廢話不說,上圖二
圖二
我們都知道main方法是一切程式的入口,所以程式一執行碰到的是main方法,main方法就第一個入棧了,所以他們的執行過程是這樣的:
- 程式執行碰到第一個方法是main方法,main方法入棧。
- main方法遇到的方法是method_one,將其入棧。
- 再遇到的下一個方法是method_two,將其放入棧。
所以就形成了圖二,當他運行的時候:
- 彈出method_two方法,在我們圖三中的箭頭就是PC寄存器的作用,所以在執行method_two,我們需要調用method_one方法。
- 彈出method_one,下一步,我們看到圖二有指針指向main方法。
- 彈出main方法,全部出棧。
這樣就形成了類似一條執行鏈,依次執行了main方法。
總結棧運行原理:
棧中的數據都是以棧幀(Stack Frame)的格式存在,棧幀是一個記憶體區塊,是一個數據集,是一個有關方法(Method)和運行期數據的數據集,當一個方法A被調用時就產生了一個棧幀 F1,並被壓入到棧中, A方法又調用了 B方法,於是產生棧幀 F2 也被壓入棧, B方法又調用了 C方法,於是產生棧幀 F3 也被壓入棧, 執行完畢後,先彈出F3棧幀,再彈出F2棧幀,再彈出F1棧幀…… 遵循“先進後出”和“後進先出”原則。每個方法執行的同時都會創建一個棧幀,用於存儲局部變數表、操作數棧、動態鏈接、方法出口等信息,每一個方法從調用直至執行完畢的過程,就對應著一個棧幀在虛擬機中入棧到出棧的過程。棧的大小和具體JVM的實現有關,通常在256K~756K之間,與等於1Mb左右。
棧溢出
講完了棧的內容,現在我們來看一個大家在實際開發中會碰到的一個錯誤,請看下列代碼:
package testJVM;
public class TestStack {
public static void method_one(){
//遞歸調用
method_one();
}
public static void main(String[] args) {
method_one();
}
}
上述是一個遞歸調用的例子,現在來執行一下,看看會出現一個什麼結果:
相信大家多多少少都會遇到過上述的錯誤,棧溢出。原因如下:
由於我們的方法method_one一直在遞歸調用自己,而且並沒有停止的條件。所以method_one這個方法就會被一直壓入棧中,JVM中的記憶體又是有限的,上述我們也提到了Java中的棧是隨著線程的生命周期結束而結束的,不會存在垃圾回收機制,記憶體得不到釋放而方法又不斷的進棧,最終記憶體不夠造成棧溢出的現象。圖三
圖三
以上就是本人對棧的理解,最後來到了重頭戲堆(heap),那就下篇再進行介紹吧,哈哈哈。
在下篇將會介紹:
- 堆(heap)
- GC垃圾回收機制