JVM之棧、堆、方法區(三)

童話述說我的結局發表於2022-01-31

一、CPU和記憶體的互動

今天除夕,祝大家新年快樂,其實,我們知道的,我們的CPU跟記憶體會有非常頻繁的互動,因為如果這個頻繁的互動是交給我們的磁碟的話,那麼隨著我們的CPU運轉速度越來越快,那麼我們的磁碟的讀寫效能遠遠跟不上我們的CPU讀寫的速度,哪怕是我們現在的SSD,固態硬碟,也僅僅只是減少了我們的尋道時間以及加快了我們的找資料的時間。所以,我們才會在我們磁碟的基礎上設計了記憶體,用來解決我們的單次IO時間過長導致我們CPU的等待成本過大的問題.但是隨著我們CPU的發展,我們CPU的效能越來越高,哪怕就算是我們的記憶體的讀寫速度都跟不上我們的CPU的讀寫速度.因此,這個時候,我們的CPU廠商就想了個辦法:在我們的每顆CPU上都加入了高速緩衝區,用來加快我們的讀寫速度,於是乎,我們的CPU跟我們的記憶體的互動就演變成為了這樣子的一張圖片.
但是,根據摩爾定律,我們的IC晶片每隔18個月能容納的電晶體會翻倍,但是我們的畢竟不可能不限制的增長,單核CPU的主頻也有效能瓶頸,想要提升效能,必須增加多個運算核心.所以,隨著時間的增長,我們的多核時代來臨了.
基於快取記憶體的儲存互動很好的解決了處理器與記憶體之間的矛盾,也引入了新的問題:快取一致性問題。在多處理器系統中,每個處理器有自己的快取記憶體,而他們又共享同一塊記憶體(下文成主存,main memory 主要記憶體),當多個處理器運算都涉及到同一塊記憶體區域的時候,就有可能發生快取不一致的現象。為了解決這一問題,需要各個處理器執行時都遵循一些協議,在執行時需要通過這些協議保證資料的一致性。比如MSI、MESI、MOSI、Synapse、Firely、DragonProtocol等。那麼怎麼做的呢?
而我們的執行時資料區其實也保有了這樣子的一種設計.其實參照這種設定,我們已經能夠推到出我們的JVM是如何跟我們的記憶體還有我們的CPU互動的了.java中使用的是多執行緒機制,那麼必然會有多個任務同時執行,這個時候類比了我們的CPU運算核心,那麼必然會有一塊區域或者一種操作能夠保證我們資料的一致性,那麼我們的JVM記憶體中資料存放的部分必然會是所有執行緒都能夠獲取到的,那麼就可以稱之為執行緒共享,而每個執行緒又有自己單獨的工作記憶體,當我們執行緒進行運作時,資料肯定會從JVM主存拷貝到執行緒自己的工作記憶體,然後再進行操作.

二、常量池

常量池分為我們前面所說過的靜態常量池,執行時常量池,還有字串常量池,那麼其實我們的執行時常量池又是什麼呢?

2.1、靜態常量池

其實儲存的就是字面量以及符號引用

2.2、執行時常量池

執行時常量池就是我們的每個類以及每個介面在我們的JVM進行run的過程中所在記憶體中開闢出來的一塊用來儲存我們靜態常量池部分資料的一塊特殊區域。

2.3、字串常量池

包含在動態常量池裡

2.4、JDK1.8中各常量池在記憶體中的劃分

 三、執行時資料區(Run-Time Data Areas)

在裝載階段的第(2),(3)步可以發現有執行時資料,堆,方法區等名詞(2)將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構(3)在Java堆中生成一個代表這個類的java.lang.Class物件,作為對方法區中這些資料的訪問入口,說白了就是類檔案被類裝載器裝載進來之後,類中的內容(比如變數,常量,方法,物件等這些資料得要有個去處,也就是要儲存起來,儲存的位置肯定是在JVM中有對應的空間)

3.1、官網概括

 

 3.2、jvm的執行時資料區

 

 3.2.1、Method Area(方法區)

(1)方法區只有一個,方法區是各個執行緒共享的記憶體區域,在虛擬機器啟動時建立
(2)雖然Java虛擬機器規範把方法區描述為堆的一個邏輯部分,但是它卻又一個別名叫做Non-Heap(非堆),目的是與Java堆區分開來
(3)用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料
(4)當方法區無法滿足記憶體分配需求時,將丟擲OutOfMemoryError異常
此時回看裝載階段的第2步,將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構如果這時候把從Class檔案到裝載的第(1)和(2)步合併起來理解的話,可以畫個圖
 

 

 3.2.2、Heap(堆) 

(1)Java堆是Java虛擬機器所管理記憶體中最大的一塊,在虛擬機器啟動時建立,被所有執行緒共享。 
(2)Java物件例項以及陣列都在堆上分配。 
(3)生命週期與虛擬機器一樣長
此時回看裝載階段的第3步,在Java堆中生成一個代表這個類的java.lang.Class物件,作為對方法區中這些資料的訪問入口 ,此時裝載(1)(2)(3)的圖可以改動一下 

 3.2.3、Java Virtual Machine Stacks(虛擬機器棧)

經過上面的分析,類載入機制的裝載過程已經完成,後續的連結,初始化也會相應的生效。 假如目前的階段是初始化完成了,後續就是要使用了,那怎樣才能被使用到;例如通過主函式main呼叫其他方法,這種方式實際上是main執行緒執行之後呼叫的方法,即要想使用裡面的各種內容,得要以執行緒為單位,執行相應的方法才行。
(1)虛擬機器棧是一個執行緒執行的區域,儲存著一個執行緒中方法的呼叫狀態。換句話說,一個Java執行緒的執行狀態,由一個虛擬機器棧來儲存,所以虛擬機器棧肯定是執行緒私有的,獨有的,隨著執行緒的建立而建立。 
(2)每一個被執行緒執行的方法,為該棧中的棧幀,即每個方法對應一個棧幀。呼叫一個方法,就會向棧中壓入一個棧幀;一個方法呼叫完成,就會把該棧幀從棧中彈出。 
圖解棧和棧幀
void a(){
b();
}
void b(){
c();
}
void c(){
}

棧幀
棧幀:每個棧幀對應一個被呼叫的方法,可以理解為一個方法的執行空間。每個棧幀中包括區域性變數表(Local Variables)、運算元棧(Operand Stack)、指向執行時常量池的引用(Areference to the run-time constant pool)、方法返回地址(Return Address)和附加資訊。
  • 區域性變數表:方法中定義的區域性變數以及方法的引數存放在這張表中區域性變數表中的變數不可直接使用,如需要使用的話,必須通過相關指令將其載入至運算元棧中作為運算元使用。
  • 運算元棧:以壓棧和出棧的方式儲存運算元的 
  • 動態連結:每個棧幀都包含一個指向執行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支援方法呼叫過程中的動態連線(Dynamic Linking)。
  • 方法返回地址:當一個方法開始執行後,只有兩種方式可以退出,一種是遇到方法返回的位元組碼指令;一種是遇見異常,並且這個異常沒有在方法體內得到處理。
結合位元組碼指令理解棧幀 
public static int calc(int, int);
 Code:
 0: iconst_3 //將int型別常量3壓入[運算元棧]
 1: istore_0 //將int型別值存入[區域性變數0]
 2: iload_0 //從[區域性變數0]中裝載int型別值入棧
 3: iload_1 //從[區域性變數1]中裝載int型別值入棧
 4: iadd //將棧頂元素彈出棧,執行int型別的加法,結果入棧
 5: istore_2 //將棧頂int型別值儲存到[區域性變數2]中
 6: iload_2 //從[區域性變數2]中裝載int型別值入棧
 7: ireturn //從方法中返回int型別的資料
 ... 

 

 

 3.2.4、The pc Register(程式計數器)

我們都知道一個JVM程式中有多個執行緒在執行,而執行緒中的內容是否能夠擁有執行權,是根據CPU排程來的。假如執行緒A正在執行到某個地方,突然失去了CPU的執行權,切換到執行緒B了,然後當執行緒A再獲得CPU執行權的時候,怎麼能繼續執行呢?這就是需要線上程中維護一個變數,記錄執行緒執行到的位置。
如果執行緒正在執行Java方法,則計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;
如果正在執行的是Native方法,則這個計數器為空。 

3.2.5、Native Method Stacks(本地方法棧)

如果當前執行緒執行的方法是Native型別的,這些方法就會在本地方法棧中執行。那如果在Java方法執行的時候呼叫native的方法呢?
除了上面五塊記憶體之外,其實我們的JVM還會使用到其他兩塊記憶體
  • 直接記憶體(Direct Memory)
並不是虛擬機器執行時資料區的一部分,也不是JVM規範中定義的記憶體區域,但是這部分記憶體也被頻繁地使用,而且也可能導致OutOfMemoryError 異常出現,所以我們放到這裡一起講解。在JDK1.4 中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffffer)的I/O 方式,它可以使用Native 函式庫直接分配堆外記憶體,然後通過一個儲存在Java 堆裡面的DirectByteBuffffer 物件作為這塊記憶體的引用進行操作。這樣能在一些場景中顯著提高效能,因為避免了在Java 堆和Native 堆中來回複製資料。
本機直接記憶體的分配不會受到Java 堆大小的限制,但是,既然是記憶體,則肯定還是會受到本機總記憶體的大小及處理器定址空間的限制。因此在分配JVM空間的時候應該考慮直接記憶體所帶來的影響,特別是應用到NIO的場景。
  • 其他記憶體: 
Code Cache:**JVM本身是個本地程式,還需要其他的記憶體去完成各種基本任務,比如,JIT編譯器在執行時對熱點方法進行編譯,就會將編譯後的方法儲存在Code Cache裡面;GC等功能。需要執行在本地執行緒之中,類似部分都需要佔用記憶體空間。這些是實現JVM JIT等功能的需要,但規範中並不涉及。 

3.3、演示說明

 3.3.1、棧指向堆

如果在棧幀中有一個變數,型別為引用型別,比如Object obj=new Object(),這時候就是典型的棧中元素指向堆中的物件。

 3.3.2、方法區指向堆

方法區中會存放靜態變數,常量等資料。如果是下面這種情況,就是典型的方法區中元素指向堆中的物件。
private static Object obj=new Object();

 3.3.3、堆指向方法區

 既然方法區可以指向堆,那麼堆能否指向方法區?
答案是可以的,不過這個不方便程式碼描述。試想一下,方法區中會包含類的資訊,堆中會有物件,那怎麼知道物件是由哪個類建立的呢?所以,在物件的物件頭中會有一個指標,用來指向方法區對應的類後設資料資訊
 

相關文章