深入理解JVM(一)JVM記憶體模型

_雲起_發表於2018-06-15

Java虛擬機器在執行Java程式的過程中會把它所管理的記憶體劃分為若干個不同的資料區域,總共包括以下幾個執行時資料區域。

深入理解JVM(一)JVM記憶體模型

1 、程式計數器(Program Counter Register)

程式計數器是一塊較小的記憶體空間,它的作用:

1.1. 可以看做是當前執行緒所執行的位元組碼的訊號指示器。位元組碼直譯器就是通過改變該計數器的值來選取下一條需要執行的位元組碼指令, 分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需依賴計數器來完成。注:但是,如果當前執行緒正在執行的是一個本地方法,那麼此時程式計數器為空。

1.2. 在多執行緒的情況下,程式計數器用於記錄當前執行緒執行的位置,從而當執行緒被切換回來的時候能夠知道該執行緒上次執行到哪兒了。

特點:執行緒私有的, 生命週期隨著執行緒的建立而建立,隨著執行緒的結束而死亡。 此記憶體區域是唯一一個在 Java 虛擬機器規範中沒有規定任何 OutOfMemoryError 情況的區域。

2、Java 虛擬機器棧(Java Virtual Machine Stack)

Java 虛擬機器棧與程式計數器一樣,也是執行緒私有的,其生命週期與執行緒相同。虛擬機器 棧描述的是 Java 方法執行的記憶體模型:每個方法被執行的時候都會同時建立一個棧幀(Stack Frame)用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。每一個方法被呼叫 直至執行完成的過程就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。

區域性變數表存放了編譯期可知的各種基本資料型別(boolean、byte、char、short、int、 float、long、double)、物件引用(reference 型別)和 returnAddress 型別(指向了一條字 節碼指令的地址)。其中 64 位長度的 long 和 double 會佔用 2 個區域性變數空間(Slot,一個 32 位),其餘資料型別只佔用 1 個。區域性變數表所需的空間在編譯期間完成分配,當進入一 個方法時,其需要在幀中分配多大的區域性變數空間是確定的,方法執行期間不會改變區域性變 量表的大小。區域性變數表的建立是在方法被執行的時候,隨著棧幀的建立而建立。而且,區域性變數表的大小在編譯時期就確定下來了,在建立的時候只需分配事先規定好的大小即可。此外,在方法執行的過程中區域性變數表的大小是不會發生改變的。

Java 虛擬機器規範中對該區域規定了兩種異常情況:

2.1. 如 線 程 請 求 的 深 度 大 於 虛 擬 機 所 允 許 的 深 度 , 棧 溢 出 , 如 遞 歸 時 , 拋 出

StackOverflowError 異常。

2.2. 虛擬機器棧動態擴充套件無法申請到足夠的記憶體時,丟擲 OutOfMemoryError 異常。

當方法傳遞引數時實際上是一個方法將自己棧幀中區域性變數表的副本傳遞給另一個方法棧幀中的局 部變數表(注意是副本,而不是其本身),不管資料型別是什麼(基本型別,引用型別)

3、本地方法棧(Native Method Stack)

Java 虛擬機器可能會使用到傳統的棧來支援 native 方法(使用 Java 語言以外的其它語言 編寫的方法)的執行。執行緒私有的,如 Sun HotSpot 虛擬機器直接把本地方法棧和虛擬機器棧合 二為一。Java 虛擬機器規範中對該區域規定了兩種異常情況:

3.1.如執行緒請求的深度大於虛擬機器所允許的深度,丟擲 StackOverflowError 異常。

3.2.虛擬機器棧動態擴充套件無法申請到足夠的記憶體時,丟擲 OutOfMemoryError 異常。

4、Java 堆(Java Heap)

Java 堆是 Java 虛擬機器管理記憶體中最大的一塊,是所有執行緒共享的記憶體區域,隨虛擬機器的啟動而建立。該區域唯一目的是存放物件例項,幾乎所有物件的例項都在堆裡面分配。 Java 虛擬機器規範規定,Java 堆可以出於物理上不連續的記憶體空間中,只要邏輯上連續即可,如同磁碟空間一樣,既可以實現成固定大小,也可以是擴充套件的,當前主流虛擬機器都是 按照擴充套件來實現的(通過-Xmx 和-Xms 控制)。

Java堆是垃圾收集器管理的主要區域,因此也叫"GC堆",細分一點可以分為新生代和老年代;再細緻一點新生代可以分為Eden空間、From Survivor空間、ToSurvivor空間。

Java 虛擬機器規範中對該區域規定了 OutOfMemoryError 異常:如果堆中沒有 記憶體完成例項分配,並且堆無法再擴充套件則丟擲 OutOfMemoryError 異常。(當 Ol d 區被放滿的之後,進行 Full GC,Full GC 後,若 Survivor 及 old 區仍然無法存放 從 Eden 複製過來的部分物件,則出現 OOM 錯誤/或者直接存放大物件、大陣列,導致老年代空間不足)

5、方法區(Method Area)

方法區與 Java 堆一樣,是各個執行緒共享的記憶體區域,用於儲存已被虛擬機器載入的類信 息、常量、靜態變數、即時編譯器編譯後的程式碼等資料。在 HotSpot 中用永久代來實現方法區,而其他虛擬機器(如 BEA JRockit、IBM J9 等)是不存在永久代的。

Java7 中已經將執行時常量池從永久代移除,在 Java 堆(Heap)中開闢了一塊區域存放執行時常量池。而在 Java8 中,已經徹底沒有了永久代,將方法區直接放在一個與堆不相連的本地記憶體區域,這個區域被叫做元空間。

元空間的本質和永久代類似,都是對 JVM 規範中方法區的實現。不過元空間與永久代 之間最大的區別在於:元空間並不在虛擬機器中,而是使用本地記憶體。因此,預設情況下,元 空間的大小僅受本地記憶體限制,但可以通過以下引數來指定元空間的大小:-XX:MetaspaceSize,初始空間大小。-XX:MaxMetaspaceSize,最大空間,預設是沒有 限制的。Java 虛擬機器規範中對方法區規定了 OutOfMemoryError 異常: 如果方法區的記憶體空間 不能滿足記憶體分配請求,那 Java 虛擬機器將丟擲一個 OutOfMemoryError 異常。

6、執行時常量池(Runtime Constant Pool)

執行時常量池是方法區的一部分。執行緒共享。Class 檔案中除了有類的版本、欄位、方 法、介面等資訊外,還有一項資訊是常量池,用於存放編譯期生成的各種字面常量和符號引 用,這部分內容在類載入後存放到方法區的常量池中。

static 修飾的靜態變數也存放在方法區中,但不是在常量池中(不能修飾區域性變數),不 能在一個方法內部定義 static 變數(final 可以),只能定義為成員變數。

當這個類被Java虛擬機器載入後,class檔案中的常量就存放在方法區的執行時常量池中。而且在執行期間,可以向常量池中新增新的常量。如:String類的intern()方法就能在執行期間向常量池中新增字串常量。

當執行時常量池中的某些常量沒有被物件引用,同時也沒有被變數引用,那麼就需要垃圾收集器回收。

Java 虛擬機器規範中對該區域規定了 OutOfMemoryError 異常: 當常量池無法申請到內 存時丟擲 OutOfMemoryError 異常。

7、直接記憶體

直接記憶體是除Java虛擬機器之外的記憶體,但也有可能被Java使用。

在NIO中引入了一種基於通道和緩衝的IO方式。它可以通過呼叫本地方法直接分配Java虛擬機器之外的記憶體,然後通過一個儲存在Java堆中的DirectByteBuffer物件直接操作該記憶體,而無需先將外面記憶體中的資料複製到堆中再操作,從而提升了資料操作的效率。

直接記憶體的大小不受Java虛擬機器控制,但既然是記憶體,當記憶體不足時就會丟擲OOM異常。

8、棧幀

棧幀是用於支援虛擬機器進行方法呼叫和方法執行的資料結構,它是虛擬機器執行時資料區 的虛擬機器棧的棧元素。棧幀儲存了方法的區域性變數表,運算元棧,動態連線和方法返回地址 等資訊。第一個方法從呼叫開始到執行完成,就對應著一個棧幀在虛擬機器棧中從入棧到出棧 的過程。在編譯程式碼的時候,棧幀中需要多大的區域性變數表,多深的運算元棧都已經完全確 定了,並且寫入到了方法表的 Code 屬性中,因此一個棧幀需要分配多少記憶體,不會受到程 序執行期變數資料的影響,而僅僅取決於具體虛擬機器的實現。一個執行緒中的方法呼叫鏈可能會很長,很多方法都同時處理執行狀態。對於執行引擎來 講,活動執行緒中,只有虛擬機器棧頂的棧幀才是有效的,稱為當前棧幀(Current Stack Frame),這個棧幀所關聯的方法稱為當前方法(Current Method)。

8.1、區域性變數表

區域性標量表是一組變數值的儲存空間,一個以字長為單位,從 0 開始計數的陣列,用於 存放方法引數和區域性變數。變數槽 (Variable Slot)是區域性變數表的最小單位,沒有強制規 定大小為 32 位,雖然 32 位足夠存放大部分型別的資料。一個 Slot 可以存放 boolean、 byte、char、short、int、float、reference 和 returnAddress 8 種型別。其中 reference 表 示對一個物件例項的引用。returnAddress 則指向了一條位元組碼指令的地址。 對於 64 位的 long 和 double 變數而言,虛擬機器會為其分配兩個連續的 Slot 空間。虛擬機器通過索引定位的方式使用區域性變數表。之前我們知道,區域性變數表存放的是方法引數和區域性變數。當呼叫方法是非 static 方法時,區域性變數表中第 0 位索引的 Slot 預設是用於傳遞方法所屬物件例項的引用,即“this”關鍵字指向的物件。分配完方法引數後,便會 依次分配方法內部定義的區域性變數。

為了節省棧幀空間,區域性變數表中的 Slot 是可以重用的。當離開了某些變數的作用域 之後,這些變數對應的 Slot 就可以交給其他變數使用。

8.2、運算元棧

運算元棧被組織成一個以字長為單位的陣列。但不是通過索引來訪問,而是通過標準棧 操作--壓棧和出棧來訪問。方法執行中進行算術運算或者是呼叫其他的方法進行引數傳遞的 時候是通過運算元棧進行的。

在概念模型中,兩個棧幀是相互獨立的。但是大多數虛擬機器的實現都會進行優化,令兩 個棧幀出現一部分重疊。令下面的部分運算元棧與上面的區域性變數表重疊在一塊,這樣在方 法呼叫的時候可以共用一部分資料,無需進行額外的引數複製傳遞。

8.3、幀資料區

棧幀需要一些資料來支援常量池解析、正常方法返回和異常處理等。在幀資料區中儲存

著訪問常量池的指標,方便程式訪問常量池。此外,當函式返回或者出現異常時,虛擬機器必 須恢復呼叫者函式的棧幀,並讓呼叫者函式繼續執行下去。對於異常處理,虛擬機器必須有一 個異常處理表,方便在發生異常的時候找到處理異常的程式碼,因此異常處理表也是幀資料區 中重要的一部分。

8.3.1 動態連線

每個棧幀都包含一個指向執行時常量池中該棧幀所屬方法的引用,持有這個引用是為了

支援方法呼叫過程中的動態連線。符號引用一部分會在類載入階段或者第一次使用的時候就 轉化為直接引用,這種轉化成為靜態解析。另外一部分在每一次執行期間轉化為直接引用, 這部分稱為動態連線。

8.3.2 方法返回地址

當一個方法開始執行後,只有兩種方式可以退出這個方法。第一種是執行引擎遇到任意 一個方法返回的位元組碼指令,這時候可能會有返回值傳遞給上層的方法呼叫者。

另一種退出方式是,在方法執行過程中遇到了異常,並且這個異常沒有在方法體內得到 處理。無論採用何種退出方式,在方法退出之後,都需要返回到方法被呼叫的位置,程式才 能繼續執行。

8.4 OutOfMemoryError

在 eclipse 中設定-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError(堆最小 值、最大值設定成一樣為了避免自動擴充套件,輸出記憶體溢位時資訊)

Java 堆用於儲存物件例項,只要不斷建立物件,並且保證 GC Roots 到物件之間有可達路 徑來避免垃圾回收,當物件數量達到最大堆容量後就產生記憶體溢位異常。

深入理解JVM(一)JVM記憶體模型
深入理解JVM(一)JVM記憶體模型


相關文章