JVM之記憶體區域總結

leonsir發表於2019-03-09

概述


  對於Java程式設計師來說,在虛擬機器自動記憶體管理機制的幫助下,不再需要像C/C++程式設計師那樣要為每一個new操作去寫配對的delete/free程式碼,不容易出現記憶體洩漏和記憶體溢位的問題,由虛擬機器管理記憶體,這一切看起來很美好。不過,也正是因為Java程式設計師把記憶體控制的權利交給了Java虛擬機器,一旦出現記憶體洩漏和溢位方面的問題,如果不瞭解虛擬機器是怎樣使用記憶體的,那麼排查錯誤將是一項異常艱難的工作。

執行時資料區域


1. Java程式執行過程概述

在討論JVM記憶體區域之前,先來看一下Java程式的執行過程:

JVM之記憶體區域總結
  如上圖所示,首先Java原始檔(.java字尾檔案)會被Java編譯器變異為位元組碼檔案(.class字尾檔案),然後由JVM中的類載入器載入各個類的位元組碼檔案,載入完畢之後,交由 JVM執行引擎執行。在整個程式執行的過程中,JVM會用一段空間來儲存程式執行期間需要用到的資料和相關資訊,這段空間一般被稱作為Runtime Data Area(執行時資料區域),也就是我們常說的JVM記憶體。

2. 執行時資料區分割槽概述

  根據《Java虛擬機器規範》的規定,執行時資料區通常包括這幾個部分:程式計數器(Program Counter Register)、Java虛擬機器棧(JVM Statck)、本地方法棧(Native Method Stack)、Java堆(Java Heap)、方法區(Method Area)。詳細見下圖:

JVM之記憶體區域總結

3. 執行資料區各分割槽到底儲存了什麼?

  • 程式計數器(Program Counter Register)

  程式計數器,也有稱作為PC暫存器。瞭解計算機組成原理的同學對這個概念應該不陌生,在計算機組成原理中程式計數器是指CPU中的暫存器,它儲存的是當前執行的指令地址(也可以說是儲存下一條指令的所在儲存單元的地址),當CPU需要執行指令時,需要從程式計數器中得到當前需要執行的指令所在儲存單元的地址,然後根據得到的地址獲取到指令,在得到指令之後,程式計數器便自動加1或根據轉移指標得到下一天指令的地址,如此迴圈,直至執行完所有的指令。

  雖然JVM中的程式計數器並不像計算機中的程式計數器一樣是物理概念上的PC暫存器,但是JVM中的程式計數器的功能跟計算機中的程式計數器的功能邏輯是等同的, 也就是說是用來指示執行哪條指令的。

  由於在JVM中,多執行緒是通過執行緒間的輪流切換並分配處理器執行時間的方式來實現的, 在任何一個確定的時刻,一個處理器(對多核處理器來說是一個核心)都會執行一條執行緒的指令。因此,為了執行緒切換後還能恢復到正確的執行位置,每條執行緒都需要一個獨立的程式計數器,各個執行緒之間的計數器互不影響,獨立儲存,我們稱這類記憶體區域為 “執行緒私有” 的記憶體。

注: 在JVM規範中規定,如果執行緒執行的是非native方法,則程式計數器中儲存的是當前需要執行的指令的地址;如果執行緒執行的是native方法,則程式計數器中的值是undefined。

  由於程式計數器中儲存的資料所佔空間的大小不會隨程式的執行而發生改變,因此,對於程式計數器是不會發生記憶體溢位現象(OutOfMemory)的。

  • Java虛擬機器棧(JVM Statck)

  虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法在執行的過程中同時會建立一個棧幀(Stack Frame)用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊,並將建立的棧幀壓棧,當方法執行完畢之後,便會將棧幀出棧。(由此可知,Java棧中存放的是一個個棧幀,執行緒當前執行的方法所對應的棧幀必定位於Java棧的頂部。)簡單來說,每一個方法從呼叫直至執行的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。

JVM之記憶體區域總結

區域性變數表
複製程式碼

用來儲存方法中的區域性變數(包括在方法中宣告的非靜態變數以及函式形參)。對於基本資料型別的變數,則直接儲存它的值,對於引用型別的變數,則存的是指向物件的引用。區域性變數表的大小在編譯器就可以確定其大小了,因此在程式執行期間區域性變數表的大小是不會改變的。

運算元棧
複製程式碼

想必學過資料結構中的棧的朋友想必對錶達式求值問題不會陌生,棧最典型的一個應用就是用來對錶達式求值。想想一個執行緒執行方法的過程中,實際上就是不斷執行語句的過程,而歸根到底就是進行計算的過程。因此可以這麼說,程式中的所有計算過程都是在藉助於運算元棧來完成的。

指向執行時常量池的引用
複製程式碼

因為在方法執行的過程中有可能需要用到類中的常量,所以必須要有一個引用指向執行時常量。

方法返回地址
複製程式碼

當一個方法執行完畢之後,要返回之前呼叫它的地方,因此在棧幀中必須儲存一個方法返回地址。

注:由於每個執行緒正在執行的方法可能不同,因此每個執行緒都會有一個自己的Java棧,互不干擾。即,為執行緒私有記憶體區,生命週期:伴隨著執行緒的產生於死亡。

  • 本地方法棧(Native Method Stack)

  本地方法棧與Java棧的作用和原理非常相似。區別只不過是Java棧是為執行Java方法服務的,而本地方法棧則是為執行本地方法(Native Method)服務的。在JVM規範中,並沒有對本地方發展的具體實現方法以及資料結構作強制規定,虛擬機器可以自由實現它。在HotSopt虛擬機器中直接就把本地方法棧和Java棧合二為一。

  • Java堆(Java Heap)

  在C語言中,堆這部分空間是唯一一個程式設計師可以管理的記憶體區域。程式設計師可以通過malloc函式和free函式在堆上申請和釋放空間。那麼在Java中是怎麼樣的呢?

  Java中的堆是用來儲存物件本身以及陣列(當然,陣列引用是存放在Java棧中的)。只不過和C語言中的不同,在Java中,程式設計師基本不用去關心空間釋放的問題,Java的垃圾回收機制會自動進行處理。因此這部分空間也是Java垃圾收集器管理的主要區域。另外,堆是被所有執行緒共享的,在JVM中只有一個堆。

  • 方法區(Method Area)

  方法區在JVM中也是一個非常重要的區域,它與堆一樣,是被執行緒共享的區域。在方法區中,儲存了每個類的資訊(包括類的名稱、方法資訊、欄位資訊)、靜態變數、常量以及編譯器編譯後的程式碼等

  在Class檔案中除了類的欄位、方法、介面等描述資訊外,還有一項資訊是常量池,用來儲存編譯期間生成的字面量和符號引用。

  在方法區中有一個非常重要的部分就是執行時常量池(Runtime Constant Pool),它是每一個類或介面的常量池的執行時表示形式,在類和介面被載入到JVM後,對應的執行時常量池就被建立出來。當然並非Class檔案常量池中的內容才能進入執行時常量池,在執行期間也可將新的常量放入執行時常量池中,比如String的intern方法。

  在JVM規範中,沒有強制要求方法區必須實現垃圾回收。很多人習慣將方法區稱為“永久代”,是因為HotSpot虛擬機器以永久代來實現方法區,從而JVM的垃圾收集器可以像管理堆區一樣管理這部分割槽域,從而不需要專門為這部分設計垃圾回收機制。不過自從JDK7之後,Hotspot虛擬機器便將執行時常量池從永久代移除了。

參考資料


相關文章