Java 內(nèi)存區(qū)域和內(nèi)存模型是不一樣的東西,內(nèi)存區(qū)域是指 Jvm 運行時將數(shù)據(jù)分區(qū)域存儲,強調(diào)對內(nèi)存空間的劃分。
而內(nèi)存模型(Java Memory Model,簡稱 JMM )是定義了線程和主內(nèi)存之間的抽象關(guān)系,即 JMM 定義了 JVM 在計算機內(nèi)存(RAM)中的工作方式,如果我們要想深入了解Java并發(fā)編程,就要先理解好Java內(nèi)存模型。
Java運行時數(shù)據(jù)區(qū)域
眾所周知,Java 虛擬機有自動內(nèi)存管理機制,如果出現(xiàn)內(nèi)存泄漏和溢出方面的問題,排查錯誤就必須要了解虛擬機是怎樣使用內(nèi)存的。
下圖是 JDK8 之后的 JVM 內(nèi)存布局。

JDK8 之前的內(nèi)存區(qū)域圖如下:

在 HotSpot JVM 中,永久代中用于存放類和方法的元數(shù)據(jù)以及常量池,比如
Class和Method。每當(dāng)一個類初次被加載的時候,它的元數(shù)據(jù)都會放到永久代中。
永久代是有大小限制的,因此如果加載的類太多,很有可能導(dǎo)致永久代內(nèi)存溢出,即萬惡的 java.lang.OutOfMemoryError: PermGen ,為此我們不得不對虛擬機做調(diào)優(yōu)。
那么,Java 8 中 PermGen 為什么被移出 HotSpot JVM 了?我總結(jié)了兩個主要原因:
- 由于 PermGen 內(nèi)存經(jīng)常會溢出,引發(fā)惱人的 java.lang.OutOfMemoryError: PermGen,因此 JVM 的開發(fā)者希望這一塊內(nèi)存可以更靈活地被管理,不要再經(jīng)常出現(xiàn)這樣的 OOM
- 移除 PermGen 可以促進 HotSpot JVM 與 JRockit VM 的融合,因為 JRockit 沒有永久代。
根據(jù)上面的各種原因,PermGen 最終被移除,方法區(qū)移至 Metaspace,字符串常量移至 Java Heap。
程序計數(shù)器
程序計數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。
由于 Java 虛擬機的多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式來實現(xiàn)的,在任何一個確定的時刻,一個處理器內(nèi)核都只會執(zhí)行一條線程中的指令。
因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個獨立的程序計數(shù)器,各條線程之間計數(shù)器互不影響,獨立存儲,我們稱這類內(nèi)存區(qū)域為“線程私有”的內(nèi)存。
如果線程正在執(zhí)行的是一個 Java 方法,這個計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址;如果正在執(zhí)行的是 Native 方法,這個計數(shù)器值則為空(Undefined)。此內(nèi)存區(qū)域是唯一一個在 Java 虛擬機規(guī)范中沒有規(guī)定任何 OutOfMemoryError 情況的區(qū)域。
Java虛擬機棧
與程序計數(shù)器一樣,Java 虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命周期與線程相同。
虛擬機棧描述的是 Java 方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀(Stack Frame,是方法運行時的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu))用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。每一個方法從調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機棧中入棧到出棧的過程。
在活動線程中,只有位千棧頂?shù)膸攀怯行У模Q為當(dāng)前棧幀。正在執(zhí)行的方法稱為當(dāng)前方法,棧幀是方法運行的基本結(jié)構(gòu)。在執(zhí)行引擎運行時,所有指令都只能針對當(dāng)前棧幀進行操作。

1. 局部變量表
局部變量表是存放方法參數(shù)和局部變量的區(qū)域。 局部變量沒有準(zhǔn)備階段, 必須顯式初始化。如果是非靜態(tài)方法,則在 index[0] 位置上存儲的是方法所屬對象的實例引用,一個引用變量占 4 個字節(jié),隨后存儲的是參數(shù)和局部變量。字節(jié)碼指令中的 STORE 指令就是將操作棧中計算完成的局部變呈寫回局部變量表的存儲空間內(nèi)。
虛擬機棧規(guī)定了兩種異常狀況:如果線程請求的棧深度大于虛擬機所允許的深度,將拋出 StackOverflowError 異常;如果虛擬機??梢詣討B(tài)擴展(當(dāng)前大部分的 Java 虛擬機都可動態(tài)擴展),如果擴展時無法申請到足夠的內(nèi)存,就會拋出 OutOfMemoryError 異常。
2. 操作棧
操作棧是個初始狀態(tài)為空的桶式結(jié)構(gòu)棧。在方法執(zhí)行過程中, 會有各種指令往
棧中寫入和提取信息。JVM 的執(zhí)行引擎是基于棧的執(zhí)行引擎, 其中的棧指的就是操
作棧。字節(jié)碼指令集的定義都是基于棧類型的,棧的深度在方法元信息的 stack 屬性中。
i++ 和 ++i 的區(qū)別:
- i++:從局部變量表取出 i 并壓入操作棧(load memory),然后對局部變量表中的 i 自增 1(add&store memory),將操作棧棧頂值取出使用,如此線程從操作棧讀到的是自增之前的值。
- ++i:先對局部變量表的 i 自增 1(load memory&add&store memory),然后取出并壓入操作棧(load memory),再將操作棧棧頂值取出使用,線程從操作棧讀到的是自增之后的值。
之前之所以說 i++ 不是原子操作,即使使用 volatile 修飾也不是線程安全,就是因為,可能 i 被從局部變量表(內(nèi)存)取出,壓入操作棧(寄存器),操作棧中自增,使用棧頂值更新局部變量表(寄存器更新寫入內(nèi)存),其中分為 3 步,volatile 保證可見性,保證每次從局部變量表讀取的都是最新的值,但可能這 3 步可能被另一個線程的 3 步打斷,產(chǎn)生數(shù)據(jù)互相覆蓋問題,從而導(dǎo)致 i 的值比預(yù)期的小。
3. 動態(tài)鏈接
每個棧幀中包含一個在常量池中對當(dāng)前方法的引用, 目的是支持方法調(diào)用過程的動態(tài)連接。
4.方法返回地址
方法執(zhí)行時有兩種退出情況:
- 正常退出,即正常執(zhí)行到任何方法的返回字節(jié)碼指令,如 RETURN、IRETURN、ARETURN 等;
- 異常退出。
無論何種退出情況,都將返回至方法當(dāng)前被調(diào)用的位置。方法退出的過程相當(dāng)于彈出當(dāng)前棧幀,退出可能有三種方式:
- 返回值壓入上層調(diào)用棧幀。
- 異常信息拋給能夠處理的棧幀。
- PC計數(shù)器指向方法調(diào)用后的下一條指令。
本地方法棧
本地方法棧(Native Method Stack)與虛擬機棧所發(fā)揮的作用是非常相似的,它們之間的區(qū)別不過是虛擬機棧為虛擬機執(zhí)行 Java 方法(也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機使用到的 Native 方法服務(wù)。Sun HotSpot 虛擬機直接就把本地方法棧和虛擬機棧合二為一。與虛擬機棧一樣,本地方法棧區(qū)域也會拋出 StackOverflowError 和 OutOfMemoryError 異常。
線程開始調(diào)用本地方法時,會進入 個不再受 JVM 約束的世界。本地方法可以通過 JNI(Java Native Interface)來訪問虛擬機運行時的數(shù)據(jù)區(qū),甚至可以調(diào)用寄存器,具有和 JVM 相同的能力和權(quán)限。 當(dāng)大量本地方法出現(xiàn)時,勢必會削弱 JVM 對系統(tǒng)的控制力,因為它的出錯信息都比較黑盒。對內(nèi)存不足的情況,本地方法棧還是會拋出 nativeheapOutOfMemory。
JNI 類本地方法最著名的應(yīng)該是 System.currentTimeMillis() ,JNI使 Java 深度使用操作系統(tǒng)的特性功能,復(fù)用非 Java 代碼。 但是在項目過程中, 如果大量使用其他語言來實現(xiàn) JNI , 就會喪失跨平臺特性。
Java堆
對于大多數(shù)應(yīng)用來說,Java 堆(Java Heap)是 Java 虛擬機所管理的內(nèi)存中最大的一塊。Java 堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機啟動時創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內(nèi)存。
堆是垃圾收集器管理的主要區(qū)域,因此很多時候也被稱做“GC堆”(Garbage Collected Heap)。從內(nèi)存回收的角度來看,由于現(xiàn)在收集器基本都采用分代收集算法,所以 Java 堆中還可以細分為:新生代和老年代;再細致一點的有 Eden 空間、From Survivor 空間、To Survivor 空間等。從內(nèi)存分配的角度來看,線程共享的 Java 堆中可能劃分出多個線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer,TLAB)。
Java 堆可以處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可,當(dāng)前主流的虛擬機都是按照可擴展來實現(xiàn)的(通過 -Xmx 和 -Xms 控制)。如果在堆中沒有內(nèi)存完成實例分配,并且堆也無法再擴展時,將會拋出 OutOfMemoryError 異常。
方法區(qū)
方法區(qū)(Method Area)與 Java 堆一樣,是各個線程共享的內(nèi)存區(qū)域,它用于存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。雖然
Java 虛擬機規(guī)范把方法區(qū)描述為堆的一個邏輯部分,但是它卻有一個別名叫做 Non-Heap(非堆),目的應(yīng)該是與 Java 堆區(qū)分開來。
Java 虛擬機規(guī)范對方法區(qū)的限制非常寬松,除了和 Java 堆一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或者可擴展外,還可以選擇不實現(xiàn)垃圾收集。垃圾收集行為在這個區(qū)域是比較少出現(xiàn)的,其內(nèi)存回收目標(biāo)主要是針對常量池的回收和對類型的卸載。當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時,將拋出 OutOfMemoryError 異常。
JDK8 之前,Hotspot 中方法區(qū)的實現(xiàn)是永久代(Perm),JDK8 開始使用元空間(Metaspace),以前永久代所有內(nèi)容的字符串常量移至堆內(nèi)存,其他內(nèi)容移至元空間,元空間直接在本地內(nèi)存分配。
為什么要使用元空間取代永久代的實現(xiàn)?
- 字符串存在永久代中,容易出現(xiàn)性能問題和內(nèi)存溢出。
- 類及方法的信息等比較難確定其大小,因此對于永久代的大小指定比較困難,太小容易出現(xiàn)永久代溢出,太大則容易導(dǎo)致老年代溢出。
- 永久代會為 GC 帶來不必要的復(fù)雜度,并且回收效率偏低。
- 將 HotSpot 與 JRockit 合二為一。
運行時常量池
運行時常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池中存放。
一般來說,除了保存 Class 文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中。
運行時常量池相對于 Class 文件常量池的另外一個重要特征是具備動態(tài)性,Java 語言并不要求常量一定只有編譯期才能產(chǎn)生,也就是并非預(yù)置入 Class 文件中常量池的內(nèi)容才能進入方法區(qū)運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發(fā)人員利用得比較多的便是 String 類的 intern() 方法。
既然運行時常量池是方法區(qū)的一部分,自然受到方法區(qū)內(nèi)存的限制,當(dāng)常量池?zé)o法再申請到內(nèi)存時會拋出 OutOfMemoryError 異常。
直接內(nèi)存
直接內(nèi)存(Direct Memory)并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是 Java 虛擬機規(guī)范中定義的內(nèi)存區(qū)域。
在 JDK 1.4 中新加入了 NIO,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的 I/O 方式,它可以使用 Native 函數(shù)庫直接分配堆外內(nèi)存,然后通過一個存儲在 Java 堆中的 DirectByteBuffer 對象作為這塊內(nèi)存的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在 Java 堆和 Native 堆中來回復(fù)制數(shù)據(jù)。
顯然,本機直接內(nèi)存的分配不會受到 Java 堆大小的限制,但是,既然是內(nèi)存,肯定還是會受到本機總內(nèi)存(包括 RAM 以及 SWAP 區(qū)或者分頁文件)大小以及處理器尋址空間的限制。服務(wù)器管理員在配置虛擬機參數(shù)時,會根據(jù)實際內(nèi)存設(shè)置 -Xmx 等參數(shù)信息,但經(jīng)常忽略直接內(nèi)存,使得各個內(nèi)存區(qū)域總和大于物理內(nèi)存限制(包括物理的和操作系統(tǒng)級的限制),從而導(dǎo)致動態(tài)擴展時出現(xiàn) OutOfMemoryError 異常。

Java內(nèi)存模型
Java內(nèi)存模型是共享內(nèi)存的并發(fā)模型,線程之間主要通過讀-寫共享變量(堆內(nèi)存中的實例域,靜態(tài)域和數(shù)組元素)來完成隱式通信。
Java 內(nèi)存模型(JMM)控制 Java 線程之間的通信,決定一個線程對共享變量的寫入何時對另一個線程可見。
計算機高速緩存和緩存一致性
計算機在高速的 CPU 和相對低速的存儲設(shè)備之間使用高速緩存,作為內(nèi)存和處理器之間的緩沖。將運算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運算能快速運行,當(dāng)運算結(jié)束后再從緩存同步回內(nèi)存之中。
在多處理器的系統(tǒng)中(或者單處理器多核的系統(tǒng)),每個處理器內(nèi)核都有自己的高速緩存,它們有共享同一主內(nèi)存(Main Memory)。
當(dāng)多個處理器的運算任務(wù)都涉及同一塊主內(nèi)存區(qū)域時,將可能導(dǎo)致各自的緩存數(shù)據(jù)不一致。
為此,需要各個處理器訪問緩存時都遵循一些協(xié)議,在讀寫時要根據(jù)協(xié)議進行操作,來維護緩存的一致性。

JVM主內(nèi)存與工作內(nèi)存
Java 內(nèi)存模型的主要目標(biāo)是定義程序中各個變量的訪問規(guī)則,即在虛擬機中將變量(線程共享的變量)存儲到內(nèi)存和從內(nèi)存中取出變量這樣底層細節(jié)。
Java內(nèi)存模型中規(guī)定了所有的變量都存儲在主內(nèi)存中,每條線程還有自己的工作內(nèi)存,線程對變量的所有操作都必須在工作內(nèi)存中進行,而不能直接讀寫主內(nèi)存中的變量。
這里的工作內(nèi)存是 JMM 的一個抽象概念,也叫本地內(nèi)存,其存儲了該線程以讀 / 寫共享變量的副本。
就像每個處理器內(nèi)核擁有私有的高速緩存,JMM 中每個線程擁有私有的本地內(nèi)存。
不同線程之間無法直接訪問對方工作內(nèi)存中的變量,線程間的通信一般有兩種方式進行,一是通過消息傳遞,二是共享內(nèi)存。Java 線程間的通信采用的是共享內(nèi)存方式,線程、主內(nèi)存和工作內(nèi)存的交互關(guān)系如下圖所示:

這里所講的主內(nèi)存、工作內(nèi)存與 Java 內(nèi)存區(qū)域中的 Java 堆、棧、方法區(qū)等并不是同一個層次的內(nèi)存劃分,這兩者基本上是沒有關(guān)系的,如果兩者一定要勉強對應(yīng)起來,那從變量、主內(nèi)存、工作內(nèi)存的定義來看,主內(nèi)存主要對應(yīng)于Java堆中的對象實例數(shù)據(jù)部分,而工作內(nèi)存則對應(yīng)于虛擬機棧中的部分區(qū)域。
重排序和happens-before規(guī)則
在執(zhí)行程序時為了提高性能,編譯器和處理器常常會對指令做重排序。重排序分三種類型:
- 編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序。
- 指令級并行的重排序?,F(xiàn)代處理器采用了指令級并行技術(shù)(Instruction-Level Parallelism, ILP)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對應(yīng)機器指令的執(zhí)行順序。
- 內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀 / 寫緩沖區(qū),這使得加載和存儲操作看上去可能是在亂序執(zhí)行。
從 java 源代碼到最終實際執(zhí)行的指令序列,會分別經(jīng)歷下面三種重排序:

JMM 屬于語言級的內(nèi)存模型,它確保在不同的編譯器和不同的處理器平臺之上,通過禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內(nèi)存可見性保證。
java 編譯器禁止處理器重排序是通過在生成指令序列的適當(dāng)位置會插入內(nèi)存屏障(重排序時不能把后面的指令重排序到內(nèi)存屏障之前的位置)指令來實現(xiàn)的。
happens-before
從 JDK5 開始,java 內(nèi)存模型提出了 happens-before 的概念,通過這個概念來闡述操作之間的內(nèi)存可見性。
如果一個操作執(zhí)行的結(jié)果需要對另一個操作可見,那么這兩個操作之間必須存在 happens-before 關(guān)系。這里提到的兩個操作既可以是在一個線程之內(nèi),也可以是在不同線程之間。
這里的“可見性”是指當(dāng)一條線程修改了這個變量的值,新值對于其他線程來說是可以立即得知的。
如果 A happens-before B,那么 Java 內(nèi)存模型將向程序員保證—— A 操作的結(jié)果將對 B 可見,且 A 的執(zhí)行順序排在 B 之前。
重要的 happens-before 規(guī)則如下:
- 程序順序規(guī)則:一個線程中的每個操作,happens- before 于該線程中的任意后續(xù)操作。
- 監(jiān)視器鎖規(guī)則:對一個監(jiān)視器鎖的解鎖,happens- before 于隨后對這個監(jiān)視器鎖的加鎖。
- volatile 變量規(guī)則:對一個 volatile 域的寫,happens- before 于任意后續(xù)對這個 volatile 域的讀。
- 傳遞性:如果 A happens- before B,且 B happens- before C,那么 A happens- before C。
下圖是 happens-before 與 JMM 的關(guān)系

volatile關(guān)鍵字
volatile 可以說是 JVM 提供的最輕量級的同步機制,當(dāng)一個變量定義為volatile之后,它將具備兩種特性:
- 保證此變量對所有線程的可見性。而普通變量不能做到這一點,普通變量的值在線程間傳遞均需要通過主內(nèi)存來完成。
注意,volatile 雖然保證了可見性,但是 Java 里面的運算并非原子操作,導(dǎo)致 volatile 變量的運算在并發(fā)下一樣是不安全的。而 synchronized 關(guān)鍵字則是由“一個變量在同一個時刻只允許一條線程對其進行 lock 操作”這條規(guī)則獲得線程安全的。
- 禁止指令重排序優(yōu)化。普通的變量僅僅會保證在該方法的執(zhí)行過程中所有依賴賦值結(jié)果的地方都能獲取到正確的結(jié)果,而不能保證變量賦值操作的順序與程序代碼中的執(zhí)行順序一致。
最后,推薦與感謝:
深入理解Java虛擬機(第2版)
碼出高效:Java開發(fā)手冊
Java內(nèi)存模型原理,你真的理解嗎?)
深入理解 Java 內(nèi)存模型