初步瞭解JVM第二篇

在一篇《初步瞭解JVM第一篇》中,咱們已經瞭解了:html

  • 類加載器:負責加載*.class文件,將字節碼內容加載到內存中。其中類加載器的類型有以下:
    • 啓動類加載器(Bootstrap)
    • 擴展類加載器(Extension)
    • 應用程序類加載器(AppClassLoader)
    • 用戶自定義加載器(User-Defined) 
  • 執行引擎:負責解釋命令,提交給操做系統執行。
  • 本地接口:目的是爲了融合不一樣的編程語言提供給Java所用,可是企業中已經不多會用到了。
  • 本地方法棧:將本地接口的方法在本地方法棧中登記,在執行引擎執行的時候加載本地方法庫
  • PC寄存器:是線程私有的,記錄方法的執行順序,用以完成分支、循環、跳轉、異常處理、線程恢復等基礎功能。

那在這一篇中咱們來聊一聊方法區、棧和堆。java

繼上一篇順序的PC寄存器編程

5.方法區架構

在JVM的架構圖中,Java棧、本地方法棧、程序計數器都是線程私有的。而方法區跟堆同樣,是一個內存共享的區域,他的主要做用就是存儲每個類的結構信息,例如運行時常量池(Runtime Constant Pool)、字段和方法數據、構造函數和普通方法的字節碼內容。編程語言

再簡單來講方法區就是一個類的模板,在上一篇咱們已經說了ClassLoader將class文件加載完成以後會把類的字節碼內容放到方法區中,就像把Car.class文件經過類加載器加載後,會把car這個類的結構信息存放在方法區中。當你要實例化的時候再經過這個模板去new出你想要的car1,car2,car2,而你建立出來這些類對象是存放在堆(heap)中的。函數

圖一是方法區中存放的內容spa

 圖一操作系統

方法區的實現:線程

方法區只是一個定義、一個規範。在不一樣的虛擬機裏頭實現是不同的。這裏咱們主要介紹的是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垃圾回收機制
相關文章
相關標籤/搜索