Java內存區域(運行時數據區域)和內存模型(JMM)

Java 內存區域和內存模型是不同的東西,內存區域是指 Jvm 運行時將數據分區域存儲,強調對內存空間的劃分。java

而內存模型(Java Memory Model,簡稱 JMM )是定義了線程和主內存之間的抽象關係,即 JMM 定義了 JVM 在計算機內存(RAM)中的工做方式,若是咱們要想深刻了解Java併發編程,就要先理解好Java內存模型。程序員

Java運行時數據區域

衆所周知,Java 虛擬機有自動內存管理機制,若是出現內存泄漏和溢出方面的問題,排查錯誤就必需要了解虛擬機是怎樣使用內存的。算法

下圖是 JDK8 以後的 JVM 內存佈局。編程

圖摘自《碼出高效》

這裏再放一張 JDK8 以前得內存區域圖。數組

程序計數器

程序計數器(Program Counter Register)是一塊較小的內存空間,它能夠看做是當前線程所執行的字節碼的行號指示器。緩存

因爲 Java 虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器內核都只會執行一條線程中的指令。安全

所以,爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,咱們稱這類內存區域爲「線程私有」的內存。服務器

若是線程正在執行的是一個 Java 方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的是 Native 方法,這個計數器值則爲空(Undefined)。此內存區域是惟一一個在 Java 虛擬機規範中沒有規定任何 OutOfMemoryError 狀況的區域。數據結構

Java虛擬機棧

與程序計數器同樣,Java 虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命週期與線程相同。多線程

虛擬機棧描述的是 Java 方法執行的內存模型:每一個方法在執行的同時都會建立一個棧幀(Stack Frame,是方法運行時的基礎數據結構)用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。

在活動線程中,只有位千棧頂的幀纔是有效的,稱爲當前棧幀。正在執行的方法稱爲當前方法,棧幀是方法運行的基本結構。在執行引擎運行時,全部指令都只能針對當前棧幀進行操做。

操做棧的壓棧與出棧-《碼出高效》

1. 局部變量表

局部變量表是存放方法參數和局部變量的區域。 局部變量沒有準備階段, 必須顯式初始化。若是是非靜態方法,則在 index[0] 位置上存儲的是方法所屬對象的實例引用,一個引用變量佔 4 個字節,隨後存儲的是參數和局部變量。字節碼指令中的 STORE 指令就是將操做棧中計算完成的局部變呈寫回局部變量表的存儲空間內。

虛擬機棧規定了兩種異常情況:若是線程請求的棧深度大於虛擬機所容許的深度,將拋出 StackOverflowError 異常;若是虛擬機棧能夠動態擴展(當前大部分的 Java 虛擬機均可動態擴展),若是擴展時沒法申請到足夠的內存,就會拋出 OutOfMemoryError 異常。

2. 操做棧

操做棧是個初始狀態爲空的桶式結構棧。在方法執行過程當中, 會有各類指令往
棧中寫入和提取信息。JVM 的執行引擎是基於棧的執行引擎, 其中的棧指的就是操
做棧。字節碼指令集的定義都是基於棧類型的,棧的深度在方法元信息的 stack 屬性中。

i++ 和 ++i 的區別:

  1. i++:從局部變量表取出 i 並壓入操做棧,而後對局部變量表中的 i 自增 1,將操做棧棧頂值取出使用,最後,使用棧頂值更新局部變量表,如此線程從操做棧讀到的是自增以前的值。
  2. ++i:先對局部變量表的 i 自增 1,而後取出並壓入操做棧,再將操做棧棧頂值取出使用,最後,使用棧頂值更新局部變量表,線程從操做棧讀到的是自增以後的值。

以前之因此說 i++ 不是原子操做,即便使用 volatile 修飾也不是線程安全,就是由於,可能 i 被從局部變量表(內存)取出,壓入操做棧(寄存器),操做棧中自增,使用棧頂值更新局部變量表(寄存器更新寫入內存),其中分爲 3 步,volatile 保證可見性,保證每次從局部變量表讀取的都是最新的值,但可能這 3 步可能被另外一個線程的 3 步打斷,產生數據互相覆蓋問題,從而致使 i 的值比預期的小。

3. 動態連接

每一個棧幀中包含一個在常量池中對當前方法的引用, 目的是支持方法調用過程的動態鏈接。

4.方法返回地址

方法執行時有兩種退出狀況:

  1. 正常退出,即正常執行到任何方法的返回字節碼指令,如 RETURN、IRETURN、ARETURN 等;
  2. 異常退出。

不管何種退出狀況,都將返回至方法當前被調用的位置。方法退出的過程至關於彈出當前棧幀,退出可能有三種方式:

  1. 返回值壓入上層調用棧幀。
  2. 異常信息拋給可以處理的棧幀。
  3. PC計數器指向方法調用後的下一條指令。

本地方法棧

本地方法棧(Native Method Stack)與虛擬機棧所發揮的做用是很是類似的,它們之間的區別不過是虛擬機棧爲虛擬機執行 Java 方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的 Native 方法服務。Sun HotSpot 虛擬機直接就把本地方法棧和虛擬機棧合二爲一。與虛擬機棧同樣,本地方法棧區域也會拋出 StackOverflowError 和 OutOfMemoryError 異常。

線程開始調用本地方法時,會進入 個再也不受 JVM 約束的世界。本地方法能夠經過 JNI(Java Native Interface)來訪問虛擬機運行時的數據區,甚至能夠調用寄存器,具備和 JVM 相同的能力和權限。 當大量本地方法出現時,勢必會削弱 JVM 對系統的控制力,由於它的出錯信息都比較黑盒。對內存不足的狀況,本地方法棧仍是會拋出 nativeheapOutOfMemory。

JNI 類本地方法最著名的應該是 System.currentTimeMillis() ,JNI使 Java 深度使用操做系統的特性功能,複用非 Java 代碼。 可是在項目過程當中, 若是大量使用其餘語言來實現 JNI , 就會喪失跨平臺特性。

Java堆

對於大多數應用來講,Java 堆(Java Heap)是 Java 虛擬機所管理的內存中最大的一塊。Java 堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存。

堆是垃圾收集器管理的主要區域,所以不少時候也被稱作「GC堆」(Garbage Collected Heap)。從內存回收的角度來看,因爲如今收集器基本都採用分代收集算法,因此 Java 堆中還能夠細分爲:新生代和老年代;再細緻一點的有 Eden 空間、From Survivor 空間、To Survivor 空間等。從內存分配的角度來看,線程共享的 Java 堆中可能劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer,TLAB)。

Java 堆能夠處於物理上不連續的內存空間中,只要邏輯上是連續的便可,當前主流的虛擬機都是按照可擴展來實現的(經過 -Xmx 和 -Xms 控制)。若是在堆中沒有內存完成實例分配,而且堆也沒法再擴展時,將會拋出 OutOfMemoryError 異常。

方法區

方法區(Method Area)與 Java 堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然
Java 虛擬機規範把方法區描述爲堆的一個邏輯部分,可是它卻有一個別名叫作 Non-Heap(非堆),目的應該是與 Java 堆區分開來。

Java 虛擬機規範對方法區的限制很是寬鬆,除了和 Java 堆同樣不須要連續的內存和能夠選擇固定大小或者可擴展外,還能夠選擇不實現垃圾收集。垃圾收集行爲在這個區域是比較少出現的,其內存回收目標主要是針對常量池的回收和對類型的卸載。當方法區沒法知足內存分配需求時,將拋出 OutOfMemoryError 異常。

JDK8 以前,Hotspot 中方法區的實現是永久代(Perm),JDK8 開始使用元空間(Metaspace),之前永久代全部內容的字符串常量移至堆內存,其餘內容移至元空間,元空間直接在本地內存分配。

爲何要使用元空間取代永久代的實現?

  1. 字符串存在永久代中,容易出現性能問題和內存溢出。
  2. 類及方法的信息等比較難肯定其大小,所以對於永久代的大小指定比較困難,過小容易出現永久代溢出,太大則容易致使老年代溢出。
  3. 永久代會爲 GC 帶來沒必要要的複雜度,而且回收效率偏低。
  4. 將 HotSpot 與 JRockit 合二爲一。
運行時常量池

運行時常量池(Runtime Constant Pool)是方法區的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池中存放。

通常來講,除了保存 Class 文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中。

運行時常量池相對於 Class 文件常量池的另一個重要特徵是具有動態性,Java 語言並不要求常量必定只有編譯期才能產生,也就是並不是預置入 Class 文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的即是 String 類的 intern() 方法。

既然運行時常量池是方法區的一部分,天然受到方法區內存的限制,當常量池沒法再申請到內存時會拋出 OutOfMemoryError 異常。

直接內存

直接內存(Direct Memory)並非虛擬機運行時數據區的一部分,也不是 Java 虛擬機規範中定義的內存區域。

在 JDK 1.4 中新加入了 NIO,引入了一種基於通道(Channel)與緩衝區(Buffer)的 I/O 方式,它可使用 Native 函數庫直接分配堆外內存,而後經過一個存儲在 Java 堆中的 DirectByteBuffer 對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在 Java 堆和 Native 堆中來回複製數據。

顯然,本機直接內存的分配不會受到 Java 堆大小的限制,可是,既然是內存,確定仍是會受到本機總內存(包括 RAM 以及 SWAP 區或者分頁文件)大小以及處理器尋址空間的限制。服務器管理員在配置虛擬機參數時,會根據實際內存設置 -Xmx 等參數信息,但常常忽略直接內存,使得各個內存區域總和大於物理內存限制(包括物理的和操做系統級的限制),從而致使動態擴展時出現 OutOfMemoryError 異常。

Java線程與內存 -《碼出高效》

Java內存模型

Java內存模型是共享內存的併發模型,線程之間主要經過讀-寫共享變量(堆內存中的實例域,靜態域和數組元素)來完成隱式通訊。

Java 內存模型(JMM)控制 Java 線程之間的通訊,決定一個線程對共享變量的寫入什麼時候對另外一個線程可見。

計算機高速緩存和緩存一致性

計算機在高速的 CPU 和相對低速的存儲設備之間使用高速緩存,做爲內存和處理器之間的緩衝。將運算須要使用到的數據複製到緩存中,讓運算能快速運行,當運算結束後再從緩存同步回內存之中。

在多處理器的系統中(或者單處理器多核的系統),每一個處理器內核都有本身的高速緩存,它們有共享同一主內存(Main Memory)。

當多個處理器的運算任務都涉及同一塊主內存區域時,將可能致使各自的緩存數據不一致。

爲此,須要各個處理器訪問緩存時都遵循一些協議,在讀寫時要根據協議進行操做,來維護緩存的一致性。

圖摘自51CTO技術棧 做者 陳彩華

JVM主內存與工做內存

Java 內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量(線程共享的變量)存儲到內存和從內存中取出變量這樣底層細節。

Java內存模型中規定了全部的變量都存儲在主內存中,每條線程還有本身的工做內存,線程對變量的全部操做都必須在工做內存中進行,而不能直接讀寫主內存中的變量。

這裏的工做內存是 JMM 的一個抽象概念,也叫本地內存,其存儲了該線程以讀 / 寫共享變量的副本。

就像每一個處理器內核擁有私有的高速緩存,JMM 中每一個線程擁有私有的本地內存。

不一樣線程之間沒法直接訪問對方工做內存中的變量,線程間的通訊通常有兩種方式進行,一是經過消息傳遞,二是共享內存。Java 線程間的通訊採用的是共享內存方式,線程、主內存和工做內存的交互關係以下圖所示:

這裏所講的主內存、工做內存與 Java 內存區域中的 Java 堆、棧、方法區等並非同一個層次的內存劃分,這二者基本上是沒有關係的,若是二者必定要勉強對應起來,那從變量、主內存、工做內存的定義來看,主內存主要對應於Java堆中的對象實例數據部分,而工做內存則對應於虛擬機棧中的部分區域。

重排序和happens-before規則

在執行程序時爲了提升性能,編譯器和處理器經常會對指令作重排序。重排序分三種類型:

  1. 編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序。
  2. 指令級並行的重排序。現代處理器採用了指令級並行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行。若是不存在數據依賴性,處理器能夠改變語句對應機器指令的執行順序。
  3. 內存系統的重排序。因爲處理器使用緩存和讀 / 寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行。

從 java 源代碼到最終實際執行的指令序列,會分別經歷下面三種重排序:

JMM 屬於語言級的內存模型,它確保在不一樣的編譯器和不一樣的處理器平臺之上,經過禁止特定類型的編譯器重排序和處理器重排序,爲程序員提供一致的內存可見性保證。

java 編譯器禁止處理器重排序是經過在生成指令序列的適當位置會插入內存屏障(重排序時不能把後面的指令重排序到內存屏障以前的位置)指令來實現的。

happens-before

從 JDK5 開始,java 內存模型提出了 happens-before 的概念,經過這個概念來闡述操做之間的內存可見性。

若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必須存在 happens-before 關係。這裏提到的兩個操做既能夠是在一個線程以內,也能夠是在不一樣線程之間。

這裏的「可見性」是指當一條線程修改了這個變量的值,新值對於其餘線程來講是能夠當即得知的。

若是 A happens-before B,那麼 Java 內存模型將向程序員保證—— A 操做的結果將對 B 可見,且 A 的執行順序排在 B 以前。

重要的 happens-before 規則以下:

  1. 程序順序規則:一個線程中的每一個操做,happens- before 於該線程中的任意後續操做。
  2. 監視器鎖規則:對一個監視器鎖的解鎖,happens- before 於隨後對這個監視器鎖的加鎖。
  3. volatile 變量規則:對一個 volatile 域的寫,happens- before 於任意後續對這個 volatile 域的讀。
  4. 傳遞性:若是 A happens- before B,且 B happens- before C,那麼 A happens- before C。

下圖是 happens-before 與 JMM 的關係

圖來自簡書用戶 你聽___

volatile關鍵字

volatile 能夠說是 JVM 提供的最輕量級的同步機制,當一個變量定義爲volatile以後,它將具有兩種特性:

  1. 保證此變量對全部線程的可見性。而普通變量不能作到這一點,普通變量的值在線程間傳遞均須要經過主內存來完成。

注意,volatile 雖然保證了可見性,可是 Java 裏面的運算並不是原子操做,致使 volatile 變量的運算在併發下同樣是不安全的。而 synchronized 關鍵字則是由「一個變量在同一個時刻只容許一條線程對其進行 lock 操做」這條規則得到線程安全的。

  1. 禁止指令重排序優化。普通的變量僅僅會保證在該方法的執行過程當中全部依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操做的順序與程序代碼中的執行順序一致。

最後,推薦與感謝:
深刻理解Java虛擬機(第2版)
碼出高效:Java開發手冊
Java內存模型原理,你真的理解嗎?)
深刻理解 Java 內存模型

相關文章
相關標籤/搜索