1. 程式人生 > 程式設計 >深入淺出 JVM - JVM 內部結構總結

深入淺出 JVM - JVM 內部結構總結

目錄

  • 執行時資料區域
  • 垃圾回收演演算法
  • 垃圾收集器

執行時資料區域

  • 執行緒私有區域
    • 程式計數器
    • Java 虛擬機器器棧
    • 本地方法棧
  • 執行緒共享區域
    • Java 堆
    • 方法區
    • 執行時常量池(屬於方法區的一部分)

執行緒私有區域

程式計數器

程式計數器(Program Counter Register)是一塊較小的記憶體空間,它可以看作是當前執行緒所執行的位元組碼的行號指示器。位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計算器來完成。

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

Java 虛擬機器器棧

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

本地方法棧

本地方法棧(Native Method Stack)與虛擬機器器棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機器器棧為虛擬機器器棧執行 Java 方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器器使用到的 Native 方法服務。


執行緒共享區域

Java 堆

對於大多數應用來說,Java 堆(Java Heap)是 Java 虛擬機器器所管理的記憶體中最大的一塊。Java 堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器器啟動是建立。此記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體。這一點在 Java 虛擬機器器規範中的描述是:所有物件的例項以及陣列都要在堆上分配,但是隨著 JIT 編譯器的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發生,所有物件都分配在堆上也漸漸變得沒那麼"絕對"了。

方法區(非堆)

方法區(Method Area)與 Java 堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。雖然 Java 虛擬機器器規範把 Java 方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做 Non Heap

(非堆),目的應該是與 Java 堆區分開來。

方法區也被開發者成為"永久代"(Permanent Generation)。

執行時常量池(屬於方法區的一部分)

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


垃圾回收演演算法

標記-清除(Mark-Sweep)演演算法

如同名字一樣,演演算法分為"標記"和"清除"兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件,它的標記過程其實在前一節講述物件標記判定時已經介紹過了。之所以說它是最基礎的演演算法,是因為後續的收集演演算法都是基於這種思路並對其不足進行改進而得到的。

它的主要不足有兩個:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程式執行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。

複製演演算法

為瞭解決效率問題,複製(Copying)演演算法出現了,它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣就是每次只對其中一塊記憶體進行回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可,實現簡單,執行高效。

這種演演算法的代價是將記憶體縮小為了原來的一半,代價很大。這種演演算法也在特殊場景中會有很大用處,比如回收新生代的時候,IBM 公司的專門研究表明,新生代的物件 98% 是"朝生夕滅"的,所以不需要按照 1:1 的比例來劃分記憶體區域,而是將記憶體分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 空間和其中一塊 Survivor 空間。當回收時,將 Eden 空間和 Survivor 空間中還存活著的物件一次性地複製到另外一塊 Survivor 空間上,最後清理掉 Eden 和剛才使用過的 Survivor 空間。這裡肯定有一個具體空間分配比例,HotSpot 虛擬機器器預設 Eden:Survivor 為 8:1,也就是每次新生代中可用記憶體為整個新生代的 90%(80%+10%),只有 10% 的記憶體會被"浪費"。當然,98% 的物件可回收只是一般場景下的資料,JVM 沒有辦法保證每次回收都只有不多於 10% 的物件存活,當 Survivor 空間不夠用時,需要依賴其它記憶體(這裡指老年代)進行分配擔保(Handle Promotion)。

記憶體的分配擔保就好比我們現在使用支付寶裡面的花唄,如果我們信譽很好,在 98% 的情況下都能按時償還,於是支付寶會預設我們會在下一月也能按時按量的償還我們的預支,只需要有一個擔保人能保證如果我下次不能還款時,可以幫助你還錢,那支付寶就認為我們預支花唄是沒有風險的。記憶體的分配擔保也一樣,如果另外一塊 Survivor 空間沒有足夠空間存放上一次新生代收集下來的存活物件時,這些物件將直接通過分配擔保機制進入老年代。具體怎麼分配擔保會在後續分析。

標記-整理演演算法

複製收集演演算法在物件存活率較高時就要進行較多的複製操作,效率將會變低。更關鍵的時,如果不想浪費 50% 的空間,就需要有額外的空間進行分配擔保,以應對使用的記憶體中所有物件都 100% 存活的極端情況,所以在老年代一般不能直接選用複製演演算法。

根據老年代存活時間較長的特點,有人提出了另一種"標記-整理"(Mark-Compact)的演演算法,標記過程仍然與"標記-清除"演演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活物件想一端移動,然後直接清理掉邊界以外的記憶體。

分代收集演演算法

這種演演算法沒有什麼新的思想,只是根據物件存活週期的不同將記憶體劃分為幾塊。一般把 Java 堆分為新生代和老年代,這樣就可以根據各個年代的特點採用最適當的演演算法。

  • 新生代:複製演演算法

    因為在新生代中,每次垃圾回收時都發現有大批物件死去,只有少量存活,那就選用複製演演算法,只需付出少量存活物件的複製成本就可以完成收集。

  • 老年代:標記-清理/標記-整理

    因為老年代物件存活率高、沒有額外空間對它進行分配擔保。

更多精彩原創內容請關注:JavaInterview,歡迎 star,支援鼓勵以下作者,萬分感謝。