JVM之記憶體結構詳解

二師兄-公眾號-程式新視界發表於2019-10-18

對於開發人員來說,如果不瞭解Java的JVM,那真的是很難寫得一手好程式碼,很難查得一手好bug。同時,JVM也是面試環節的中重災區。今天開始,《JVM詳解》系列開啟,帶大家深入瞭解JVM相關知識。

我們不能為了面試而面試,但是學習會這些核心知識你必定會成為面試與工作中“最亮的一顆星”。本系列首發於微信公眾號“程式新視界”。下面,開啟我們的第一篇文章《JVM之記憶體結構詳解》。

學習也是要講究方式方法的,本系列學習過程中會引導大家通過《費曼學習法》來學習,同時儘量採用圖文方式來進行講解。正所謂一圖勝千言。

思考一下

學習一項知識總該知道為什麼學習吧。有人會說,這些寫程式碼好像又用不上,貌似所有的事情JVM都替我們做好了。那就,思考一下為什麼要學習JVM虛擬機器結構。

那你是否遇到這樣的困惑:堆記憶體該設定多大?OutOfMemoryError異常到底是怎麼引起的?如何進行JVM調優?JVM的垃圾回收是如何?甚至建立一個String物件,JVM都做了些什麼?

這些疑問隨著學習的深入都會慢慢得到解答,而要解決這些問題的第一步,就是先了解JVM的構成。

JVM記憶體結構

java虛擬機器在執行程式的過程中會將記憶體劃分為不同的資料區域,看一下下圖。

image

如果理解了上圖,JVM的記憶體結構基本上掌握了一半。通過上圖我們可以看到什麼?外行看熱鬧,內行看門道。從圖中可以得到如下資訊。

第一,JVM分為五個區域:虛擬機器棧、本地方法棧、方法區、堆、程式計數器。PS:大家不要排斥英語,此處用英文記憶反而更容易理解。

第二,JVM五個區中虛擬機器棧、本地方法棧、程式計數器為執行緒私有,方法區和堆為執行緒共享區。圖中已經用顏色區分,綠色表示“通行”,橘黃色表示停一停(需等待)。

第三,JVM不同區域的佔用記憶體大小不同,一般情況下堆最大,程式計數器較小。那麼最大的區域會放什麼?當然就是Java中最多的“物件”了。

學習延伸:如果你記住了這張圖,是不是就可以說出關於JVM的記憶體結構了呢?可以嘗試一下,切記不用死記硬背,發揮你的想象。

堆(Heap)

上面已經得出結論,堆記憶體最大,堆是被執行緒共享,堆的目的就是存放物件。幾乎所有的物件例項都在此分配。當然,隨著優化技術的更新,某些資料也會被放在棧上等。

槍打出頭鳥,樹大招風。因為堆佔用記憶體空間最大,堆也是Java垃圾回收的主要區域(重點物件),因此也稱作“GC堆”(Garbage Collected Heap)。

關於GC的操作,我們後面章節會詳細講,但正因為GC的存在,而現代收集器基本都採用分代收集演算法,堆又被細化了。

image

同樣,對上圖呈現內容彙總分析。

第一,堆的GC操作採用分代收集演算法。

第二,堆區分了新生代和老年代;

第三,新生代又分為:Eden空間、From Survivor(S0)空間、To Survivor(S1)空間。

Java虛擬機器規範規定,Java堆可以處於物理上不連續的記憶體空間中,只要邏輯上是連續的即可。也就是說堆的記憶體是一塊塊拼湊起來的。要增加堆空間時,往上“拼湊”(可擴充套件性)即可,但當堆中沒有記憶體完成例項分配,並且堆也無法再擴充套件時,將會丟擲OutOfMemoryError異常。

方法區(Method Area)

方法區與堆有很多共性:執行緒共享、記憶體不連續、可擴充套件、可垃圾回收,同樣當無法再擴充套件時會丟擲OutOfMemoryError異常。

正因為如此相像,Java虛擬機器規範把方法區描述為堆的一個邏輯部分,但目前實際上是與Java堆分開的(Non-Heap)。

方法區個性化的是,它儲存的是已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。

方法區的記憶體回收目標主要是針對常量池的回收和對型別的解除安裝,一般來說這個區域的回收“成績”比較難以令人滿意,尤其是型別的解除安裝,條件相當苛刻,但是回收確實是有必要的。

image

程式計數器(Program Counter Register)

關於程式計數器我們已經得知:佔用記憶體較小,現成私有。它是唯一沒有OutOfMemoryError異常的區域。

程式計數器的作用可以看做是當前執行緒所執行的位元組碼的行號指示器,位元組碼直譯器工作時就是通過改變計數器的值來選取下一條位元組碼指令。其中,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴計數器來完成。

Java虛擬機器的多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個核心)只會執行一條執行緒中的指令。

image

因此,為了執行緒切換後能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各條執行緒之間的計數器互不影響,獨立儲存,我們稱這類記憶體區域為“執行緒私有”的記憶體。

如果執行緒正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是Natvie方法,這個計數器值則為空(Undefined)。

虛擬機器棧(JVM Stacks)

虛擬機器棧執行緒私有,生命週期與執行緒相同。

棧幀(Stack Frame)是用於支援虛擬機器進行方法呼叫和方法執行的資料結構。棧幀儲存了方法的區域性變數表、運算元棧、動態連線和方法返回地址等資訊。每一個方法從呼叫至執行完成的過程,都對應著一個棧幀在虛擬機器棧裡從入棧到出棧的過程。

image

區域性變數表(Local Variable Table)是一組變數值儲存空間,用於存放方法引數和方法內定義的區域性變數。包括8種基本資料型別、物件引用(reference型別)和returnAddress型別(指向一條位元組碼指令的地址)。

其中64位長度的long和double型別的資料會佔用2個區域性變數空間(Slot),其餘的資料型別只佔用1個。

如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常;如果虛擬機器棧動態擴充套件時無法申請到足夠的記憶體時會丟擲OutOfMemoryError異常。

運算元棧(Operand Stack)也稱作操作棧,是一個後入先出棧(LIFO)。隨著方法執行和位元組碼指令的執行,會從區域性變數表或物件例項的欄位中複製常量或變數寫入到運算元棧,再隨著計算的進行將棧中元素出棧到區域性變數表或者返回給方法呼叫者,也就是出棧/入棧操作。

動態連結:Java虛擬機器棧中,每個棧幀都包含一個指向執行時常量池中該棧所屬方法的符號引用,持有這個引用的目的是為了支援方法呼叫過程中的動態連結(Dynamic Linking)。

方法返回:無論方法是否正常完成,都需要返回到方法被呼叫的位置,程式才能繼續進行。

本地方法棧(Native Method Stacks)

本地方法棧(Native Method Stacks)與虛擬機器棧作用相似,也會丟擲StackOverflowError和OutOfMemoryError異常。

區別在於虛擬機器棧為虛擬機器執行Java方法(位元組碼)服務,而本地方法棧是為虛擬機器使用到的Native方法服務。

小結

經過上面的講解,想必大家已經瞭解到JVM記憶體結構的基本情況。下面對照腦圖,歸納總結一下,看你能說出來多少。

image

後續更多《JVM詳解》系列內容及其他面試題系列內容請關注微信公眾號“程式新視界”,持續更新中。

原文連結:《JVM之記憶體結構詳解

系列文章:《面試官,不要再問我“Java GC垃圾回收機制”了


程式新視界

關注程式設計師的職場生涯,大量優質學習資源、技術文章分享

程式新視界-微信公眾號

相關文章