Java虛擬機基礎——2JVM運行時數據區

Java虛擬機整體篇幅如下:

本篇文章主要講解JVM運行時數據區,所以我們按照線程是否私有的維度將本篇文章一分爲二,分爲線程私有數據區所有線程共有的數據區。而在線程私有的數據區又可以分爲程序計數器虛擬機棧本地方法棧;所有線程共有的數據區又可以分爲Java堆方法區。 思維導圖如下:

JVM運行時數據區.png

事實上,JVM在執行Java代碼時都會把內存分爲幾個部分,即數據區域來使用,這些區域都有自己的用途,並隨着JVM進程的啓動或者用戶線程啓動和結束或銷燬。接下來我們通過下面這幅圖,我們一個一個細數一下JVM運行時的數據區結構。

JVM運行時數據區.png

所以本片文章的主要內容也如此:

  • 1、 線程私有數據區
    • 1.1、程序計數器
    • 1.2、虛擬機棧
    • 1.3、本地方法棧
  • 2、線程共享數據區
    • 2.1、Java堆
    • 2.2、方法區
    • 2.3、元空間與持久代
  • 3、執行引擎
  • 4、總結
  • 5、思考

一、線程私有數據區

(一)、程序計數器(Program Counter Register)

程序計數器(Program Counter Register),也稱作PC寄存器。想必學過彙編語言的朋友對程序計數器這個概念應該並不陌生,在彙編語言中,程序計數器是指CPU中的寄存器,它保存的是程序當前執行的指令地址(也可以說保存下一條指令的所在存儲單元的地址),當CPU需要執行指令時,需要從程序計數器中得到當前需要執行的指令所在存儲單元的地址,然後根據得到的地址獲取到指令,在得到指令之後,程序機器便自動加1或者根據轉移指針得到下一條指令的地址,依次循環,直至執行完所有的指令

雖然JVM中的程序計數器並不像彙編語言中的程序計數器一樣是物理概念上的CPU寄存器,但是JVM中的程序計數器的功能跟彙編語言的程序計數器的功能在邏輯上是等同的,也就是說用來指示執行那條指令的。由於在JVM中,多線程是通過線程輪流切換來獲得CPU執行時間的,因此,在任一具體時刻,一個CPU的內核只會執行一條線程中的指令,因此爲了能夠使得每個線程都在線程切換後能夠恢復在切換之前的程序執行位置,每個線程都需要有自己獨立的程序計數器,並且不能相互干擾,否則就會影響到程序的正常執行次序。因此,可以這麼說,程序計數器是每個線程所私有的。

1、作用:

記錄當前線程執行到的字節碼的行號,字節碼的解釋器工作的時候就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令。

2、意義:

JVM的多線程是通過線程輪流切換並分配由處理器來實現的,對於我們來說的並行事實上一個處理器也只會執行一條線程中的指令。所以,爲了保證各線程指令的安全順利執行,每條線程都有獨立的私有程序計數器。

3、存儲內容:

  • 當線程中執行的是一個Java方法時,程序計數器中記錄的是正在執行的線程的虛擬機字節碼指令的地址。
  • 當線程中執行的是一個本地方法時,程序計數器的值爲空。

4、異常

此內存區域是JVM裏面唯一一個不會發生內存溢出(OOM OutOfMemoryError)的區域。

(二) 虛擬機棧(Java Virtual Machine Stacks)

虛擬機棧也就是我們常常說的棧,跟C語言的數據段中棧類似,事實上,Java棧是Java方法執行的內存模型。Java棧中存放的是一個個棧幀。並且是線程私有,生命週期與線程相同,描述的是Java方法執行的內存模型:每一個方法執行的同時都會創建一個棧幀(Stack Frame),由於存儲局部變量表、操作數棧、動態鏈鏈接、方法出口等信息。每一個方法的執行就是對應棧幀在虛擬機棧中的入棧、出棧的過程。當一個線程執行一個方法時,就會隨之創建一個對應的棧幀,並將建立的棧幀壓棧。當方法執行完畢之後,便會將棧幀移除棧。因此可知,線程當前執行的方法所對應的棧幀必定位於Java的棧頂。講到這裏,大家就應該會明白爲什麼在使用遞歸方法的時候容易導致棧內溢出的現象了以及爲什麼棧區的空間不用程序員去管理了(當然在Java中,程序員基本不用關係到內存分配和釋放的事情,因爲Java有自己的垃圾回收機制),這部分空間的分配和釋放都是由系統自動實施的。對於所有程序設計語言來說,棧這部分空間對程序員來說是不透明的。下圖表示了一個Java棧的模型

虛擬機棧.png

棧.png

1、作用:

描述Java方法執行的內存模型。每個方法在執行的同時都會開闢一段內存區域用於存放方法運行時所需要的數據,稱爲棧幀,一個棧幀包含如:局部變量表、操作數棧、動態鏈接、方法出口等信息。

2、意義

JVM是基於棧的,所以每個方法從調用到執行結束,就對應一個棧幀在虛擬機棧中入棧和出棧的整個過程。

3、存儲內容

局部變量表(編譯器可知的各種基本數據類型、引用類型和指向一條字節碼指令的returnAddress類型)、操作數棧、動態鏈接、方法出口等信息。值得注意的是:局部變量表所需的內存空間在編譯期間完成分配。在方法運行的階段是不會改變局部變量表的大小的。

4、虛擬機棧內容簡介:

4.1 局部變量表:

存放編譯器可知的各種基本數據類型、對象引用類型和returnAddress類型(指向一條字節碼指令的地址:函數返回地址)。long、double、佔用兩個局部變量控件的Slot。局部變量表所需要的內存空間在編譯器確定,當進入一個方法時,方法在棧幀中所需要分配的局部變量控件是完全確定的,不可動態改變大小。

4.2 操作數棧:

後進先出LIFO,最大深度由編譯期決定。棧幀剛建立時,操作數棧爲空,執行方法操作時,操作數棧用於存放JVM從局部變量表複製的常量或者變量,提供提取,及結果入棧,也用於存放調用方法需要的參數及接受方法返回的結果。操作數棧可以存放一個JVM中定義的任意數據類型的值。在任意時刻,操作數棧都有一個固定的棧深度,基本類型除了long、double佔用兩個深度,其他佔用一個深度。

4.3 動態鏈接:

每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程中的動態鏈接。Class文件的常量池中存在有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的服務引用爲參數。這些符號引用,一部分會在類加載階段或者第一次使用的時候轉化爲直接引用(如final、static域等),稱爲靜態解析,另一部分將在每一次的運行期間轉化爲直接應用,這部分稱爲動態鏈接。

4.4 方法返回地址:

  • 當一個方法被執行後,有兩種方式退出該方法:執行引擎遇到任意一個方法返回的字節碼指令 或遇到了 異常 ,並且該異常沒有在方法體內得到處理。無論採用何種方式退出,在方法退出之後,都需要返回到方法被調用的位置,程序才能繼續執行。方法返回時可能需要在棧幀中保存一些信息,用來幫助恢復它的上層方法的執行狀態。一般來說方法正常退出時,調用者的程序計數器的值就可以作爲返回地址,棧幀中很可能保存了這個計數器的值,而方法異常退出時,返回地址是要通過異常處理器來確定的,棧幀中一般不保存這部分信息。
  • 方法退出的過程實際上等同於把當前棧幀出棧,因此退出時可能執行的操作有:回覆上層方法的局部變量表和操作數棧,如果有返回值,則把它壓入調用者棧幀的操作數棧中,調用程序計數器的值以指向方法調用指令後面的一條指令。

5、異常

如果線程請求的棧深入大於虛擬機所允許的深度,將拋出StackOverflowError異常。如果虛擬機棧可以動態擴展(大部分虛擬機允許動態擴展,也可以設置固定大小的虛擬機棧),但是無法申請到足夠的內存,會拋出OutOfMemorError。

(三) 本地方法棧(Native Method Stacks)

本地方法棧與虛擬機棧所發揮的作用很相似,他們的區別在於虛擬機棧爲執行Java代碼方法服務,而本地方法棧爲Native方法服務。與虛擬機棧一樣,本地方法棧也會拋出和StackOverflowError和OutOfMemoryError異常。在JVM規範中,並沒有對本地方法的具體實現方法以及數據結構做強制規定,虛擬機可以自由實現它。在HotSopt虛擬機中直接就把本地方法棧和Java棧合二爲一。

1、作用

爲JVM所調用到的Native即本地方法服務

2、異常

如果線程請求的棧深入大於虛擬機所允許的深度,將拋出StackOverflowError異常。

二、線程共享數據區域

(一) Java堆(Java Heap)

Java堆可以說是虛擬機中最大的一塊內存了。它是所有線程共享的內存區域,幾乎所有的實例對象都是在這塊區域中存放。當然,隨着JIT(just in time,及時編譯技術) 編譯器的發展,所有對象在"堆"上分配也變得不那麼"絕對"了。同時Java堆也是垃圾收集器管理的主要區域。由於現在收集器基本上採用的都是分帶收集算法,所有Java堆又可以細分爲:"新生代"和"老年代"。再細緻分就是把新生代分爲:Eden空間、From Survivor空間、To Survivor空間。

1、作用:

所有線程共享的一塊內容區域,在虛擬機開啓的時候創建。

2、意義:

  • 存儲對象實例,更好地分配內存。
  • 垃圾回收(GC),堆是垃圾收集器管理的主要區域。更好的回收內存。

3、存儲內容:

存放對象實例,幾乎所有對象的實例都在這裏進行分配。堆可以處理物理上不連續的內存空間,只要邏輯上連續的就可以。

4、異常:

堆可以是固定大小的,也可以通過設置配置文件來設置該爲可擴展的。如果堆上沒有內存進行分配,並無法進行擴展時,將會拋出OutOfMemoryError異常

(二) 方法區(Method Area)

方法區在JVM中也是一個非常重要的區域,在方法區中,存儲了每個類的信息(包括類的名稱、方法信息、字段信息)、靜態變量、常量以及編譯器編譯後的代碼等。它與堆一樣,是被線程共享的區域,很容易理解,我們在寫Java代碼時,每個線程都可以訪問同一個類的靜態變量。在Class文件中除了類的字段、方法、接口等描述信息外,還有一項信息是常量池,用來存儲編譯期間生成的字面量和符號引用。在方法去還有一個非常重要的部分就是運行時常量池,它是每一個類或接口的常量池的運行時表現形式,在類和接口被加載到JVM後,對應的運行時常量池,在運行期間也可以將新的常量放入運行時常量池,比如String的intern方法。

在JVM規範中,沒有強制要求方法區必須實現垃圾回收,很多人習慣將方法區稱爲"永久代",是因爲HotSpot虛擬機以永久代來實現方法區,從而JVM的垃圾處理器可以像堆區一樣管理這部分的區域,從而不需要專門爲這部分設計垃圾回收機制。不過JDK8之後,Hotspot虛擬機將運行時常量池從永久代移除了。然後引入了一個新的概念"元空間"。

1、作用:

用於存儲運行時常量池、已被虛擬機加載的類信息、常量、靜態變量、即使編譯器編譯後的代碼等數據。

2、意義

對運行時常量池、常量、靜態變量等數據做出了規定。

3、存儲內容

運行時常量池(具有動態性)、已被虛擬機記載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。

4、異常

當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。

(三) 元空間(Metaspace)與持久代(PermGen space)

JDK8 HotSpot JVM使用本地內存來存儲類元數據信息並稱之爲:元空間(Metaspace)。這與Oracle JRockit 和 IBM JVM 很相似。意味着不會再有"ava.lang.OutOfMemoryError: PermGen問題",也不需要你進行調優及監控內存空間的使用。

持久代.png

1、歷史背景

其實移除永久代的工作從JDK 1.7就開始了。JDK1.7中,存儲在永久代的部分數據就已經轉移到Java Heap或者Native Heap。但永久代仍存在於JDK1.7中,並沒有完全移除,譬如符號引用(Symbols)轉移到native heap;字面量(interned strings)轉移到了Java Heap;類的靜態變量(class static)轉移到了Java heap。

2、持久代去哪裏了

  • 持久代的空間被徹底地刪除了,它被一個叫元空間的區域所替代了。持久代刪除了之後,很明顯,JVM會忽略PermSize和MaxPermSize這兩個參數,還有就是你再也看不到java.lang.OutOfMemoryError: PermGen error的異常了。
  • JDK 8的HotSpot JVM現在使用本地的內存來表示類的元數據,這個區域就叫做元空間。

3、爲什麼刪除持久代

  • 1、它的大小是在啓動時固定好的——很難進行調優。
  • 2、HotSpot的內部類型也是Java對象:它可能會在Full GC中被移動,同時它對應用不透明,且是非強類型的,難以跟蹤調試,還需要存儲元數據的元數據信息(meta-metadata).
  • 3、簡化Full GC:每一個回收器有專門的元數據迭代器。
  • 4、可以在GC不進行暫停的情況下併發地釋放類數據。
  • 5、使得原來不受限於持久代的一些改進未來有可能實現。

4、元空間的特點:

  • 充分利用了Java語言規範中的好處:類及相關的元數據的聲明週期與類加載器的一致。
  • 每個加載器有專門的存儲空間
  • 類元數據的空間都是從本地內存中分配
  • 只進行線性分配
  • 不會單獨回收某個類
  • 省掉了GC掃描及壓縮的時間
  • 元空間裏面的對象的位置是固定的
  • 如果GC發現了某個類加載器不再存貨了,會把相關的空間整個回收掉
  • 減少碎片的策略

三、執行引擎(Execution Engine)

類加載器將字節碼載入內存之後,執行引擎以Java字節碼指令爲目標,讀取Java字節碼,問題是,現在的Java字節碼機器是讀不懂的,因此還必須想辦法將字節碼轉化爲平臺相關的機器碼。這個過程可以由解釋器來執行,也可以有即使編譯器(JIT Compiler)來完成。

執行引擎.png

四、總結

先用一張圖演示今天的內容:

image.png

JVM只不過是運行在你操作系統的一個進程而已,這一些的魔法始於一個Java命令。正如任何一個操作系統進程那樣,JVM也需要內存來完成它的於運行時操作。JVM也可以理解爲以硬件的一層軟件抽象,在這之上才能夠運行Java程序,也纔有了我們所吹噓的平臺獨立性以及"write-once-run-anywhere "(一次編寫,處處運行)。

五、思考

老規矩 先拋出一個問題:爲什麼JVM在運行時數據區設計成如此模型?

每個人都有每個人的理解,我先說下我的理解

  • 背景1:Java字節碼.class文件是通過JVM解釋執行的,JVM解釋的時候通過JIT生成相應的機器碼,機器碼也就是機器指令,就是某種CPU的指令集。
  • 背景2:我們知道一個Java類通常由變量和方法組成,一個程序一般有一堆類組成。所以這一堆類構成了一個Java程序。換句話說,一個Java程序是有一堆變量和一堆方法組成的
  • 背景3:Java程序一般都是有個入口方法,然後依次調用對應的某個對象的某個方法,來一步步執行的。
  • OK好的,假如讓我們來設計這個JVM。既然JVM是解釋上面所說的內容,是不是要在JVM裏面設置對應的部分,所以我們要先設計一塊區域用來保存上面說的Java對象——就有了Java堆;也要設計出一塊區域用來保存類信息、常量等——就有了方法區;既然我們的程序要解析方法,我們是不是要保存這個方法的信息,一個方法內部可能很長,我們要知道目前執行的位置,所以有了——程序計數器;因爲方法有本地方法和Java方法之分,對應跟蹤每個方法內部的信息,就衍生出了——本地方法棧和虛擬機棧

上面就是我對JVM運行時數據區的理解,希望能幫助到大家!

大家喜歡就點贊,您的每一次點贊,都是我努力和進步的動力!您可能想不到:您的小小一按,可能就會對另外一個人產生翻天覆地的影響。!最後謝謝您的支持與厚愛

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章