運行時常量池(Runtime Constant Pool)是方法區中的一部分,用于存儲編譯期間生成的各種字面量和符號引用。在JAVA程序運行時,JVM將編譯期生成的class文件中的常量池內容讀取到運行時常量池中。
JVM簡介
JVM(Java Virtual machine,Java虛擬機)是Java語言的核心,是一個用于解釋Java字節碼的虛擬計算機。它可以在運行Java程序時自動管理內存、處理異常等。Java程序員不需要關心底層硬件和操作系統的細節,只需要編寫符合Java語法規范的代碼,就可以實現跨平臺的編程。
當我們編寫Java程序時,Java源代碼會被編譯成為Java字節碼( .java 文件被編譯成 .class 文件)。這些字節碼可以在任何安裝了Java虛擬機的平臺上運行。JVM在執行Java字節碼時,將其轉換成特定于底層CPU和操作系統的機器代碼。
運行時數據區簡介
為了執行字節碼,JVM在內存中定義了一系列的數據區,用于在運行時存儲各類數據,即運行時數據區(Runtime Data Areas)。理解這些數據區及其作用,是掌握Java性能調優和錯誤排查的關鍵。
JVM 運行時數據區是 Java 虛擬機在執行 Java 程序時用于數據存儲的內存區域,這些區域各司其職,確保了 Java 程序的正確執行。JVM 運行時數據區主要分為五個部分:程序計數器(Program Counter Register)、虛擬機棧(VM Stack)、本地方法棧(Native Method Stack)、堆(Heap)、方法區(Method Area)。JVM運行時數據區在程序運行時動態地分配和釋放內存,內存管理由JVM自動完成。不同的數據區域有不同的內存管理機制和垃圾回收算法,以保證程序運行的效率和穩定性。
其中程序計數器、虛擬機棧、本地方法棧屬于線程私有區域,跟隨線程的啟動和結束而建立和銷毀。堆和方法區是線程共享區域,跟隨虛擬機進程的啟動而存在。
程序計數器(Program Counter Register) 是一塊較小的內存空間,作用是指示當前線程正在執行的 JVM 字節碼指令地址。
虛擬機棧(VM Stack) 存放的是一些基本類型的變量(如int, long)和對象引用。Java 方法執行的內存模型是以棧幀(Stack Frame)為基礎的,每個方法在執行的時候都會創建一個棧幀,棧幀中存放了局部變量表、操作數棧、動態鏈接、方法出口等信息。
本地方法棧(Native Method Stack) 與虛擬機棧類似,其主要服務于 JVM 使用到的 Native 方法。
堆區(Heap) 是 JVM 所管理的最大一塊內存空間,主要用于存放所有線程共享的 Java 對象實例。這也是垃圾回收器主要活動區域。
方法區(Method Area) 是用來存儲加載的類信息、常量、靜態變量等數據的。這個區域是線程共享的。
1. 程序計數器
程序計數器(Program Counter Register)是線程私有區域,生命周期與線程一致,也是 JVM 內存中唯一一個沒有任何 OutOfMemoryError 的區域。
程序計數器的作用是記錄當前線程正在執行的指令地址,換句話說,它指向了下一條將要被執行的 JVM 字節碼指令。在 JVM 的概念模型中,字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令。
當線程執行的是 Java 方法時,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是 Native 方法,這個計數器的值則為空(Undefined)。
程序計數器對于現代多線程而言至關重要,因為在 CPU 切換各個線程時,需要將各個線程的程序計數器記錄下來,以便在下一次切換回這個線程時,能知道該從哪里繼續執行。
總結:
- 程序計數器是一塊很小的內存空間,也是運行速度最快的存儲區域。
- 在 JVM 規范中,每個線程都有它自己的程序計數器,是線程私有的,生命周期與線程的生命周期一致。
- 如果當前線程正在執行的是 Java 方法,程序計數器記錄的是 JVM 字節碼指令地址,如果是執行 native 方法,則是未指定值(undefined)
- 它是程序控制流的指示器,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成
- 字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令
- 它是唯一一個在 JVM 規范中沒有規定任何 OutOfMemoryError 情況的區域
2. 虛擬機棧
與程序計數器一樣,Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,生命周期與線程相同。描述的是Java方法執行的內存模型。
在 JVM 中,每當一個新的線程被創建,都會創建一個與之關聯的私有 JVM 棧。這個棧會隨著線程的運行而進行入棧(push)和出棧(pop)操作。它主要用于存儲局部變量、操作數堆棧以及方法調用的情況。
JVM 棧是由一系列棧幀(Stack Frame)組成的。每當一個方法被調用,一個新的棧幀就會被壓入棧中,每當一個方法調用結束,一個棧幀就會被彈出棧。每個棧幀中都包含了局部變量表、操作數棧、動態鏈接和方法返回地址等信息。
局部變量表主要存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference 類型,它不等同于指針,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或者其他與此對象相關的位置)和 returnAddress 類型(指向了一條字節碼指令的地址)。
操作數棧則是在執行字節碼指令時用到的臨時存儲區,比如在進行算數運算時,操作數棧就會用來存放操作數和接收結果。
Java虛擬機棧可能會拋出以下異常:
- 如果線程請求的棧深度大于 JVM 所允許的深度,將拋出 StackOverflowError。
- 如果 JVM 棧可以動態擴展,當擴展時無法申請到足夠的內存,會拋出 OutOfMemoryError。
3. 本地方法棧
本地方法棧(Native Method Stack)也是線程私有,生命周期與線程相同。作用是與虛擬機棧類似,虛擬機棧是為Java 方法服務的,而本地方法棧是為 Native 方法服務的。
和虛擬機棧一樣,本地方法棧的大小可以是固定的也可以是動態的。如果是固定的,當線程請求的棧深度超過最大深度時,會拋出 StackOverflowError。如果是動態的,并且在嘗試擴展時無法申請到足夠的內存,會拋出 OutOfMemoryError。
4. 堆
堆(Heap)是 JVM 所管理的最大一塊內存空間,也是所有線程共享的一塊內存區域,在虛擬機啟動時創建。堆主要用于存儲對象實例和數組,這也是 Java 垃圾回收器主要活動的區域。
在物理上,堆區可以處于分散的內存空間中,但在邏輯上它被視為連續的。堆區在 JVM 啟動時創建,如果堆區的空間不足,將會拋出 OutOfMemoryError。
堆分為新生代(Young Generation)和老年代(Old Generation)。新生代又分為 Eden 區、From Survivor 區(簡稱 S0)、 To Survivor 區(簡稱 S1)。劃分這么多區域的目的是為了更好地回收內存,或者更快地分配內存。
新生代中各個區域的內存占比分別是,Eden : S0 : S1 = 8 : 1 : 1
新創建的對象優先在 Eden 區進行分配。當 Eden 區滿時,會觸發一次 Minor GC(新生代垃圾回收,也叫 Young GC),將仍然存活的對象從 Eden 區和 S0 區移動到 S1 區,下次 Minor GC 處理情況類似,把存活的對象從 Eden 區和 S1 區移動到 S0 區。當 Survivor 區也滿了,還存活的對象會被移動到老年代。如果老年代也滿了,將會觸發 Major GC(老年代垃圾回收,也叫 Old GC)。當老年代滿了,也可能觸發 Full GC,Full GC 會對整個堆內存進行垃圾回收,包含新生代、老年代和方法區。Full GC 會導致較長的停頓時間,并且會消耗大量的系統資源。
5. 方法區
方法區(Method Area)與堆一樣,是所有線程共享的內存區域,用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。
方法區只是 JVM 規范中定義的一個概念,針對 Hotspot 虛擬機,JDK8 之前使用永久代(Permanent Generation,簡稱 PermGen)實現,JDK8 使用元空間(Metaspace)實現。
JDK8 之前可以通過 -XX:PermSize 和 -XX:MaxPermSize 來設置永久代大小,JDK8 之后,使用元空間替換了永久代,改為通過 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 來設置元空間大小。
運行時常量池
運行時常量池(Runtime Constant Pool)是方法區中的一部分,用于存儲編譯期間生成的各種字面量和符號引用。在Java程序運行時,JVM將編譯期生成的class文件中的常量池內容讀取到運行時常量池中。
運行時常量池存儲了類和接口中的常量,包括字符串字面量、被聲明為final的常量值等。它還存儲了類和接口中的符號引用,如類和接口、字段和方法的引用等。
在JVM中,運行時常量池是線程安全的。每個線程都有一個自己的線程棧,其中包含了局部變量表,而這些局部變量表中所引用的對象都位于堆中。當一個線程需要引用運行時常量池中的常量時,JVM會先將常量值從運行時常量池中復制到線程棧的局部變量表中,然后再進行引用。
需要注意的是,在JDK8中,運行時常量池已經被移動到元空間(Metaspace)中。元空間是在本地內存中分配的,與JVM的堆內存是分離的,因此不會受到Java堆大小的限制。