Java
虛擬機器在執行 Java
程式 時,把它所管理的記憶體劃分為若干個不同的資料區域,主要包括以下五個部分:程式計數器、Java
堆、Java
虛擬機器棧、方法區和本地方法棧。
JVM 記憶體結構
程式計數器
程式計數器是當前執行緒所執行的位元組碼的行號指示器,它會指出下一條將要執行的指令的地址,位元組碼直譯器就是通過改變計數器的值來選取程式接下來執行的操作。
程式計數器是執行緒私有的一小塊記憶體,每條執行緒都要有一個獨立的程式計數器,以使執行緒切換後恢復到正確的執行位置。
- 如果執行緒正在執行
Java
方法,則計數器記錄的是正在執行的虛擬機器位元組碼指令的地址 - 如果執行
native
方法,則計數器為空
它也是唯一一個不會出現 OutOfMemoryError
的記憶體區域。
Java 虛擬機器棧
與程式計數器一樣,Java
虛擬機器棧也是執行緒私有的,線上程建立時 Java
棧會被建立,每個方法在在執行的同時都會建立一個棧幀,用於存放區域性變數表,運算元棧,動態連結,方法出口等資訊。每一個方法從呼叫直至執行完成,都對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。
一般所謂的“棧”,指的是虛擬機器棧中區域性變數表部分,其中存放了各種基本資料型別( 8
種),物件引用(reference
型別) 和 returnAddress
型別。區域性變數表所需的空間在編譯期就已經確定並完成分配,在方法執行期間不會被改變。
Java
虛擬棧中可能出現兩種異常:
StackOverflowError
:執行緒請求的棧深度大於虛擬機器所允許的深度OutOfMemoryError
:虛擬機器棧擴充套件時無法申請到足夠的記憶體
本地方法棧
本地方法棧與 Java
虛擬機器棧的作用類似,區別是 Java
虛擬機器棧為虛擬機器執行 Java
方法服務,而本地方法棧為虛擬機器執行 Native
方法服務。有的虛擬機器(例如 HotSpot
虛擬機器)直接把本地方法棧和 Java
虛擬機器棧合併在一起。
本地方法棧也可能會丟擲 StackOverflowError
和 OutOfMemoryError
異常。
Java 堆
Java
堆是是虛擬機器中最主要的記憶體區域。它為執行緒共享,在虛擬機器啟動時建立,幾乎所有的物件例項都儲存在 Java
堆中。
Java
堆也被稱作 "GC"
堆。從記憶體回收角度看,可分為新生代和老年代。而新生代又可分為 Eden
區、From Survivor
區、To Survivor
區等。
Java
堆的實現,既可以實現為固定的,也可以是擴充套件的。當前虛擬機器都按照可擴充套件來實現,通過 -Xmx
和 -Xms
控制堆大小。
如果堆中沒有記憶體並且也無法再擴充套件時,會丟擲 OutOfMemeoryError
異常。
方法區
方法區與 Java
堆一樣,為執行緒共享。用於儲存已被虛擬機器載入的類資訊,常量,靜態變數,即時編譯器編譯後的程式碼等資料。也叫作 Non-Heap
(非堆)。
如果方法區無法滿足記憶體分配需求,會丟擲 OutOfMemoryError
異常。
執行時常量池
執行時常量池是方法區的一部分。Class
檔案中的常量池用於編譯期生成的各種字面量和符號引用,這部分內容在類載入後被存入執行時常量池。
動態性是執行時常量池相對於 Class
檔案常量池的一個重要特徵,即不要求常量一定只有編譯期才能產生,執行期間也可能將新的常量放入池中。
執行時常量池受到方法區記憶體的限制,如果常量池無法再申請記憶體,就會丟擲 OutOfMemoryError
異常。
直接記憶體
直接記憶體並不由 JVM
管理,它是利用 Native
函式庫在 Java
堆外申請分配的記憶體區域,可以避免在 Java
堆和 Native
堆中複製資料以提高效能。
例如 NIO
中的 DirectByteBuffer
就可以作為這塊記憶體的引用進行操作直接記憶體。
永久代與元空間
有時會看到方法區被稱為永久代,其實兩者有著本質的區別。方法區是 JVM
規範中的定義,而永久代是 JVM
規範的一種實現,並且只有在 HotSpot
虛擬機器中如此,其他虛擬機器中沒有永久代的說法。
在 JDK1.6
之前,HotSpot
虛擬機器把 GC
分代收集擴充套件至方法區,或者說使用永久代實現方法區。不過永久代有 -XX:MaxPermSize
的上限,很容易遇到記憶體溢位問題。
所以在 JDK1.7
中,將部分資料已經轉移 Java Heap
或 Native Heap
中,例如:將原本放在永久代中的字串池和類的靜態變數移出到 Java Heap
中,將符號引用轉移到 Native Heap
中。但永久代仍然存在,並沒有移除。
在 JDK1.8
中,取消了永久代,代替為元空間實現,它也是 JVM
規範中方法區的一種實現。不過它與永久代最大的不同是:元空間並不在虛擬機器中,而是將元空間放到本地記憶體中。所以預設情況下,它只受本地記憶體的限制,可以通過 -XX:MetaspaceSize
引數設定初始空間大小,預設沒有最大空間限制。
常見的 OOM 及原因
Java
中的 OOM
指的就是 java.lang.OutOfMemoryError
異常。主要有以下幾種:
java.lang.OutOfMemoryError:Java heap space
Java
堆中主要用於存放各種物件例項。當堆中沒有足夠的空間分配給新物件時,或者說達到了堆空間設定的最大空間限制,則會丟擲此異常。
引起記憶體溢位的原因主要有:
- 流量訪問量大,超過設定的堆空間大小;
- 記憶體洩露,不能被回收的物件消耗過多堆空間;
java.lang.OutOfMemoryError:Permgen space
在 JDK7
中,HotSpot
虛擬機器使用永久代實現方法區,永久代較小,而且回收效率較低,很容易出現記憶體溢位。
因此,JDK8
取消了永久代,使用元空間來實現方法區,存放在本地記憶體中。
java.lang.OutOfMemoryError:Metaspace
方法區主要儲存類的元資訊,HotSpot
後設資料區。當元空間沒有足夠的空間分配給載入的類時,會丟擲此異常。
引起後設資料區空間不足的原因主要有:
- 載入的類太多,常見於
jsp
頁面過多時; - 元空間被實現在堆外,主要受到程式本身的記憶體限制,一般很難出現溢位。
參考資料
- Hollis:JVM記憶體結構 VS Java記憶體模型 VS Java物件模型
- liuxiaopeng:Java8記憶體模型—永久代(PermGen)和元空間(Metaspace)