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