經典Java技術面試:JVM 內存模型講解

2019-10-10     IT技術分享

經常有人會有這麼一個疑惑,難道一定要懂得 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,永久代被 元空間 (處於本地內存)取代了。

看到這兒,就想到了 GC 回收算法,不用急,我會在下一篇文章中進行講解。

方法區(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 文件中常量池多個相同的字符串在運行時常量池只會存在一份。

涉及到的Error

  1. OutOfMemoryError 出現在方法區無法滿足內存分配需求的時候,比如一直往常量池中加入數據, 運行時常量池 就會溢出,從而報錯。

程序計數器(Program Counter Register)

程序計數器是一塊很小的內存空間,主要用來記錄各個線程執行的位元組碼的地址,例如,分支、循環、跳轉、異常、線程恢復等都依賴於計數器。

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

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

虛擬機棧(VM Stack)

虛擬機棧是線程私有的內存空間,它和 Java 線程一起創建。

當創建一個線程時,會在虛擬機棧中申請一個線程棧,用來保存方法的局部變量、操作數棧、動態連結方法和返回地址等信息,並參與方法的調用和返回。

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

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

涉及到的Error

StackOverflowError
OutOfMemoryError

本地方法棧(Native Method Stack)

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

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

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

總結

以上就是 JVM 內存模型的基本介紹,大致了解了一下5個分區及其相應的含義和功能,由此可以繼續延伸出 Java 內存模型、 GC 算法等等。

文章來源: https://twgreatdaily.com/zh-hk/gWQTtm0BMH2_cNUg4Txs.html