簡要了解JVM的內存劃分

概括

在如今大家使用的JVM中,一般都將運行時數據區劃分爲以下五塊區域:

  • 方法區(Method Area)
  • 堆(Heap)
  • 虛擬機棧(VM Stack)
  • 本地方法棧(Native Method Stack)
  • 程序計數器(Program Counter Register)

這些區域,又可以按照其中的數據是否線程間可共享,將其劃分爲線程共享區線程獨佔區

其中,線程共享區包括:

  • 方法區

線程獨佔區包括:

  • 虛擬機棧
  • 本地方法棧
  • 程序計數器

整體結構如下圖所示:
結構

線程共享區

線程共享區,也就是所有線程共用的一塊內存區域,其中的數據可以被每個線程訪問,在這部分區域的操作需要注意多線程下的併發安全問題

Java堆

關於Java堆,即使你完全不瞭解JVM,也應該聽說過Java堆的概念,我們通過new來創建的對象就分配在這部分區域,也是最容易發生OutOfMemoryError(內存溢出錯誤)的地方

同時,因爲這部分區域管理着我們所有創建對象的實例,而且是線程共享的區域,所以Java堆也是整個Java內存中最大的一塊區域

關於其存放的內容,正式的描述是:

所有的對象實例和數組

其實數組也屬於對象的一種,所以換句話說,Java堆存放着所有的(更嚴謹的說法是絕大多數)對象實例,因此我們Java的自動內存管理也主要作用於這塊區域

雖然這部分區域屬於線程共享區,但是仍可能在其中劃分出線程私有的分配緩衝區(TLAB),也就是線程私有變量,但依然存放的是對象實例

同時,因爲我們new出來的變量並不由我們手動釋放內存,所以Java堆也是垃圾收集器作用的主要區域,爲了提高效率,JVM會對Java堆做進一步的劃分,這部分內容以及垃圾收集器的概念我會放在接下來的幾篇文章中再進一步描述

剛纔講解的基本都是邏輯層面的概念,在物理層面,Java堆並不是一塊連續的真實內存區域。我們都知道,連續內存的分配一般只會作用於小區域,像Java堆動不動幾十m,甚至幾個g的空間,一般都採用邏輯連續的結構,而真實物理的結構並不是連續的

Java堆默認是可以動態擴展的,可以通過-Xms和-Xmx兩個虛擬機參數來分別調整初始堆大小和最大堆大小,但是動態擴展會額外消耗性能,所以一般爲了使用方便,都會將這兩個參數設爲相同的值,比如:

-Xms:512m -Xmx 512m
方法區

如果你熟悉HotSpot虛擬機,就應該知道HotSpot在之前的版本中,使用了永久代來實現方法區,也就是說HotSpot將Java堆和方法區視爲類似的區域,而實際上,這兩塊區域結構類似,但是存放的東西卻十分不同,在方法區中,主要存放以下內容:

  • 被虛擬機加載的類信息
  • 常量
  • 靜態變量
  • 被JIT編譯後的代碼等數據

一般我們提到方法區的時候,指的就是方法區中的運行時常量池,想了解這個運行時常量池的概念,首先就要了解常量池的概念,一般來說,常量池分爲以下兩種

  • 靜態常量池:即class文件的常量池,保存着類的各種信息,包括字段和方法等,存放在編譯生成的“.class”文件中
  • 運行時常量池:會在class文件加載後將其中的常量池載入方法區中,這部分區域就叫做運行時常量池

現在我們大致清楚了,也就是說方法區的常量池實際上就是“.class”中的常量池部分,我們平時所稱的常量池實際上也是指方法區中的運行時常量池

我們可以看出這裏的存放數據都有一個共同點,那就是基本不會在運行時修改的數據,也可以叫做常量。在這部分區域,依然可能會發生內存溢出的錯誤,所以我們可以通過-XX:MaxPermSize來設置方法區的容量上限

這部分區域存放的數據一般保持時間非常長,並不會像Java堆一樣經常發生垃圾回收,方法區的垃圾回收一般只針對以下兩種情況:

  • 常量池的回收
  • 類型的卸載

如果這部分的回收出現異常,則會成爲內存泄露的隱患,但是這就屬於JVM要考慮的內容了

線程獨佔區

線程獨佔區,顧名思義就是每一個線程都有一個自己的線程獨佔區,線程之間不能相互訪問,只能通過線程共享區中的內容來達到通信的效果

虛擬機棧

虛擬機棧,全程爲Java虛擬機棧,即Java Virtual Machine Stacks,因爲處於線程獨佔區,所以每一個線程都有一個自己的虛擬機棧,其創建和銷燬伴隨着線程的創建和銷燬

這部分區域存放的主要內容是Java方法執行的內存模型,對沒錯,只存放這麼一種內容,有人可能會問,Java方法的執行是怎麼存放在內存中的?在Java虛擬機棧中,使用了棧幀這一結構,在棧幀中,存放了以下內容:

  • 局部變量表(Local Variable Table):存放方法參數和方法內部存放的局部變量,具體的大小在編譯時就已經固定
  • 操作數棧(Operand Stack):字節碼指令會向其中寫入和提取數據,一個棧容量能存放一個32位數據類型,棧的深度在編譯時已經固定
  • 動態鏈接(Dynamic Linking):包含一個指向運行時常量池中存放的該棧幀所屬方法的引用,爲了支持一部分符號變量在運行期間轉換爲直接引用的過程
  • 返回地址(Return Address):也叫方法出口,分爲正常完成出口和異常完成出口
  • 附加信息:取決於虛擬機的具體實現,可能會附加一些調試使用的信息

根據以上信息,棧幀可以理解爲描述方法執行的信息,這裏需要與方法區常量池中存放的方法信息區分開,它們兩者的關係就可以理解爲類和對象實例的關係,一個程序中只有一個類,但是卻可以創建很多這個類的對象實例,也就是說,我們每執行一個方法,就會創建一個棧幀,但是方法的信息數據卻只有一份

既然是棧,就會經常發生入棧和出棧的情況,當方法開始執行時,入棧一個棧幀,方法執行結束後,就會有一個棧幀出棧

這部分的異常有以下兩種情況:

  • StackOverflowError:棧溢出錯誤,原因是線程請求的棧深度大於虛擬機允許的深度,一般發生在遞歸過深的情況
  • OutOfMemoryError:內存溢出錯誤,原因是在虛擬機棧的動態擴展時,無法申請到足夠的內存

關於虛擬機棧,需要記住的就是其中存放的內容含義,以及注意局部變量表和操作數棧的大小在運行期間是不會改變的,其容量大小在編譯時就已經完成分配

本地方法棧

在具體分析之前,我們先需要明確本地方法的含義,如果我們點進過一些底層類中,經常會發現一些方法上帶有native的修飾符,用於標誌一個方法是本地方法,而不是Java方法,比如Object中的hashcode方法:

	public native int hashCode();

我們都知道Java方法是用Java代碼寫的,而本地方法則是用C/C++甚至其他的代碼寫成的,通過執行引擎與本地方法庫接口的交互,來間接調用了其他語言的程序,這就極大提高了效率,因爲C系語言的執行效率是相當高的,我們如果在Java源碼中發現某個方法是native方法,那就可以毫無顧忌的進行調用,不需要考慮效率問題

明白了本地方法的含義,實際上本地方法棧也明白了,因爲本地方法棧和Java虛擬機棧的唯一區別就是,其中保存的不是Java方法,而是本地方法,其餘的棧幀結構完全一致

程序計數器

這部分區域也是最容易理解的一部分區域,用於表示當前線程執行的字節碼的行號,學過計算機組成的應該很容易聯想到計算機中的程序計數器,沒錯,這兩個實際的作用是一致的,任何循環、跳轉和分支等操作都需要依賴於程序計數器

同時需要注意一點,如果正在執行Java方法,則計數器記錄的是正在執行的虛擬機字節碼指令的地址,如果執行的是本地方法,則這個計數器的值爲空。同時,這部分區域也不會發生任何內存的溢出錯誤

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