JVM 系列文章之 Java 的內存區域

前言

下面關於 Java 的內存區域介紹大部分參考深入理解Java虛擬機,也參考了網上很多資料,以下圖片均摘自網絡

運行時數據區域

Java虛擬機在執行 Java 程序的過程中會把它管理的內存劃分爲若干個不同的數據區域。根據《Java 虛擬機規範》將 Java虛擬機所管理的內存分爲以下幾個運行時數據區域:
- 程序計數器
- Java虛擬機棧
- 本地方法棧
- Java堆
- 方法區

jvm_data

程序計數器

程序計數器 ,也稱作 PC寄存器或者指令地址寄存器。在彙編語言中,它保存的是程序當前執行的指令的地址(或者說是保存一條),當CPU需要執行指令時,需要從程序計數器中得到當前需要執行的指令所在存儲單元的地址,然後根據得到的地址獲取指令,在得到指令之後,程序計數器便自動加1或者根據轉移指針得到下一條指令的地址,如此循環,直至執行完所有的指令。

在JVM中,程序計數器是一塊較小的內存空間,它可以看做是當前線程所執行的字節碼的行號指示器,字節碼解釋器工作時就是通過改變這個計數器的值來選取嚇一跳需要執行的字節碼指令

由於Java 虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時間,一個處理器(對於多核處理器來說是一個內核)都只會執行一條線程中的指令。因此,爲了線程切換後能夠恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,這是 “線程私有的”

如果線程正在執行的是一個 Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是 Native方法,計數器值爲空,此內存區域是唯一一個在 Java虛擬機規範中沒有規定任何 OutOfMemoryError 情況的區域

Java虛擬機棧

Java虛擬機棧也是線程私有的,它的生命週期與線程相同,它描述的是 Java 方法執行的內存模型: 每個方法在執行的同時都會創建一個棧幀( Stack Frame)用於存儲局部變量表,操作數棧,動態鏈接,方法出口等信息。每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。至於關於棧幀的具體介紹後續文章再分析。

因爲除了棧幀的出棧和入棧之外,Java虛擬機棧不會再受其他因素的影響,所以 棧幀可以在系統的堆中分配(注意,是系統的Heap而不是Java 堆)

JVM保留了兩個內存區:Java 堆和本機(或系統堆)。這個堆具有不同的用途,並使用不同的機制進行維護,Java堆就是我下面要將的包含對象實例的”堆”,而系統的堆使用操作系統的底層 malloc 和 free機制進行分配,且用於底層實施特定的Java對象。

Java虛擬機棧所使用的內存不需要保證是連續的。

Java虛擬機棧可能發生如下異常情況:
- 如果線程請求的棧深度大於虛擬機所允許的深度,將拋出 StackOverflowError 異常
- 如果虛擬機棧可以動態擴展(當前大部分的Java虛擬機都可以動態擴展,只不過Java虛擬機規範中也允許固定長度的虛擬機棧),如果擴展時無法申請到足夠的內存,就會拋出 OutOfMemoryError 異常。

注意: Java虛擬機棧就是棧,也可以成爲”堆棧”,只是堆棧這種說法容易讓人混淆。關於Java虛擬機的堆,棧,堆棧如何去理解這類問題,JVM專家R大也在知乎上對其進行了詳細的解答,傳送門: Java虛擬機的堆、棧、堆棧如何去理解? - RednaxelaFX的回答 - 知乎

本地方法棧

本地方法棧(Native Method Stack)與虛擬機棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機棧爲虛擬機執行 Java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的是 Native方法服務。在虛擬機規範中對本地方法棧中方法使用的語言,使用方法與數據結構並沒有強制規定,因此具體的虛擬機可以自由實現它。

看到這個本地方法棧,總是對本地方法有些疑惑,下面簡單說下Native Method

什麼是 Native Method

一個Natvie Method就是一個Java調用非Java代碼的接口,它由非Java語言實現,比如C語言

在定義一個 Native Method時,並不提供實現體(有些像定義一個 Java Interface),因爲其實現體是由非Java語言在外面實現的,比如:

public class IHaveNatives
    {
      native public void Native1( int x ) ;
      native static public long Native2() ;
      native synchronized private float Native3( Object o ) ;
      native void Native4( int[] ary ) throws Exception ;
    }

native方法可以返回任何 Java類型,也能夠實現異常控制。

爲什麼使用Native Method

Java對一些層次的任務用 Java實現不容易,對某些程序效率不高:
- Java與Java外的環境交互:

Java與一些底層系統如操作系統或某些硬件交換信息,native方法提供一個非常簡潔的接口,無需瞭解Java應用之外的細節。
- 與操作系統交互
通過使用本地方法讓 Java實現 JRE與底層系統的交互
- Sun’Java
Sun的解釋器是用 C實現的,JRE 大部分用 Java實現,其通過一些本地方法與外界交互

所以對於本地方法棧來說,它本質是爲本地方法服務的,如果某個虛擬機實現的本地方法接口是使用 C連接模型的話,那麼它的本地方法棧就是 C棧。下圖展示了一個Java棧和本地方法棧之間的跳轉,(圖片摘自網絡):
native

Java堆

對於大多數應用來說,Java 堆(Java Heap)是Java 虛擬機所管理的內存中最大的一塊。Java 堆是被所有線程共享的一塊內存區域,在虛擬機啓動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這裏分配內存

Java堆是垃圾收集器管理的主要區域,因此很多時候也被稱作爲 “GC堆”。從內存回收的角度看,由於現在收集器基本採用分代收集算法,(關於垃圾算法的介紹後續文章分析),所以 Java 堆中還可以細分爲: 新生代和老年代,再細緻一點有 Eden空間,From Survivor空間,To Survivor 空間等

根據 Java 虛擬機規範的規定, Java 堆可以處於物理不連續的內存空間中,只要邏輯是連續的即可,就像我們的磁盤空間一樣。在實現時,即可以實現成固定大小的,也可以是可擴展的,不過當前主流的虛擬機都是按照可擴展來實現的 (通過 -Xmx 和 -Xms 控制)。

如果堆上沒有內存完成實例分配,並且 堆也無法再擴展時,將會拋出 OutOfMemoryError異常。

JVM中堆和棧的區別

這裏簡單說說JVM中堆和棧的區別:
- 功能不同
- 棧內存用來存儲局部變量,操作數棧等信息
- 堆內存用來存儲Java中的對象
- 共享性不同
- 棧內存是線程私有的
- 堆內存是所有線程共有的
- 空間大小
- 棧的空間大小遠遠小於堆的
- 異常錯誤不同
- 棧有兩種異常情況,如果線程請求的棧深度大於虛擬機所允許的深度,將拋出 StackOverflowError 異常,如果虛擬機棧動態擴展時無法申請到足夠的內存,就會拋出 OutOfMemoryError 異常。
- 堆一般在堆中沒有內存完成實例分配,並且堆也無法進行擴展時,拋出 OutOfMemoryError 異常。

方法區

方法區(Method Area) 與 Java堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息,常量,靜態變量,即時編譯器編譯後的代碼等數據。雖然Java虛擬機規範把方法區描述爲 堆的一個邏輯部分,但是它有一個別名叫做 “非堆”.

對於HotSpot虛擬機來講,方法區域又被稱爲 “永久代”,本質上兩者並不等價,僅僅是因爲HotSpot虛擬機的設計團隊選擇把 GC 分代收集擴展至方法區,或者說使用 永久代來實現方法區而已,這樣的HotSpot的垃圾收集器可以像 管理 Java堆一樣管理這部分內存,能夠省去專門爲方法區編寫內存管理代碼的工作。對於其他虛擬機(如 BEA JRockit,IBM J9等)來說是不存在永久代的概念的。

在JDK 1.7及以前的HotSpot JVM中,方法區位於永久代(Permanent Generation,PermGen) 中。如下圖,是JDK 1.7及以前的 Java堆內存的結構圖,裏面包含了 Permanent Generation:
permanent

由於 永久代內可能發生內存泄漏或溢出的問題(永久代有 -XX:MaxPermSize的上限)而導致的 java.lang.OutOfMemoryError: PermGen space,JEP小組從JDK 1.7 開始就籌劃移除永久代 (JEP 122: Remove the Permanent Generation,並且在 JDK 1.7 中把字符串常量,符號引用等移除了永久代。到了Java 8 ,永久代被徹底地移除了 JVM,取而代之的是元空間 (Metaspace)
permanent generation remove
JDK 8開始將類的元數據放到本地堆內存(native heap)中,這一塊區域就叫 Metaspace,關於 Metaspace 的介紹請參考 Metaspace in Java 8

運行時常量池

前面講 方法區的時候就提到,運行時常量池是方法區的一部分,它是 class文件中每一個類或接口的常量池表的運行時表示形式。它包括了若干種不同的常量,常量池表存放 編譯器生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。
changliang
運行時常量池具有動態性,運行期間也可以將新的量放到運行時常量池中,典型的應用是 String 類的 intern方法:

public native String intern()

String類的 intern方法會從字符串常量池中查詢當前字符串是否存在,如果存在,就會直接返回當前字符串,若不存在就會將當前字符串放入常量池中,再返回。關於String.intern()更爲詳盡的分析,請參閱文章: 深入解析String#intern

而從JDK 1.7開始,字符串常量和符號引用等被移除永久代:
- 符號引用遷移至系統堆內存 (Native Heap)
- 字符串字面量遷移至 Java堆(Java Heap)

小結

以上的分析參考了深入理解Java虛擬機這本書,同時也參考了很多優秀的文章。在此過程中,我們要注意JDK版本變化帶來的問題,比如在 JDK 8版本中,永久代被徹底移除了。

當上面提到一個JVM的巨牛級別的人物——R大,R大是國內JVM巨牛級人物,他的回答都是非常權威的,所以學習 JVM的知識可以多參考 R大的分析。目前本人對 JVM也是一枚渣渣級選手,現在輸出對JVM的一些學習筆記。如有錯誤之處,歡迎指出。

參考資料 & 鳴謝

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