最近和幾個之前一起做安卓的朋友喝酒,他最近在研究JVM,我們就簡單的討論了起來,他比我研究的深很多,我也不甘墮落,自己也開始研究了一下,寫了4篇文章整理了一下自己的思路,Java虛擬機整體篇幅如下:
本片文章內容如下:
- 1、硬件的效率與一致性
- 2、Java內存模型
- 3、重排序
- 4、JVM體系結構簡介
多任務和高并發(fā)是衡量一臺計算機處理器的能力重要指標之一。一般衡量一個服務器性能的高低好壞,使用每秒事務處理數(shù)(Transactions Per Second,TPS)這個指標比較能說明問題,它代表著一秒內服務器平均能響應的請求數(shù),而TPS值與程序并發(fā)能力有著非常密切的關系。在討論Java內存模型和線程之前,先簡單介紹一下硬件的效率與一致性。
一、硬件的效率與一致性
由于計算機的存儲設備與處理器的運算能力之間有幾個數(shù)量級的差距。所以現(xiàn)代計算機系統(tǒng)都不得不加入一層讀寫速度盡可能接近處理器運算速度的高速緩存(cache)來作為內存與處理器之間的緩沖:將運算需要使用到的數(shù)據(jù)復制到緩存中,讓運算能快速進行,當勻速結束后再從緩存同步回內存之中,這樣處理器就無需等待緩慢的內存讀寫了。
基于告訴緩存的存儲交互很好地解決了處理器與內存的速度矛盾,但是引入了一個新的問題:緩存一致性(Cache Coherence)。在多處理器系統(tǒng)中,每個處理器都有自己的告訴緩存,而他們又共享統(tǒng)一主存,如下圖所示:多個處理器運算任務都涉及同一塊主存,需要一種協(xié)議來保障數(shù)據(jù)的一致性,這類協(xié)議有MSI、MESI、MOSI及Dragon Protocol等。Java虛擬機內存模型中定義的內存訪問操作與硬件的緩存訪問操作是具有可比性的。

除此之外,為了使得處理內部的運算單元盡可能的被充分利用,處理可能會對出入代碼進行亂序執(zhí)行(Out-Of-Order Execution)優(yōu)化,處理器會在計算之后將對亂序執(zhí)行的代碼進行結果重組,保證結果準確性。與處理器的亂序執(zhí)行優(yōu)化類似,Java虛擬機的即使編譯器(JIT)中也有類似的指令重排序(Instruction Recorder)優(yōu)化。
二、Java內存模型
Java內存模型的主要目標是定義程序中各個變量的訪問規(guī)則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣底層細節(jié)。此處的變量與Java編程時所說的變量不一樣,指包括了實例字段、靜態(tài)姿態(tài)和構成數(shù)組對象的元素,但是不包括局部變量與方法參數(shù),后者是線程私有的,不會被共享。
Java內存模型中規(guī)定了所有的變量都存儲在主內存中,每條線程還有自己的工作內存(可以與前面的處理器高速緩存類比),線程的工作內存中保存了該線程使用到的變量到主內存副本拷貝,線程對變量的所有操作(讀取、賦值)都必須在工作內存中進行,而不能直接讀寫主內存中的變量。不同線程之間無法直接訪問對象工作內存中的變量,線程間變量值的傳遞均需要在主內存來完成,線程、主內存和工作內存的交互關系如下圖所示,和上圖很類似。

這里的主內存、工作內存與Java內存區(qū)域的Java堆、棧、方法區(qū)不是同一層次內存劃分。

對普通變量,一個線程中更新的值,不能馬上反應在其他變量中。如果需要在其他線程中立即可見,需要使用volatile關鍵字作為標識
- 1、原子性:
八種基本類型都具有原子性的。Java內存模型只保證了基本讀取和賦值是原子性操作,如果要實現(xiàn)更大范圍操作的原子性,可以通過synchronized和Lock來實現(xiàn)的。由于synchronized和Lock能夠保證任一時刻只有一個線程執(zhí)行該代碼塊,那么自然就不存在原子性問題了,從而保證了原子性。- 2、可見性:一個線程修改了變量,其他線程可以立即知道,保證可見性的方法:
- volatile
- synchronized(unlock)
- final(一旦初始化完成,其他線程就可見)
- 3、有序性:在本線程內,操作都是有序的;在線程外,操作都是無序的。在Java內存模型中,允許編譯器和處理對執(zhí)行進行重排序,但是重排序過程不會影響到單線程程序的執(zhí)行,卻影響到多線程并發(fā)執(zhí)行的正確性。在Java里面,可以通過volatile關鍵字來保證一定的"有序性"。另外可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個線程來執(zhí)行同步代碼,相當于讓線程順序執(zhí)行同步代碼,自然就保證了有序性。對于volatile,JMM(Java Memory Model)內存屏障插入策略:
- 在每個volatile寫操作的前面插入一個StoreStroe屏障
- 在每個volatile寫操作的后面插入一個StroeLoad屏障
- 在每個volatile讀操作的后面插入一個LoadLoad屏障
- 在每個volatile讀操作的后面插入一個LoadStore屏障
內存間交互操作:
關于主內存工作內存之間的具體交互協(xié)議,即一個變量如何從主內存拷貝到工作內存、如何從工作內存同步到主內存之間的實現(xiàn)細節(jié),Java內存模型定義了以下8種操作來完成:
- lock(鎖定):作用于主內存的變量,把一個變量標識為一條線程獨占狀態(tài)。
- unlock(解鎖):作用于主內存變量,把一個處于鎖定狀態(tài)的變量釋放出來,釋放后的編碼那個才可以被其他線程鎖定
- read(讀取):作用于主內存變量,把一個變量的值從主內存?zhèn)鬏數(shù)骄€程的工作內存中,以便隨后的load動作使用
- load(載入):作用于工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。
- use(使用):作用于工作內存的變量,把工作內存中的一個變量值傳遞給執(zhí)行引擎,每當虛擬機遇到一個需要使用變量的值的字節(jié)碼指令時將會執(zhí)行這個操作
- assign(賦值):作用于工作內存的變量,它把一個從執(zhí)行引擎接收到的值賦值給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節(jié)碼執(zhí)行令時執(zhí)行這個操作。
- store(存儲):作用于工作內存的變量,把工作內存中的一個變量的值傳送到主內存中,以便隨后的write的操作。
- write(寫入):作用于主內存的變量,它把store操作從工作內存中的一個變量的值傳送到主內存的變量中。

如果把一個變量從主內存中復制到工作內存,就需要按順序地執(zhí)行read和load操作,如果把變量從工作內存中同步回主內存,就要按順序地執(zhí)行store和write操作。每一個操作都是原子的,即執(zhí)行期間不會被中斷。Java內存模型只要求上述操作必須按順序執(zhí)行,而沒有保證必須是連續(xù)執(zhí)行。也就是read和load之間,store和write之間是可以插入其他指令的,如對內存中的變量啊a、b進行訪問,可能的順序是read a,read b,load b,load a。Java內存模型還規(guī)定了在執(zhí)行上述八種基本操作時,必須滿足如下規(guī)則:
- read和load、store和write操作之一單獨出現(xiàn)
- 不允許一個線程丟棄它的最近assign的操作,即變量在工作內存中改變了之后必須同步到主內存中。
- 不允許一個線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從工作內存同步回主內存。
- 一個新的變量只能在主內存中誕生,不允許在工作內存中直接使用一個未被初始化(load或assign)的變量。即就是對一個變量實施use和store操作之前,必須先執(zhí)行過了assign和load操作。
- 一個變量在同一時刻只允許一條線程對其進行l(wèi)ock操作,lock和unlock必須成對出現(xiàn)。
- 如果對一個變量執(zhí)行l(wèi)ock操作,將會清空工作內存中此變量的值,在執(zhí)行引擎使用這個變量錢需要重新執(zhí)行l(wèi)oad或assign操作初始化量的值。
- 如果一個變量事先沒有被lock操作鎖定,則不允許對它執(zhí)行unlock操作;也不允許去unlock一個被其他線程鎖定的變量。
- 對一個變量執(zhí)行unlock操作之前,必須先把此變量同步到主內存中(執(zhí)行store和write操作)。
三、重排序
在執(zhí)行程序時為了提高性能,編譯器和處理器經常會對指令進行重排序。重排序分成三種類型:
- 編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義放入前提下,可以重新安排語句的執(zhí)行順序。
- 指令級并行的重排序?,F(xiàn)代處理器采用了指令級并行技術來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對應機器指令的執(zhí)行順序。
- 內存系統(tǒng)的重排序。由于處理器使用緩存和讀寫緩沖區(qū),這使得加載和存儲操作看上去可能是在亂序執(zhí)行。
從Java源代碼到最終實際執(zhí)行的指令序列,會經過下面三種重排序:

為了保證內存的可見性,Java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序。
四、JVM體系結構簡介
為了展示虛擬機進程和.class文件的關系,特意畫了下面一張圖:

通過上面這幅圖片,我們編譯后的.class文件是作為Java虛擬機的原料被輸入到Java虛擬機內部的,那么具體由誰來做這一部分工作呢?其實在Java虛擬機內部,有一個叫做類加載的子系統(tǒng),這個子系統(tǒng)用來在運行時根據(jù)需要加載類。注意上面一句話中的"根據(jù)需要"4個字。在Java虛擬機執(zhí)行過程中,只有他需要一個類的時候,才會調用類加載器來加載這個類,并不會再開始運行時加載所有類。就像一個人,只有餓的時候才去吃飯,而不是一次把一年的飯都吃到肚子里。一般來說,虛擬機加載類的時機,在第一次使用一個新的類的時候。我們將會在Java虛擬機基礎——3類加載機制中詳細講解。
由于虛擬機加載的類,被加載到Java虛擬機內存中之后,虛擬機會讀取并執(zhí)行它里面存在的字節(jié)碼指令。虛擬機中執(zhí)行字節(jié)碼指令的部分叫做執(zhí)行引擎。就像一個人,不把飯吃下去就完事了,還要進行消化,執(zhí)行引擎就相當于一個人的腸胃系統(tǒng)。在執(zhí)行的過程中,還會把各個class文件動態(tài)的連接起來。關于執(zhí)行引擎的具體行為和動態(tài)鏈接相關的內容也會在Java虛擬機基礎——3類加載機制中詳細講解。
我們知道,Java虛擬機會進行自動內存管理。具體來說就是自動釋放沒有用的對象,而不需要程序員編寫代碼來釋放分配的內存。這部分工作由垃圾收集子系統(tǒng)負責。從上面的論述可以知道,一個Java虛擬機實例在運行過程中有3個子系統(tǒng)來保障它的正常運行,分別是:
- 類加載系統(tǒng)
- 執(zhí)行引擎系統(tǒng)
- 垃圾回收系統(tǒng)
如下圖:

虛擬機的運行,必須加載.class文件,并且執(zhí)行class文件中的字節(jié)碼指令。它做這么多事情,必須需要自己的空間。就像人吃下去的東西首先要放到胃里。虛擬機也需要空間來存放這個數(shù)據(jù)。首先,加載字節(jié)碼,需要一個單獨的內存空間來存放;一個線程的執(zhí)行,也需要內存空間來維護方法的調用關系,存放方法中的數(shù)據(jù)和中間計算結果;在執(zhí)行的過程中,無法避免的要創(chuàng)建對象,創(chuàng)建的對象需要一個專門的內存空間來存放。關于虛擬機運行時區(qū)域的內容,我們將在Java虛擬機基礎——2JVM運行時數(shù)據(jù)區(qū)中詳細講解。
大家喜歡就點贊,您的每一次點贊,都是我努力和進步的動力!您可能想不到:您的小小一按,可能就會對另外一個人產生翻天覆地的影響。!最后謝謝您的支持與厚愛