JMM內存模型和JVM內存結構
JAVA內存模型(Java Memory Model)
Java內存模型,一般指的是JDK 5 開始使用的新的內存模型,主要由JSR-133: JavaTM Memory Model and Thread Specification 描述。
JMM
就是一種符合內存模型規范的,屏蔽了各種硬件和操作系統的訪問差異的,保證了Java
程序在各種平台下對內存的訪問都能保證效果一致的機制及規范。
內存模型可以理解為在特定的操作協議下,對特定的內存或者高速緩存進行讀寫訪問的過程抽象,不同架構下的物理機擁有不一樣的內存模型,Java虛擬機也有自己的內存模型,即Java內存模型(Java Memory Model, JMM)。在C/C++語言中直接使用物理硬件和操作系統內存模型,導致不同平台下並發訪問出錯。而JMM的出現,能夠屏蔽掉各種硬件和操作系統的內存訪問差異,實現平台一致性,是的Java程序能夠“一次編寫,到處運行”。
JMM主要解決的問題: 解決由於多線程通過共享內存進行通信時,存在的本地內存數據不一致、編譯器會對代碼指令重排序、處理器會對代碼亂序執行等帶來的問題
- 緩存一致性問題其實就是可見性問題。
- 處理器優化是可以導致原子性問題
- 指令重排即會導致有序性問題
內存模型解決並發問題主要采用兩種方式:限制處理器優化和使用內存屏障
參考:
JVM內存結構
傳送門:JAVA 虛擬機規范
HotSpot 白皮書: Memory Management in the Java HotSpot Virtual Machine 它描述了垃圾回收(GC)觸發的內存自動管理
其實 Java 虛擬機的內存結構並不是官方的說法,在《Java 虛擬機規范》中用的是「運行時數據區(Run-Time Data Areas)」這個術語
下圖能很清晰的說明JVM內存結構布局:
JVM內存結構主要有三大塊:
- 堆內存(Heap)
- 方法區/永久代(Method Area/PermGen) 或者別名Non-Heap(非堆)
- 棧(Thraed...)
在《深入理解Java虛擬機(第二版)》中的描述是下面這個樣子的:
按照《JAVA 虛擬機規范》的中所述,可以分為公有和私有兩部分
-
公有部分: 堆[Heap]、方法區[Method Area]、常量池[Constant Pool]
-
私有部分: PC寄存器、VM虛擬機棧、本地方法棧
各個區域的內存大小
通過一張圖了解如何通過參數來控制各個區域的內存大小
參數說明:
-Xms:設置堆的最小空間大小。
-Xmx:設置堆的最大空間大小。
-XX:NewSize設置新生代最小空間大小。
-XX:MaxNewSize設置新生代最大空間大小。
-XX:PermSize設置永久代最小空間大小。
-XX:MaxPermSize設置永久代最大空間大小。
-Xss:設置每個線程的堆棧大小。
沒有直接設置老年代的參數,但是可以設置堆空間大小和新生代空間大小兩個參數來間接控制。
老年代空間大小=堆空間大小-年輕代大空間大小
JAVA堆(Heap)
Java堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建.
這塊區域專門用於 Java 實例對象和數組的內存分配,幾乎所有實例對象都在會這里進行內存的分配
之所以說幾乎是因為有特殊情況,有些時候小對象會直接在棧上進行分配,這種現象我們稱之為「棧上分配」
Java堆的內存划分
Java堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“GC堆”。
如果從內存回收的角度看,由於現在收集器基本都是采用的分代收集算法
,所以Java堆中還可以細分
主要被划分為: 新生代「Young Generation
」和老年代「Old Generation
」
新生代「Young Generation
」又可分為:Eden
、From Survivor 0
、To Survivor 1
根據《JAVA 虛擬機規范》的規定,Java堆可以處於物理上不連續的內存空間中,只要邏輯上是連續的即可,就像我們的磁盤空間一樣。在實現時,既可以實現成固定大小的,也可以是可擴展的,不過當前主流的虛擬機都是按照可擴展來實現的(通過-Xmx
和-Xms
控制)。
堆可以具有固定大小,或者可以根據計算的需要進行擴展,並且如果不需要更大的堆,則可以收縮。
如果在堆中沒有內存完成實例分配,並且堆也無法再擴展時,將會拋出OutOfMemoryError異常。
不同區域的生命周期
- 新建(New)或者短期對象會存放在
Eden
區域 - 幸存或者中期的對象會從Eden拷貝到
Survivor
區域 - 始終存在的或者長期存在的對象將會從
Survivor
拷貝到Old Generation
當有對象需要分配時,一個對象永遠優先被分配在年輕代的 Eden
區,等到 Eden
區域內存不夠時,Java 虛擬機
會啟動垃圾回收。此時 Eden
區中沒有被引用的對象的內存就會被回收,而一些存活時間較長的對象則會進入到老年代。
在 JVM 中有一個名為 -XX:MaxTenuringThreshold
的參數(默認為7)專門用來設置晉升到老年代所需要經歷的 GC 次數,即在年輕代的對象經過了指定次數的 GC 后,將在下次 GC 時進入老年代
為什么 Java 堆要進行這樣一個區域划分呢
虛擬機中的對象必然有存活時間長的對象,也有存活時間短的對象,這是一個普遍存在的正態分布規律。如果我們將其混在一起,那么因為存活時間短的對象有很多,那么勢必導致較為頻繁的垃圾回收。而垃圾回收時不得不對所有內存都進行掃描,但其實有一部分對象,它們存活時間很長,對他們進行掃描完全是浪費時間。因此為了提高垃圾回收效率
,分區就理所當然了
虛擬機Heap Space
的默認配置為: Eden:from :to = 8:1:1
其實這是 IBM 公司根據大量統計得出的結果。根據 IBM 公司對對象存活時間的統計,他們發現 80% 的對象存活時間都很短。於是他們將 Eden 區設置為年輕代的 80%,這樣可以減少內存空間的浪費,提高內存空間利用率
方法區(Method Area)
方法區(Method Area)與Java堆一樣,是各個線程共享的內存區域,在虛擬機啟動時創建。
它用於存儲已被虛擬機加載的類結構,如:運行時常量池、靜態變量、字段、和方法數據,即時編譯器編譯后的代碼等數據,以及方法和構造函數的代碼
Java虛擬機規范對這個區域的限制非常寬松,除了和Java堆一樣不需要連續的內存和可以選擇固定大小或者可擴展外,還可以選擇不實現垃圾收集。相對而言,垃圾收集行為在這個區域是比較少出現的,但並非數據進入了方法區就如永久代的名字一樣“永久”存在了。這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載,一般來說這個區域的回收“成績”比較難以令人滿意,尤其是類型的卸載,條件相當苛刻,但是這部分區域的回收確實是有必要的。
根據《Java虛擬機規范的規定》,當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。
方法區在不同版本的虛擬機有不同的表現形式
例如在 1.7
版本的 HotSpot 虛擬機
中,方法區被稱為永久代(Permanent Space)
,而在 JDK 1.8
中則被稱之為 MetaSpace
拓展點
可以看到常量池其實是存放在方法區中的,但《Java 虛擬機規范》將常量池和方法區放在同一個等級上
雖然《Java虛擬機規范》把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是與Java堆區分開來。
對於習慣在HotSpot虛擬機上開發和部署程序的開發者來說,很多人願意把方法區稱為“永久代”(Permanent Generation),本質上兩者並不等價,僅僅是因為HotSpot虛擬機的設計團隊選擇把GC分代收集擴展至方法區,或者說使用永久代來實現方法區而已。
程序計數器(Program Counter Register)
每個Java虛擬機線程都有自己私有獨立
的 pc(程序計數器)寄存器。
它的作用可以看做是當前線程所執行的字節碼的行號指示器。
在任何時候,每個Java虛擬機線程都在執行單個方法的代碼
,即該線程的當前方法。如果不是該方法 native,則pc寄存器包含當前正在執行的Java虛擬機指令的地址。如果線程當前正在執行native
方法,則Java虛擬機pc 寄存器的值為undefined
。
此內存區域是唯一一個在Java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域
JVM棧(Java Virtual Machine Stacks)
與程序計數器一樣,Java虛擬機棧(Java Virtual Machine Stack
)也是線程私有的,它的生命周期與線程相同,與線程同時創建。
JVM Stack
存儲幀(Stack Frame
) [A Java Virtual Machine stack stores frames]
JVM Stack
類似於傳統語言的Stack
,例如C語言:它保存局部變量和部分結果,並在方法調用和返回中起作用。由於除了推送和彈出幀(Frames
)之外,永遠不會直接操作Java虛擬機堆棧(Java Virtual Machine Stack),因此幀(Frames)
可以是堆(heap)
分配的。Java虛擬機堆棧的內存不需要是連續的。
虛擬機棧(JVM Stacks)
描述的是Java方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀(Stack Frame)
用於存儲局部變量表、操作數棧、動態鏈接、方法出口以及一些過程結果等信息。每一個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。
局部變量表存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)
、對象引用(reference類型
,它不等同於對象本身,根據不同的虛擬機實現,它可能是一個指向對象起始地址的引用指針,也可能指向一個代表對象的句柄或者其他與此對象相關的位置)和returnAddress類型
(指向了一條字節碼指令的地址)。
其中64位長度的long和double類型的數據會占用2個局部變量空間(Slot),其余的數據類型只占用1個。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。
在Java虛擬機規范中,對這個區域規定了兩種異常狀況:如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError
異常;如果虛擬機棧可以動態擴展(當前大部分的Java虛擬機都可動態擴展,只不過Java虛擬機規范中也允許固定長度的虛擬機棧),當擴展時無法申請到足夠的內存時會拋出OutOfMemoryError
異常。
本地方法棧(Native Method Stacks)
本地方法棧(Native Method Stacks)與虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是為虛擬機使用到的Native方法服務。虛擬機規范中對本地方法棧中的方法使用的語言、使用方式與數據結構並沒有強制規定,因此具體的虛擬機可以自由實現它。甚至有的虛擬機(譬如Sun HotSpot虛擬機
)直接就把本地方法棧和虛擬機棧合二為一。與虛擬機棧一樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError
異常。