講講 JVM 的內存結構
- 什么是 JVM 內存結構?
- 線程私有
- 程序計數器?
- 虛擬機棧
- 本地方法棧
- 線程共享
- 堆?
- 方法區?
- 注意
- 永久代?
- 元空間?
- 運行時常量池?
- 直接內存?
- 代碼詳解
什么是 JVM 內存結構?
JVM內存結構分為5大區域,程序計數器、虛擬機棧、本地方法棧、堆、方法區。?
HotSpot在JDK1.8之前方法區就是永久代,永久代就是方法區。JDK1.8后刪除了永久代,改為元空間,元空間在本地內存中。方法區就是元空間,元空間就是方法區。?
創建一個線程,JVM就會為其分配一個私有內存空間,其中包括PC、虛擬機棧和本地方法棧?
簡單來說:
- 堆:存放 new 出來的東西
- 方法區:被虛擬機加載的類信息、常量、靜態常量等。
- 棧:存放局部變量
- 程序計數器:記錄指令
- 本地方法棧:Native 方法
接著進行詳細的介紹:
線程私有
程序計數器?
線程私有的,作為當前線程的行號指示器,用于記錄當前虛擬機正在執行的線程指令地址。程序計數器主要有兩個作用:?
- 當前線程所執行的字節碼的行號指示器,通過它實現代碼的流程控制,如:順序執行、選擇、循環、異常處理。?
- 在多線程的情況下,程序計數器用于記錄當前線程執行的位置,當線程被切換回來的時候能夠知道它上次執行的位置。?
程序計數器是唯一一個不會出現 OutOfMemoryError
的內存區域,它的生命周期隨著線程的創建而創建,隨著線程的結束而死亡。?
虛擬機棧
Java 虛擬機棧是由一個個棧幀組成,而每個棧幀中都擁有:局部變量表、操作數棧、動態鏈接、方法出口信息。每一次函數調用都會有一個對應的棧幀被壓入虛擬機棧,每一個函數調用結束后,都會有一個棧幀被彈出。?
局部變量表是用于存放方法參數和方法內的局部變量。?
每個棧幀都包含一個指向運行時常量池中該棧所屬方法的符號引用,在方法調用過程中,會進行動態鏈接,將這個符號引用轉化為直接引用。?
- 部分符號引用在類加載階段的時候就轉化為直接引用,這種轉化就是靜態鏈接?
- 部分符號引用在運行期間轉化為直接引用,這種轉化就是動態鏈接?
Java 虛擬機棧也是線程私有的,每個線程都有各自的 Java 虛擬機棧,而且隨著線程的創建而創建,隨著線程的死亡而死亡。
Java 虛擬機棧會出現兩種錯誤:StackOverFlowError
和 OutOfMemoryError
。?
可以通過 -Xss
參數來指定每個線程的虛擬機棧內存大小:java -Xss2M?
本地方法棧
虛擬機棧為虛擬機執行 Java 方法服務,而本地方法棧則為虛擬機使用到的 Native 方法服務。Native 方法一般是用其它語言(C、C++等)編寫的。?
本地方法被執行的時候,在本地方法棧也會創建一個棧幀,用于存放該本地方法的局部變量表、操作數棧、動態鏈接、出口信息。?
線程共享
堆?
堆用于存放對象實例,是垃圾收集器管理的主要區域,因此也被稱作GC堆。?
堆可以細分為:新生代(Eden空間(伊甸園)、From Survivor、To Survivor空間)和老年代。?
通過 -Xms
設定程序啟動時占用內存大小,通過 -Xmx
設定程序運行期間最大可占用的內存大小。如果程序運行需要占用更多的內存,超出了這個設置值,就會拋出OutOfMemory
異常。?
方法區?
方法區與 Java 堆一樣,是各個線程共享的內存區域,它用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。?
對方法區進行垃圾回收的主要目標是對常量池的回收和對類的卸載。?
注意
永久代?
方法區是 JVM 的規范,而永久代PermGen 是方法區的一種實現方式,并且只有 HotSpot 有永久代。對于其他類型的虛擬機,如 JRockit 沒有永久代。由于方法區主要存儲類的相關信息,所以對于動態生成類的場景比較容易出現永久代的內存溢出。?
元空間?
JDK 1.8 的時候,HotSpot的永久代被徹底移除了,使用元空間替代。元空間的本質和永久代類似,都是對JVM規范中方法區的實現。
兩者最大的區別在于:元空間并不在虛擬機中,而是使用本地內存。
?
為什么要將永久代替換為元空間呢??
永久代內存受限于 JVM 可用內存,而元空間使用的是直接內存,受本機可用內存的限制,雖然元空間仍舊可能溢出,但是相比永久代內存溢出的概率更小。?
運行時常量池?
運行時常量池是方法區的一部分,在類加載之后,會將編譯器生成的各種字面量和符號引號放到運行時常量池。在運行期間動態生成的常量,如 String 類的 intern()方法,也會被放入運行時常量池。?
直接內存?
直接內存并不是虛擬機運行時數據區的一部分,也不是虛擬機規范中定義的內存區域,但是這部分內存也被頻繁地使用,而且也可能導致 OutOfMemoryError 錯誤出現。?
NIO 的 Buffer 提供了 DirectBuffer,可以直接訪問系統物理內存,避免堆內內存到堆外內存的數據拷貝操作,提高效率。
DirectBuffer直接分配在物理內存中,并不占用堆空間,其可申請的最大內存受操作系統限制,不受最大堆內存的限制。?
直接內存的讀寫操作比堆內存快,可以提升程序I/O操作的性能。通常在I/O通信過程中,會存在堆內內存到堆外內存的數據拷貝操作,對于需要頻繁進行內存間數據拷貝且生命周期較短的暫存數據,都建議存儲到直接內存。
代碼詳解
以下是一個簡單的 Java 類,我們將在注釋中說明各個內存區域的作用:
public class MemoryStructureExample {// 靜態變量,存放在方法區private static int staticVar = 42;public static void main(String[] args) {// 局部變量,存放在虛擬機棧int localVar = 10;// 創建一個對象,存放在堆中MemoryStructureExample obj = new MemoryStructureExample();// 調用方法,創建一個新的棧幀obj.doSomething(localVar);// 創建一個數組,存放在堆中int[] array = new int[5];// 常量,存放在方法區final String constantString = "Hello, world!";// 本地方法調用,使用本地方法棧System.out.println("Static variable: " + staticVar);System.out.println("Constant string: " + constantString);}// 方法,存放在方法區public void doSomething(int value) {// 局部變量,存放在虛擬機棧int result = value * 2;System.out.println("Result: " + result);}
}
在這段代碼中,我們可以看到:
staticVar
是一個靜態變量,存放在方法區。localVar
是一個局部變量,存放在虛擬機棧。obj
是一個對象,存放在堆中。array
是一個數組,也存放在堆中。constantString
是一個常量,存放在方法區。doSomething
方法創建了一個新的棧幀,其中的局部變量存放在虛擬機棧。
總之,這些內存區域共同構成了 Java 虛擬機的內存結構,確保 Java 程序的正常運行和優化。如果你還有其他問題,歡迎繼續提問!