Java記憶體區域

打瞌睡的布偶貓 發表於 2021-06-07
Java

概述

對於 Java 程式設計師來說,在虛擬機器自動記憶體管理機制下,不再需要像C/C++程式開發程式設計師這樣為內一個 new 操作去寫對應的 delete/free 操作,不容易出現記憶體洩漏和記憶體溢位問題。正是因為 Java 程式設計師把記憶體控制權利交給 Java 虛擬機器,一旦出現記憶體洩漏和溢位方面的問題,如果不瞭解虛擬機器是怎樣使用記憶體的,那麼排查錯誤將會是一個非常艱鉅的任務。

執行時資料區

Java記憶體區域

Java記憶體區域

執行緒私有的包括:程式計數器、虛擬機器棧、本地方法棧

執行緒共享的:堆、方法區、直接記憶體

程式計數器

記錄正在執行的虛擬機器位元組碼指令的地址。由於是多執行緒,執行緒輪流切換,切換執行緒後為了能恢復到正常的執行位置,每個執行緒需要一個獨立的程式計數器。如果執行的是本地(Naive)方法,計數器為空。此記憶體區域是唯一一個沒有規定任何OutOfMemoryError情況的區域,它的生命週期隨著執行緒建立而建立,隨著執行緒結束而死亡。

Java虛擬機器棧

Java 記憶體可以粗糙的區分為堆記憶體(Heap)和棧記憶體(Stack),其中棧就是現在說的虛擬機器棧,或者說是虛擬機器棧中區域性變數表部分。

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

Java記憶體區域
區域性變數表存放了編譯器可知的各種基本資料型別(boolean,byte,char,short,int,float,long,double)、物件引用(reference型別,不同於物件本身,可能是一個指向物件起始地址的引用指標,也可能是指向一個代表物件的控制程式碼或其他與此物件相關的位置)和returnAddressleixing (位元組碼指令地址)。區域性變數表所需記憶體在編譯期間完成分配,執行期間不會改變。

可以通過 -Xss 這個虛擬機器引數來指定每個執行緒的 Java 虛擬機器棧記憶體大小: java -Xss 512M

可能丟擲的異常情況:

  • 若Java虛擬機器棧的記憶體大小不允許動態擴充套件,那麼當執行緒請求棧的深度超過當前Java虛擬機器棧的最大深度的時候(棧幀過多),就丟擲StackOverFlowError異常。
  • 若 Java 虛擬機器棧的記憶體大小允許動態擴充套件,且當執行緒請求棧時記憶體用完了,無法再動態擴充套件了,此時丟擲OutOfMemoryError異常。

本地方法棧

本地方法棧與 Java 虛擬機器棧類似,虛擬機器棧為虛擬機器執行 Java 方法 (也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的 Native 方法服務。有的虛擬機器如HotSpot虛擬機器把二者合二為一。丟擲的異常與上述一致。

本地方法一般是用其它語言(C、C++ 或組合語言等)編寫的,並且被編譯為基於本機硬體和作業系統的程式,對待這些方法需要特別處理。

Java堆

Java堆是整個虛擬機器所管理的最大記憶體區域,所有的物件建立都是在這個區域進行記憶體分配,是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立,此記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項以及陣列都在這裡分配記憶體。

Java堆是垃圾收集器管理的主要區域(方法區也需要回收),因此又稱為GC堆(Garbage Collected Heap)。現在收集器基本採用分代收集演算法,可以將堆分為新生代和老年代。劃分的好處是可以方便垃圾的準確回收。

Java記憶體區域

Java堆可以處於物理上不連續的記憶體空間中,只要邏輯上是連續的即可。堆還可以動態增加其記憶體,當堆中無法申請到新記憶體建立例項,並且堆也無法再擴充套件時,將會丟擲OutOfMemroyError。

可以通過 -Xms 和 -Xmx 兩個虛擬機器引數來指定一個程式的堆記憶體大小,第一個引數設定初始值,第二個引數設定最大值。

java -Xms1M -Xmx2M

方法區

方法區與 Java 堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。方法區也是所有執行緒共享。方法區邏輯上屬於堆的一部分,但是為了與堆進行區分,通常又叫“非堆”。

方法區和堆一樣不需要連續的記憶體,並且可以動態擴充套件,動態擴充套件失敗一樣會丟擲 OutOfMemoryError 異常。

在HotSpot虛擬機器中,把方法區當做永久代來進行GC,對起回收的目標主要是針對常量池的回收以及對型別的解除安裝,但是一般比較難實現。垃圾收集行為在這個區域是比較少出現的,但並非資料進入方法區後就“永久存在”了。由於方法區主要儲存類的相關資訊,所以對於動態生成類的情況比較容易出現永久代的記憶體溢位。在JDK1.8中,已經移除了永久代,用元空間來替代。元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機器中,而是使用本地記憶體。因此,預設情況下,元空間的大小僅受本地記憶體限制。

執行時常量池

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

Java記憶體區域
既然執行時常量池時方法區的一部分,自然受到方法區記憶體的限制,當常量池無法再申請到記憶體時會丟擲 OutOfMemoryError 異常。JDK1.7及之後版本的 JVM 已經將執行時常量池從方法區中移了出來,在 Java 堆(Heap)中開闢了一塊區域存放執行時常量池。

《Java 中幾種常量池的區分》: blog.csdn.net/qq_26222859…

直接記憶體

直接記憶體並不是虛擬機器執行時資料區的一部分,也不是虛擬機器規範中定義的記憶體區域,但是這部分記憶體也被頻繁地使用。而且也可能導致OutOfMemoryError異常出現。

JDK1.4中新加入的 NIO(New Input/Output) 類,引入了一種基於通道(Channel) 與快取區(Buffer) 的 I/O 方式,它可以直接使用Native函式庫直接分配堆外記憶體,然後通過一個儲存在 Java 堆中的 DirectByteBuffer 物件作為這塊記憶體的引用進行操作。這樣就能在一些場景中顯著提高效能,因為避免了在 Java 堆和 Native 堆之間來回複製資料。

本機直接記憶體的分配不會收到 Java 堆的限制,但是,既然是記憶體就會受到本機總記憶體大小以及處理器定址空間的限制。

參考資料