Java面試- JVM 記憶體模型講解

健健君發表於2019-10-22

經常有人會有這麼一個疑惑,難道 Java 開發就一定要懂得 JVM 的原理嗎?我不懂 JVM ,但我照樣可以開發。確實,但如果懂得了 JVM ,可以讓你在技術的這條路上走的更遠一些。

JVM 的重要性

首先你應該知道,執行一個 Java 應用程式,我們必須要先安裝 JDK 或者 JRE 。這是因為 Java 應用在編譯後會變成位元組碼,然後通過位元組碼執行在 JVM 中,而 JVM 是 JRE 的核心組成部分。

優點

JVM 不僅承擔了 Java 位元組碼的分析(JIT compiler)和執行(Runtime),同時也內建了自動記憶體分配管理機制。這個機制可以大大降低手動分配回收機制可能帶來的記憶體洩露和記憶體溢位風險,使 Java 開發人員不需要關注每個物件的記憶體分配以及回收,從而更專注於業務本身。

缺點

這個機制在提升 Java 開發效率的同時,也容易使 Java 開發人員過度依賴於自動化,弱化對記憶體的管理能力,這樣系統就很容易發生 JVM 的堆記憶體異常、垃圾回收(GC)的不合適以及 GC 次數過於頻繁等問題,這些都將直接影響到應用服務的效能。

記憶體模型

JVM 記憶體模型共分為5個區:堆(Heap)方法區(Method Area)程式計數器(Program Counter Register)虛擬機器棧(VM Stack)本地方法棧(Native Method Stack)

其中,堆(Heap)方法區(Method Area)執行緒共享程式計數器(Program Counter Register)虛擬機器棧(VM Stack)本地方法棧(Native Method Stack)執行緒隔離

堆(Heap)

堆是 JVM 記憶體中最大的一塊記憶體空間,該記憶體被所有執行緒共享,幾乎所有物件和陣列都被分配到了堆記憶體中。

堆被劃分為新生代和老年代,新生代又被進一步劃分為 Eden 區和 Survivor 區,最後 Survivor 由 From Survivor 和 To Survivor 組成。

隨著 Java 版本的更新,其內容又有了一些新的變化:

在 Java6 版本中,永久代在非堆記憶體區;到了 Java7 版本,永久代的靜態變數和執行時常量池被合併到了堆中;而到了 Java8,永久代被元空間(處於本地記憶體)取代了。

為什麼要用元空間替換永久代呢?

  1. 為了融合 HotSpot JVM 與 JRockit VM,因為 JRockit 沒有永久代,所以不需要配置永久代。
  2. 永久代記憶體經常不夠用或發生記憶體溢位(應該是 JVM 中佔用記憶體最大的一塊),產生異常 java.lang.OutOfMemoryError: PermGen。在 JDK1.7 版本中,指定的 PermGen 區大小為 8M,由於 PermGen 中類的後設資料資訊在每次 FullGC 的時候都可能被收集,回收率都偏低,成績很難令人滿意;還有,為 PermGen 分配多大的空間很難確定,PermSize 的大小依賴於很多因素,比如,JVM 載入的 class 總數、常量池的大小和方法的大小等。

看到這兒,自然就想到了 GC 回收演算法,不用急,我會在之後的文章中進行講解,現在還是以 JVM 記憶體模型為主。

方法區(Method Area)

什麼是方法區?

方法區主要是用來存放已被虛擬機器載入的類相關資訊,包括類資訊常量池(字串常量池以及所有基本型別都有其相應的常量池)、執行時常量池。這其中,類資訊又包括了類的版本、欄位、方法、介面和父類等資訊。

類資訊

JVM 在執行某個類的時候,必須經過載入、連線、初始化,而連線又包括驗證、準備、解析三個階段。

在載入類的時候,JVM 會先載入 class 檔案,而在 class 檔案中便有類的版本、欄位、方法和介面等描述資訊,這就是類資訊

常量池

在 class 檔案中,除了類資訊,還有一項資訊是常量池 (Constant Pool Table),用於存放編譯期間生成的各種字面量符號引用

字面量符號引用又是什麼呢?

字面量包括字串(String a=“b”)、基本型別的常量(final 修飾的變數),符號引用則包括類和方法的全限定名(例如 String 這個類,它的全限定名就是 Java/lang/String)、欄位的名稱和描述符以及方法的名稱和描述符。

執行時常量池

當類載入到記憶體後,JVM 就會將 class 檔案常量池中的內容存放到執行時常量池中;在解析階段,JVM 會把符號引用替換為直接引用(物件的索引值)。

例如:

類中的一個字串常量在 class 檔案中時,存放在 class 檔案常量池中的。

在 JVM 載入完類之後,JVM 會將這個字串常量放到執行時常量池中,並在解析階段,指定該字串物件的索引值。

執行時常量池是全域性共享的,多個類共用一個執行時常量池,因此,class 檔案中常量池多個相同的字串在執行時常量池只會存在一份。

講到這裡,大家是不是有些頭暈了,說實話,我在看到這些內容的時候,也是雲裡霧裡的,這裡舉個例子幫助大家理解:

    public static void main(String[] args) {
        String str = "Hello";
        System.out.println((str == ("Hel" + "lo")));

        String loStr = "lo";
        System.out.println((str == ("Hel" + loStr)));

        System.out.println(str == ("Hel" + loStr).intern());
    }複製程式碼

其執行結果為:

true
false
true複製程式碼

第一個為 true,是因為在編譯成 class 檔案時,能夠識別為同一字串的, JVM 會將其自動優化成字串常量,引用自同一 String 物件。

第二個為 false,是因為在執行時建立的字串具有獨立的記憶體地址,所以不引用自同一 String 物件。

最後一個為 true,是因為 String 的 intern() 方法會查詢在常量池中是否存在一個相等(呼叫 equals() 方法結果相等)的字串,如果有則返回該字串的引用,如果沒有則新增自己的字串進入常量池。

涉及到的Error

  1. OutOfMemoryError出現在方法區無法滿足記憶體分配需求的時候,比如一直往常量池中加入資料,執行時常量池就會溢位,從而報錯。

程式計數器(Program Counter Register)

程式計數器是一塊很小的記憶體空間,主要用來記錄各個執行緒執行的位元組碼的地址,例如,分支、迴圈、跳轉、異常、執行緒恢復等都依賴於計數器。

由於 Java 是多執行緒語言,當執行的執行緒數量超過 CPU 數量時,執行緒之間會根據時間片輪詢爭奪 CPU 資源。如果一個執行緒的時間片用完了,或者是其它原因導致這個執行緒的 CPU 資源被提前搶奪,那麼這個退出的執行緒就需要單獨的一個程式計數器,來記錄下一條執行的指令。

由此可見,程式計數器和上下文切換有關。

虛擬機器棧(VM Stack)

虛擬機器棧是執行緒私有的記憶體空間,它和 Java 執行緒一起建立。

當建立一個執行緒時,會在虛擬機器棧中申請一個執行緒棧,用來儲存方法的區域性變數、運算元棧、動態連結方法和返回地址等資訊,並參與方法的呼叫和返回。

每一個方法的呼叫都伴隨著棧幀的入棧操作,方法的返回則是棧幀的出棧操作。

可以這麼理解,虛擬機器棧針對當前 Java 應用中所有執行緒,都有一個其相應的執行緒棧,每一個執行緒棧都互相獨立、互不影響,裡面儲存了該執行緒中獨有的資訊。

涉及到的Error

  1. StackOverflowError出現在棧記憶體設定成固定值的時候,當程式執行需要的棧記憶體超過設定的固定值時會丟擲這個錯誤。
  2. OutOfMemoryError出現在棧記憶體設定成動態增長的時候,當JVM嘗試申請的記憶體大小超過了其可用記憶體時會丟擲這個錯誤。

本地方法棧(Native Method Stack)

本地方法棧跟虛擬機器棧的功能類似,虛擬機器棧用於管理 Java 方法的呼叫,而本地方法棧則用於管理本地方法的呼叫。

但本地方法並不是用 Java 實現的,而是由 C 語言實現的。

也就是說,本地方法棧中並沒有我們寫的程式碼邏輯,其由native修飾,由 C 語言實現。

總結

以上就是 JVM 記憶體模型的基本介紹,大致瞭解了一下5個分割槽及其相應的含義和功能,由此可以繼續延伸出 Java 記憶體模型、 GC 演算法等等,我也會在之後的文章中進行講解。如果你有什麼想法,歡迎在下方留言。

有興趣的話可以訪問我的部落格或者關注我的公眾號、頭條號,說不定會有意外的驚喜。

death00.github.io/

相關文章