天天看點

Java虛拟機記憶體管理(一)—記憶體劃分

Java 與 C++ 之間有一堵由記憶體動态配置設定和垃圾收集技術所圍成的 “高牆”,牆外面的人想進去,牆裡面的人卻想出來。——《深入了解Java虛拟機:JVM進階特性與最佳時實踐(第二版)》周志明

Java 虛拟機作為運作 Java 程式抽象出來的計算機,具有記憶體管理的能力,像記憶體配置設定、垃圾回收等這些相關的記憶體管理問題,Java 虛拟機都會幫我們解決,是以作為一個 Java 程式員要比 C++ 程式員幸福,但是記憶體方面一旦出現問題,如果對虛拟機怎樣使用記憶體不了解,就很難排查錯誤。

這段時間看周志明先生的《深入了解Java虛拟機:JVM進階特性與最佳時實踐(第二版)》,下面就對 Java 虛拟機對記憶體的管理做一個系統的整理,本篇文章是該專題的第一篇。

1、記憶體劃分

記憶體是計算機中運作系統和軟體的場所,而記憶體劃分是 Java 虛拟機管理記憶體中人為添加的概念,是為了更好的描述 Java 虛拟機對記憶體的管理。下圖中的的運作時資料區域即是 Java 虛拟機所管理的記憶體區域。

Java虛拟機記憶體管理(一)—記憶體劃分

記憶體劃分.png

1.1 程式計數器

在 CPU 的寄存器中有指令計數器,而在 Java 虛拟機記憶體管理中也有類似的程式計數器。程式計數器占用一塊很小的記憶體空間,并且每條線程中都有獨立的程式計數器。指令計數器記錄的是 CPU 将要執行的下一條指令的位址,而程式計數器略有不同。線上程執行的 Java 方法時,程式計數器記錄的是正在執行的虛拟機位元組碼指令的位址,而線上程執行 Native 方法時,程式計數器為空,因為此時 Java 虛拟機調用是和作業系統相關的接口,與 Java 語言無關。

此區域是唯一一個在 Java 虛拟機規範中沒有規定會出現 OutOfMemoryError 情況的區域,對 OutOutOfMemoryError 的講解會在後面說到。

1.2 Java 虛拟機棧

我們常在程式運作的記憶體劃分為堆區和棧區,但是在 Java 中,這樣的劃分是很粗糙的,Java 虛拟機中棧有 Hava虛拟機棧和本地方法棧。同程式計數器一樣,Java 虛拟機棧也是每條線程私有的。虛拟機棧描述的是 Java 方法執行時的記憶體模型:每個方法在執行的同時都會建立一個棧幀用于存儲局部變量表、操作數棧、動态連結、方法出口等資訊。每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛拟機棧中入棧到出棧的過程。棧這種資料結構,就不多說了,特點是先進先出(FIFO),而棧幀就是棧中的資料元素,下圖中是棧幀的這種資料在 Java 棧中的結構圖。

Java虛拟機記憶體管理(一)—記憶體劃分

棧幀資料結構.png

局部變量表中存放的是編譯期可知的各種基本資料類型,包括 Java 的八大基本資料類型和對象引用(reference)類型(這種類型不在這裡詳細說了)。一個方法需要在幀中配置設定多大的局部變量空間是完全确定的,并且在其方法運作期間不會改變局部變量表的大小,進而可以知道局部變量表所需的記憶體空間在編譯期就确定下來了。

在 Java 虛拟機規範中,對這個區域規定了兩種異常出現的情況:

  • 如果線程請求的棧深度大于虛拟機所允許的深度,抛出 StackOverflowError 異常。
  • 如果虛拟機棧在動态擴充時無法申請到足夠的記憶體,抛出 OutOfMemoryError 異常。

1.3 本地方法棧

本地方法棧和虛拟機棧作用是相似的,他們之間的差別無非是虛拟機棧為虛拟機執行的是 Java 方法,本地方法棧為虛拟機使用的是 Native 方法。其實,不同的 Java 虛拟機,對棧區域的實作是不同的,比如主流的 HotSpot 虛拟機就把虛拟機棧和本地放棧合二為一了。

與虛拟機棧一樣,本地方法棧也會抛出 StackOverflowError 和 OutOfMemoryError 異常。

1.4 Java 堆

Java 堆是 Java 虛拟機記憶體所管理的記憶體最大的一塊,所有的線程都共享此區域,此區域可以說是 Java 對象的出生地,此區域的唯一目睹就是存放 Java 執行個體,幾乎所有的對象執行個體都在這裡配置設定記憶體(不同的編譯器有所不同)。Java 堆也是垃圾收集器管理的主要區域,也被稱為是 “GC堆”。Java 堆在實體上可以處于不連續的記憶體空間,隻要在邏輯上是連續的就可以了,就像磁盤空間存放檔案一樣。Java 堆既可以是固定大小的,也可以是可擴充的,在主流的 java 虛拟機中是按照可擴充來實作的。關于 Java 堆的詳細介紹将在後面說明。

如果在 Java 堆中沒有足夠的記憶體空間完成對象執行個體的配置設定,并且堆也無法再擴充,将會抛出 OutOfMemoryError 異常。

1.5 方法區

同 Java 堆一樣,方法區也是各個線程共享的區域,它用于存儲已經被虛拟機加載過的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料。Java 虛拟機規範中把方法區描述為堆的一個邏輯部分,也叫做 “非堆”,也是為了和 Java 堆區分開來。方法區和 Java 堆一樣,也不需要連續的記憶體空間,在 Java 虛拟機的實作中,也是可以選擇固定大小或者可擴充,并且還可以選擇不實作垃圾回收,因為這個區域需要用到回收的地方很少,但是實際開發種的教訓告訴我們,對方法區進行垃圾回收也是很有必要的,這個區域同樣會出現記憶體洩漏的問題。

在方法區中,有一部分被稱為是運作時常量池。常量池除了用于存放在編譯期生成的各種字面量和符号引用,此外還有直接引用也被存儲在運作時常量池中。運作時常量池具有動态性,常量并不一定實在編譯期才被放入該常量池,在運作期間也可以有新的常量放入池中,如我們在開發中使用 String 類的 intern() 方法時。

對字面量和符号引用不清楚的小夥伴可以看下面兩篇擴充閱讀。

字面量,常量和變量之間的差別?

個人了解 java虛拟機中的符号引用和直接引用

方法區中并不是像字面意思那樣存放方法的,它很像一個Java世界的身份資訊中心,類,常量、變量的資訊都有。——個人了解

當方法區無法滿足記憶體配置設定時,抛出 OutOfMemoryError 異常。

1.6 直接記憶體

直接記憶體并不在 Java 虛拟機管理的記憶體區域内,也不是 Java 虛拟機規範中定義的記憶體區域。直接記憶體是 Java 程式不經過 Java 虛拟機配置設定,直接使用主機的實體記憶體,在一些場景(如檔案指派)中可以提高性能,但是直接在使用直接記憶體中也要注意主機記憶體大小的限制(包括實體和系統級的限制),否則也會抛出 OutOfMemoryError 異常。