Java 內存模型和 JVM 內存結構真不是一回事

這兩個概念估計有很多人會混淆,它們均可以說是 JVM 規範的一部分,但真不是一回事!它們描述和解決的是不一樣問題,簡單來講,程序員

  • Java 內存模型,描述的是多線程容許的行爲
  • JVM 內存結構,描述的是線程運行所設計的內存空間

JVM 是什麼呢?它屏蔽了底層架構的差別性,是 Java 跨平臺的依據,也是每一個 Java 程序員必須瞭解的一部分。編程

JVM 體系結構

Java Virtual Machine(JVM) 是一種抽象的計算機,基於堆棧架構,它有本身的指令集和內存管理。它加載 class 文件,分析、解釋並執行字節碼。基本結構以下:數組

JVM Architecture

如上圖所示,JVM 主要分爲三個子系統:類加載器運行時數據區執行引擎緩存

類加載器子系統

它主要功能是處理類的動態加載,還有連接,而且在第一次引用類時進行初始化數據結構

Loading - 加載,顧名思義,用於加載類,它有三種類加載器,根據雙親委託模型,從不一樣路徑進行加載:多線程

  • Bootstrap ClassLoader - 加載 rt.jar 核心類庫,是優先級最高的加載器
  • Extension ClassLoader - 負責加載 jre\lib\ext 文件夾中的類
  • Application ClassLoader -負責加載 CLASSPATH 指定的類庫

Linking - 連接,動態連接到運行時所需的資源,分爲三步:架構

  • Verify - 驗證:驗證生成的字節碼是否正確
  • Prepare - 準備:爲全部靜態變量,分配內存並賦予默認值
  • Resolve - 解析:將 class 文件常量池中全部對內存的符號引用,替換成到方法區的直接引用

Initialization - 類初始化,類加載的最後階段,這裏對靜態變量進行賦值,並執行靜態塊。(注意區分對象初始化併發

運行時數據區

它約定了在運行時程序代碼的數據好比變量、參數等等的存儲位置,主要包含如下幾部分:app

  • PC 寄存器(程序計數器):保存正在執行的字節碼指令的地址
  • :在方法調用時,建立一個叫棧幀的數據結構,用於存儲局部變量和部分過程的結果,棧幀由如下幾部分組成:
    • 局部變量表:存儲方法調用時傳遞的參數,從0開始存儲this、方法參數、局部變量
    • 操做數棧:執行中間操做,存儲從局部變量表或對象實例字段複製的常量或變量值,以及操做結果,另外,還用來準備被調用方法的參數和接受方法調用的返回結果
    • 動態連接:一個指向運行時常量池的引用,將 class 文件中的符號引用(描述一個方法調用了其餘方法或訪問成員變量)轉爲直接引用
    • 方法返回地址:方法正常退出或拋出異常退出,返回方法被調用的位置
  • :存儲類實例對象和數組對象,垃圾回收的主要區域
  • 方法區:也被稱爲元空間,還有個別名 non-heap(非堆),使用本地內存存儲 class meta-data 元數據(運行時常量池,字段和方法的數據,構造函數和方法的字節碼等),在 JDK 8 中,把 interned String 和類靜態變量移動到了 Java 堆
  • 運行時常量池:存儲類或接口中的數值字面量字符串字面量以及全部方法或字段的引用,基本上涉及到方法或字段,JVM 就會在運行時常量池中搜索其具體的內存地址
  • 本地方法棧:與 JVM 棧相似,只不過服務於 Native 方法

執行引擎

運行時數據區存儲着要執行的字節碼,執行引擎將會讀取並逐個執行。函數

Interpreter - 解釋器,它對字節碼的解釋很快,但執行慢,有個缺點是,當方法被屢次調用時,每次都須要從新解釋。

JIT Compiler- JIT編譯器, 解決了解釋器的缺點,仍使用解釋器來轉換字節代碼,但發現有代碼重複執行時,會使用 JIT 編譯器,將整個字節碼編譯成本地代碼,將本地代碼用於重複調用,從而提升系統的性能,有如下幾部分組成:

  • 中間代碼生成器 - 生成中間代碼
  • 代碼優化器 - 負責優化上面生成的中間代碼
  • 目標代碼生成器 - 負責生成機器代碼或本地代碼
  • Profiler - 一個特殊組件,負責查找熱點,判斷該方法是否被屢次調用

Garbage Collector- 垃圾收集器,收集和刪除未引用的對象。

另外,還包括執行引擎所需的本地庫*(Native Method Libraries)和與其交互的 JNI 接口(Java Native Interface)*。

如今來看下 Java 內存模型和 JVM 內存結構有何不一樣。

JVM 內存結構

常說的 JVM 內存結構指的就是上文提交到運行時數據區,其中方法區線程共享程序計數器運行時常量池線程獨享

它描述的是,在運行時,字節碼和代碼數據存儲的位置。

內存模型

先拋開 Java 不說,先來看下內存模型是什麼?維基百科中的定義:

In computing, a memory model describes the interactions of threads through memory and their shared use of the data.

意思就是,在計算中,內存模型描述了多線程如何正確的經過內存進行交互和使用共享數據。換句話說,內存模型約束了處理器對內存的讀寫。

cpu-memory.jpg

CPU 和內存之間一般會存在一層或多層高速緩存,這對單處理器可能沒問題,但在多處理器系統中,可能就會出現緩存一致性問題,也就是當兩個處理器(線程)同時讀取相同內存位置會發生什麼?什麼狀況下會看到相同的值?

緩存一致性問題,在併發編程中,又被稱做可見性問題。內存模型處理器級別,爲處理器彼此之間對內存寫入結果的可見性,定義了充分必要條件:

  • 強內存模型,通常說的是順序一致性,全部內存操做存在一個全序關係,每一個操做都是原子的且當即對全部處理器可見
  • 弱內存模型,不限制處理器的內存操做順序,而使用特殊指令刷新或者使本地緩存失效,以便看到其餘處理器的寫入,或使此處理器的寫入對其餘處理器可見,這些特殊指令被稱爲內存屏障

大多數處理器不會限制內存操做的順序,多線程在執行時可能會出現讓人困惑和違背直覺的結果。這是由於 CPU 爲了充分利用不一樣類型存儲器(寄存器、高速緩存、主存)的總線帶寬,會將內存操做從新排序,以無序執行,這個動做稱爲內存排序指令重排序

重排序,也被稱爲編譯器優化和處理器優化,由於它既能夠發生在編譯期間,也能夠發生在 CPU 運行時。爲了保證多線程的有序性,須要使用內存屏障禁止重排序

因此說,內存模型就是在硬件層面描述了使用內存屏障(刷新緩存或禁用指令重排序)解決多線程編程中的可見性有序性的問題。

Java 內存模型

Java 內存模型(下文簡稱 JMM)就是在底層處理器內存模型的基礎上,定義本身的多線程語義。它明確指定了一組排序規則,來保證線程間的可見性。

這一組規則被稱爲 Happens-Before, JMM 規定,要想保證 B 操做可以看到 A 操做的結果(不管它們是否在同一個線程),那麼 A 和 B 之間必須知足 Happens-Before 關係

  • 單線程規則:一個線程中的每一個動做都 happens-before 該線程中後續的每一個動做
  • 監視器鎖定規則:監聽器的解鎖動做 happens-before 後續對這個監聽器的鎖定動做
  • volatile 變量規則:對 volatile 字段的寫入動做 happens-before 後續對這個字段的每一個讀取動做
  • 線程 start 規則:線程 start() 方法的執行 happens-before 一個啓動線程內的任意動做
  • 線程 join 規則:一個線程內的全部動做 happens-before 任意其餘線程在該線程 join() 成功返回以前
  • 傳遞性:若是 A happens-before B, 且 B happens-before C, 那麼 A happens-before C

怎麼理解 happens-before 呢?若是按字面意思,好比第二個規則,線程(不論是不是同一個)的解鎖動做發生在鎖定以前?這明顯不對。happens-before 也是爲了保證可見性,好比那個解鎖和加鎖的動做,能夠這樣理解,線程1釋放鎖退出同步塊,線程2加鎖進入同步塊,那麼線程2就能看見線程1對共享對象修改的結果。

Java 提供了幾種語言結構,包括 volatile, finalsynchronized, 它們旨在幫助程序員向編譯器描述程序的併發要求,其中:

  • volatile - 保證可見性有序性
  • synchronized - 保證可見性有序性; 經過管程(Monitor)保證一組動做的原子性
  • final - 經過禁止在構造函數初始化給 final 字段賦值這兩個動做的重排序,保證可見性(若是 this 引用逃逸就很差說可見性了)

編譯器在遇到這些關鍵字時,會插入相應的內存屏障,保證語義的正確性。

有一點須要注意的是,synchronized 不保證同步塊內的代碼禁止重排序,由於它經過鎖保證同一時刻只有一個線程訪問同步塊(或臨界區),也就是說同步塊的代碼只需知足 as-if-serial 語義 - 只要單線程的執行結果不改變,能夠進行重排序。

因此說,Java 內存模型描述的是多線程對共享內存修改後彼此之間的可見性,另外,還確保正確同步的 Java 代碼能夠在不一樣體系結構的處理器上正確運行。

小結

它們之間的關係能夠這樣來個總結,實現一個 JVM 要知足內存結構描述的組成部分,設計如何執行多個線程的時候,要知足Java 內存模型約定的多線程語義。

搜索公衆號「頓悟源碼」獲取更多源碼分析和造的輪子。

相關文章
相關標籤/搜索