JMM & JVM 內存模型

JMM & JVM 內存模型

JMM

JMM 定義了 Java 虛擬機(JVM)在計算機內存(RAM)中的工作方式。JVM是整個計算機虛擬模型,所以JMM是隸屬於JVM的。從抽象的角度來看,JMM 定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(Main Memory)中,每個線程都有一個私有的本地內存(Local Memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存、寫緩衝區、寄存器以及其他的硬件和編譯器優化。

主存和緩存

大家都知道,計算機在執行程序時,每條指令都是在CPU中執行的,而執行指令過程中,勢必涉及到數據的讀取和寫入。由於程序運行過程中的臨時數據是存放在主存(物理內存)當中的,這時就存在一個問題,由於CPU執行速度很快,而從內存讀取和寫入數據的過程跟CPU執行指令的速度比起來要慢的多,因此如果任何時候對數據的操作都要通過和內存的交互來進行,會大大降低指令執行的速度,因此在CPU裏面就有了高速緩存

通常高速緩存分爲三類,讀寫速度和製造成本由高到低依次爲一級緩存、二級緩存和三級緩存。

int i;
i = i + 1;

當線程執行這個語句時,會先從主存當中讀取i的值,然後複製一份到高速緩存當中,然後CPU執行指令對i進行加1操作,然後將數據寫入高速緩存,最後將高速緩存中i最新的值刷新到主存當中。

Java 內存模型規定了所有的變量都存儲在主內存中,每條線程有自己的工作內存。這是具體是什麼意思呢,以上面的操作爲例,在多線程模型下,當同時有多個線程在對主存中的變量 i 進行操作,會從主存中讀取 i 的值複製到線程自己工作內存對應的高速緩存中,然後 CPU 從高速緩存中取到值並執行指令運算,最後將結果寫到高速緩存中,最後由高速緩存將 i 的計算結果刷新到主存中。

這樣就會可能帶來我們所喜聞樂見的併發問題,由於計算是在線程自己的工作內存中進行的,其他線程無法獲取,而計算是個耗時操作,極大可能發生多個線程操作同一個臨時緩存變量的情況,這樣再將操作後的變量刷新到主存中就會發生數據錯亂。

併發三個概念

  • 原子性

    對應關鍵字:Atomic、synchronized

    一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。

  • 可見性

    當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。

  • 有序性

    程序執行的順序按照代碼的先後順序執行。舉個簡單的例子,看下面這段代碼:

    int i = 0;              
    boolean flag = false;
    i = 1;                //語句1  
    flag = true;          //語句2
    

    上面代碼定義了一個int型變量,定義了一個boolean類型變量,然後分別對兩個變量進行賦值操作。從代碼順序上看,語句1是在語句2前面的,那麼JVM在真正執行這段代碼的時候會保證語句1一定會在語句2前面執行嗎?不一定,爲什麼呢?這裏可能會發生指令重排序(Instruction Reorder)。

    下面解釋一下什麼是指令重排序,一般來說,處理器爲了提高程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行先後順序同代碼中的順序一致,但是它會保證程序最終執行結果和代碼順序執行的結果是一致的。

    比如上面的代碼中,語句1和語句2誰先執行對最終的程序結果並沒有影響,那麼就有可能在執行過程中,語句2先執行而語句1後執行。

JVM 內存模型

主要分爲五大內存區域

程序計數器

程序計數器是一塊很小的內存空間,它是線程私有的,可以認作爲當前線程的行號指示器。

Java棧(虛擬機棧)

線程私有,就是我們平時所說的棧,棧描述的是Java方法執行的內存模型

每個方法被執行的時候都會創建一個棧幀用於存儲**局部變量表,操作棧,動態鏈接,方法出口等信息。**每一個方法被調用的過程就對應一個棧幀在虛擬機棧中從入棧到出棧的過程。

平時說的棧一般指局部變量表:

一片連續的內存空間,用來存放方法參數,以及方法內定義的局部變量,存放着編譯期間已知的數據類型(八大基本類型和對象引用(reference類型),returnAddress類型。它的最小的局部變量表空間單位爲Slot,虛擬機沒有指明Slot的大小,但在jvm中,long和double類型數據明確規定爲64位,這兩個類型佔2個Slot,其它基本類型固定佔用1個Slot。

reference類型:與基本類型不同的是它不等同本身,即使是String,內部也是char數組組成,它可能是指向一個對象起始位置指針,也可能指向一個代表對象的句柄或其他與該對象有關的位置。

需要注意的是,局部變量表所需要的內存空間在編譯期完成分配,當進入一個方法時,這個方法在棧中需要分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表大小。

Java虛擬機棧可能出現兩種類型的異常:

  1. 線程請求的棧深度大於虛擬機允許的棧深度,將拋出StackOverflowError。
  2. 虛擬機棧空間可以動態擴展,當動態擴展是無法申請到足夠的空間時,拋出OutOfMemory異常。

本地方法棧

本地方法棧是與虛擬機棧發揮的作用十分相似,區別是虛擬機棧執行的是Java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的native方法服務,可能底層調用的c或者c++,我們打開jdk安裝目錄可以看到也有很多用c編寫的文件,可能就是native方法所調用的c代碼。

對於大多數應用來說,堆是java虛擬機管理內存最大的一塊內存區域,因爲堆存放的對象是線程共享的,所以多線程的時候也需要同步機制。因此需要重點了解下。

java虛擬機規範對這塊的描述是:所有對象實例及數組都要在堆上分配內存,但隨着JIT編譯器的發展和逃逸分析技術的成熟,這個說法也不是那麼絕對,但是大多數情況都是這樣的。

注意:它是所有線程共享的,它的目的是存放對象實例。同時它也是GC所管理的主要區域,因此常被稱爲GC堆,又由於現在收集器常使用分代算法,Java堆中還可以細分爲新生代和老年代,再細緻點還有Eden(伊甸園)空間之類的不做深究。

根據虛擬機規範,Java堆可以存在物理上不連續的內存空間,就像磁盤空間只要邏輯是連續的即可。它的內存大小可以設爲固定大小,也可以擴展。

方法區

方法區同堆一樣,是所有線程共享的內存區域,爲了區分堆,又被稱爲非堆。

用於存儲已被虛擬機加載的類信息、常量、靜態變量,如static修飾的變量加載類的時候就被加載到方法區中。

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