1. 程式人生 > 其它 >編碼轉換 C++

編碼轉換 C++

Java 記憶體模型

Java 記憶體模型簡稱JMM,全名 Java Memory Model 。Java 記憶體模型規定了 JVM 應該如何使用計算機記憶體(RAM)。 廣義來講, Java 記憶體模型分為兩個部分:

  • JVM 記憶體結構
  • JMM 與執行緒規範

其中,JVM 記憶體結構是底層實現,也是我們理解和認識 JMM 的基礎。 大家熟知的堆記憶體、棧記憶體等執行時資料區的劃分就可以歸為 JVM 記憶體結構。

JVM 記憶體結構

我們先來看看 JVM 整體的記憶體概念圖:

JVM 內部使用的 Java 記憶體模型, 在邏輯上將記憶體劃分為執行緒棧(thread stacks)和堆記憶體

(heap)兩個部分。 如下圖所示:

JVM 中,每個正在執行的執行緒,都有自己的執行緒棧。 執行緒棧包含了當前正在執行的方法鏈/呼叫鏈上的所有方法的狀態資訊。

所以執行緒棧又被稱為“方法棧”或“呼叫棧”(call stack)。執行緒在執行程式碼時,呼叫棧中的資訊會一直在變化。

執行緒棧裡面儲存了呼叫鏈上正在執行的所有方法中的區域性變數。

  • 每個執行緒都只能訪問自己的執行緒棧。
  • 每個執行緒都不能訪問(看不見)其他執行緒的區域性變數。

即使兩個執行緒正在執行完全相同的程式碼,但每個執行緒都會在自己的執行緒棧內建立對應程式碼中宣告的區域性變數。 所以每個執行緒都有一份自己的區域性變數副本。

  • 所有原生型別的區域性變數都儲存線上程棧中,因此對其他執行緒是不可見的。
  • 執行緒可以將一個原生變數值的副本傳給另一個執行緒,但不能共享原生區域性變數本身。
  • 堆記憶體中包含了 Java 程式碼中建立的所有物件,不管是哪個執行緒建立的。 其中也涵蓋了包裝型別(例如Byte,Integer,Long等)。
  • 不管是建立一個物件並將其賦值給區域性變數, 還是賦值給另一個物件的成員變數, 建立的物件都會被儲存到堆記憶體中。

下圖演示了執行緒棧上的呼叫棧和區域性變數,以及儲存在堆記憶體中的物件:

  • 如果是原生資料型別的區域性變數,那麼它的內容就全部保留線上程棧上。
  • 如果是物件引用,則棧中的區域性變數槽位中儲存著物件的引用地址,而實際的物件內容儲存在堆中。
  • 物件的成員變數與物件本身一起儲存在堆上, 不管成員變數的型別是原生數值,還是物件引用。
  • 類的靜態變數則和類定義一樣都儲存在堆中。

總結一下:原始資料型別和物件引用地址在棧上;物件、物件成員與類定義、靜態變數在堆上。

堆記憶體又稱為“共享堆”,堆中的所有物件,可以被所有執行緒訪問, 只要他們能拿到物件的引用地址。

  • 如果一個執行緒可以訪問某個物件時,也就可以訪問該物件的成員變數。
  • 如果兩個執行緒同時呼叫某個物件的同一方法,則它們都可以訪問到這個物件的成員變數,但每個執行緒的區域性變數副本是獨立的。

示意圖如下所示:

總結一下:雖然各個執行緒自己使用的區域性變數都在自己的棧上,但是大家可以共享堆上的物件,特別地各個不同執行緒訪問同一個物件例項的基礎型別的成員變數,會給每個執行緒一個變數的副本。

棧記憶體的結構

每啟動一個執行緒,JVM 就會在棧空間棧分配對應的執行緒棧, 比如 1MB 的空間(-Xss1m)。

執行緒棧也叫做 Java 方法棧。 如果使用了 JNI 方法,則會分配一個單獨的本地方法棧(Native Stack)。

執行緒執行過程中,一般會有多個方法組成呼叫棧(Stack Trace), 比如 A 呼叫 B,B 呼叫 C……每執行到一個方法,就會建立對應的棧幀(Frame)。

棧幀是一個邏輯上的概念,具體的大小在一個方法編寫完成後基本上就能確定。

比如返回值需要有一個空間存放吧,每個區域性變數都需要對應的地址空間,此外還有給指令使用的運算元棧,以及 class 指標(標識這個棧幀對應的是哪個類的方法, 指向非堆裡面的 Class 物件)。

堆記憶體的結構

堆記憶體是所有執行緒共用的記憶體空間,理論上大家都可以訪問裡面的內容。

但 JVM 的具體實現一般會有各種優化。比如將邏輯上的 Java 堆,劃分為堆(Heap)和非堆(Non-Heap)兩個部分.。這種劃分的依據在於,我們編寫的 Java 程式碼,基本上只能使用 Heap 這部分空間,發生記憶體分配和回收的主要區域也在這部分,所以有一種說法,這裡的 Heap 也叫 GC 管理的堆(GC Heap)。

GC 理論中有一個重要的思想,叫做分代。 經過研究發現,程式中分配的物件,要麼用過就扔,要麼就能存活很久很久。

因此,JVM 將 Heap 記憶體分為年輕代(Young generation)和老年代(Old generation, 也叫 Tenured)兩部分。

年輕代還劃分為 3 個記憶體池,新生代(Eden space)和存活區(Survivor space), 在大部分 GC 演算法中有 2 個存活區(S0, S1),在我們可以觀察到的任何時刻,S0 和 S1 總有一個是空的, 但一般較小,也不浪費多少空間。

具體實現對新生代還有優化,那就是 TLAB(Thread Local Allocation Buffer), 給每個執行緒先劃定一小片空間,你建立的物件先在這裡分配,滿了再換。這能極大降低併發資源鎖定的開銷。

Non-Heap 本質上還是 Heap,只是一般不歸 GC 管理,裡面劃分為 3 個記憶體池。

  • Metaspace, 以前叫持久代(永久代, Permanent generation), Java8 換了個名字叫 Metaspace. Java8 將方法區移動到了 Meta 區裡面,而方法又是class的一部分和 CCS 交叉了?
  • CCS, Compressed Class Space, 存放 class 資訊的,和 Metaspace 有交叉。
  • Code Cache, 存放 JIT 編譯器編譯後的本地機器程式碼。

JVM 的記憶體結構大致如此。

指令重排序

計算機按支援的指令大致可以分為兩類:

  • 精簡指令集計算機(RISC), 代表是如今大家熟知的 ARM 晶片,功耗低,運算能力相對較弱。
  • 複雜指令集計算機(CISC), 代表作是 Intel 的 X86 晶片系列,比如奔騰,酷睿,至強,以及 AMD 的 CPU。特點是效能強勁,功耗高。(實際上從奔騰 4 架構開始,對外是複雜指令集,內部實現則是精簡指令集,所以主頻才能大幅度提高)

不管哪一種指令集,CPU 的實現都是採用流水線的方式。如果 CPU 一條指令一條指令地執行,那麼很多流水線實際上是閒置的。於是硬體設計人員就想出了一個好辦法: “指令亂序”。 CPU 完全可以根據需要,通過內部排程把這些指令打亂了執行,充分利用流水線資源,只要最終結果是等價的,那麼程式的正確性就沒有問題。但這在如今多 CPU 核心的時代,隨著複雜度的提升,併發執行的程式面臨了很多問題。

記憶體屏障簡介

前面提到了CPU會在合適的時機,按需要對將要進行的操作重新排序,但是有時候這個重排機會導致我們的程式碼跟預期不一致。

怎麼辦呢?JMM 引入了記憶體屏障機制。

記憶體屏障(Memory Barrier)與記憶體柵欄(Memory Fence)是同一個概念,不同的叫法。

通過volatile標記,可以解決編譯器層面的可見性與重排序問題。而記憶體屏障則解決了硬體層面的可見性與重排序問題。

先簡單瞭解兩個指令:

  • Store:將處理器快取的資料重新整理到記憶體中。
  • Load:將記憶體儲存的資料拷貝到處理器的快取中。

記憶體屏障可分為讀屏障和寫屏障,用於控制可見性。 常見的記憶體屏障包括:

LoadLoad StoreStore LoadStore StoreLoad

屏障型別指令示例說明
LoadLoad Barriers Load1;LoadLoad;Load2 該屏障確保Load1資料的裝載先於Load2及其後所有裝載指令的的操作
StoreStore Barriers Store1;StoreStore;Store2 該屏障確保Store1立刻重新整理資料到記憶體(使其對其他處理器可見)的操作先於Store2及其後所有儲存指令的操作
LoadStore Barriers Load1;LoadStore;Store2 確保Load1的資料裝載先於Store2及其後所有的儲存指令重新整理資料到記憶體的操作
StoreLoad Barriers Store1;StoreLoad;Load2 該屏障確保Store1立刻重新整理資料到記憶體的操作先於Load2及其後所有裝載裝載指令的操作。它會使該屏障之前的所有記憶體訪問指令(儲存指令和訪問指令)完成之後,才執行該屏障之後的記憶體訪問指令

這些屏障的主要目的,是用來短暫遮蔽 CPU 的指令重排序功能。 和 CPU 約定好,看見這些指令時,就要保證這個指令前後的相應操作不會被打亂。

  • 比如看見LoadLoad, 那麼屏障前面的 Load 指令就一定要先執行完,才能執行屏障後面的 Load 指令。
  • 比如我要先把 a 值寫到 A 欄位中,然後再將 b 值寫到 B 欄位對應的記憶體地址。如果要嚴格保障這個順序,那麼就可以在這兩個 Store 指令之間加入一個 StoreStore屏障。
  • 遇到LoadStore屏障時, CPU 自廢武功,短暫遮蔽掉指令重排序功能。
  • StoreLoad屏障, 能確保屏障之前執行的所有 store 操作,都對其他處理器可見; 在屏障後面執行的 load 指令, 都能取得到最新的值。換句話說, 有效阻止屏障之前的 store 指令,與屏障之後的 load 指令亂序 、即使是多核心處理器,在執行這些操作時的順序也是一致的。

代價最高的是 StoreLoad屏障, 它同時具有其他幾類屏障的效果,可以用來代替另外三種記憶體屏障。

如何理解呢?

就是隻要有一個 CPU 核心收到這類指令,就會做一些操作,同時發出一條廣播, 給某個記憶體地址打個標記,其他 CPU 核心與自己的快取互動時,就知道這個快取不是最新的,需要從主記憶體重新進行載入處理。