【Java】JMM內存模型和JVM內存結構


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」又可分為:EdenFrom Survivor 0To 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異常。


參考文檔

JVM內存結構

JAVA的內存模型及結構

JVM基礎系列第6講:Java 虛擬機內存結構

JAVA 虛擬機規范

Memory Management in the Java HotSpot Virtual Machine


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM