Java虛擬機在執行Java程序的過程中會把它所管理的內存劃分為若干個不同的數據區域。這些區域
有各自的用途,以及創建和銷毀的時間,有的區域隨著虛擬機進程的啟動而一直存在,有些區域則是
依賴用戶線程的啟動和結束而建立和銷毀。
1. 程序計數器(Program Counter Register)
- 作用:程序計數器是一塊較小的內存空間,它可以看作是當前線程所執行的字節碼的行號指示器。字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。(簡化:用來存儲指向下一條指令的地址,即將要執行的指令代碼)
- 線程私有:由于 Java 虛擬機的多線程是通過線程輪流切換并分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對于多核處理器來說是一個內核)都只會執行一條線程中的指令。因此,為了線程切換后能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲。
- 無內存溢出情況:程序計數器是唯一一個在《Java 虛擬機規范》中沒有規定任何 OutOfMemoryError 情況的區域。
2. Java 虛擬機棧(Java Virtual Machine Stacks)
- 作用:與程序計數器一樣,Java 虛擬機棧也是線程私有的,它的生命周期與線程相同。虛擬機棧描述的是 Java 方法執行的線程內存模型:每個方法被執行的時候,Java 虛擬機都會同步創建一個棧幀(Stack Frame)用于存儲局部變量表、操作數棧、動態連接、方法出口等信息。每一個方法被調用直至執行完畢的過程,就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。
- 局部變量表:存放了編譯期可知的各種 Java 虛擬機基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference 類型,它并不等同于對象本身,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或者其他與此對象相關的位置)和 returnAddress 類型(指向了一條字節碼指令的地址)。
- 異常情況:如果線程請求的棧深度大于虛擬機所允許的深度,將拋出 StackOverflowError 異常;如果 Java 虛擬機棧容量可以動態擴展,當棧擴展時無法申請到足夠的內存會拋出 OutOfMemoryError 異常。
3. 本地方法棧(Native Method Stacks)
- 作用:本地方法棧與虛擬機棧所發揮的作用是非常相似的,其區別只是虛擬機棧為虛擬機執行 Java 方法(也就是字節碼)服務,而本地方法棧則是為虛擬機使用到的本地(Native)方法服務。
- 本地方法是使用非 Java 語言(如 C、C++)實現的方法,它們可以直接訪問底層操作系統的資源。例如:Java 程序有時需要與操作系統的底層功能進行交互,比如文件操作、網絡操作等。由于 Java 本身的跨平臺性,有些底層操作無法直接實現,這時就需要借助本地方法。
- 異常情況:和虛擬機棧一樣,本地方法棧也會在棧深度溢出或者棧擴展失敗時分別拋出 StackOverflowError 和 OutOfMemoryError 異常。
4. Java 堆(Java Heap)
- 作用:Java 堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內存。從內存分配的角度來看,線程共享的 Java 堆中可能劃分出多個線程私有的分配緩沖區(Thread Local Allocation Buffer,TLAB)。不過無論如何劃分,都與存放內容無關,無論哪個區域,存儲的都仍然是對象實例。
- 垃圾回收的主要區域:Java 堆是垃圾收集器管理的主要區域,因此很多時候也被稱作 “GC 堆”。根據垃圾收集算法的不同,Java 堆還可以細分為:新生代和老年代;再細致一點有 Eden 空間、From Survivor 空間、To Survivor 空間等。
- 異常情況:如果在 Java 堆中沒有內存完成實例分配,并且堆也無法再擴展時,Java 虛擬機將會拋出 OutOfMemoryError 異常。
5. 方法區(Method Area)
- 作用:方法區是存放基礎信息的位置,線程共享。主要包含三部分內容:1.類的元信息,保存了所有類的基本信息,2.運行時常量池,保存了字節碼文件中的常量池內容、3.字符串常量池,保存了字符串常量
- 運行時常量池:是方法區的一部分,Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池表(Constant Pool Table),用于存放編譯期生成的各種字面量與符號引用,這部分內容將在類加載后存放到方法區的運行時常量池中。運行時常量池相對于 Class 文件常量池的另外一個重要特征是具備動態性,Java 語言并不要求常量一定只有編譯期才能產生,也就是說,并非預置入 Class 文件中常量池的內容才能進入方法區運行時常量池,運行期間也可以將新的常量放入池中,這種特性被開發人員利用得比較多的便是 String 類的 intern () 方法。
- 異常情況:當方法區無法滿足新的內存分配需求時,將拋出 OutOfMemoryError 異常。
- 方法區的實現
-
- JDK7及之前的版本將方法區存放在堆區域中的永久代空間,堆的大小由虛擬機參數來控制。
- JDK8及之后的版本將方法區存放在元空間中,元空間位于操作系統維護的直接內存中,默認情況下只要不超過操作系統承受的上限,可以一直分配。
6. 直接內存(Direct Memory)
- 作用:直接內存并不是虛擬機運行時數據區的一部分,也不是《Java 虛擬機規范》中定義的內存區域。但是這部分內存也被頻繁地使用,而且也可能導致 OutOfMemoryError 異常出現。在 JDK 1.4 中新加入了 NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(Buffer)的 I/O 方式,它可以使用 Native 函數庫直接分配堆外內存,然后通過一個存儲在 Java 堆里面的 DirectByteBuffer 對象作為這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在 Java 堆和 Native 堆中來回復制數據。
- 異常情況:由于直接內存不受 Java 堆大小的限制,但是受本機總內存(包括 RAM 及 SWAP 區或者分頁文件)大小以及處理器尋址空間的限制,所以在動態分配直接內存時可能會出現 OutOfMemoryError 異常。
面試題
基礎概念類
- 問題:請簡要介紹一下 JVM 運行時數據區域包含哪些部分?
參考答案:JVM 運行時數據區域主要包含以下幾個部分:
-
- 程序計數器:線程私有,可看作當前線程所執行的字節碼的行號指示器,是唯一不會出現 OutOfMemoryError 的區域。
- Java 虛擬機棧:線程私有,描述 Java 方法執行的線程內存模型,每個方法執行會創建棧幀,存儲局部變量表、操作數棧等信息,可能拋出 StackOverflowError 和 OutOfMemoryError 異常。
- 本地方法棧:與虛擬機棧類似,為虛擬機使用的本地方法服務,也可能拋出 StackOverflowError 和 OutOfMemoryError 異常。
- Java 堆:線程共享,是對象實例分配內存的主要區域,也是垃圾收集器管理的主要區域,可能拋出 OutOfMemoryError 異常。
- 方法區:線程共享,用于存儲已被虛擬機加載的類型信息、常量、靜態變量等數據,可能拋出 OutOfMemoryError 異常。
- 運行時常量池:是方法區的一部分,存放編譯期生成的字面量與符號引用,具備動態性。
- 直接內存:不是虛擬機運行時數據區的一部分,但也會被頻繁使用,可能導致 OutOfMemoryError 異常。
- 問題:哪些區域是線程共享的,哪些是線程私有的?
參考答案:線程共享的區域有 Java 堆、方法區(包含運行時常量池);線程私有的區域有程序計數器、Java 虛擬機棧、本地方法棧。
原理機制類
- 問題:Java 虛擬機棧中棧幀的作用是什么,包含哪些內容?
參考答案:棧幀是 Java 虛擬機棧中用于支持方法調用和方法執行的數據結構。每一個方法從調用開始到執行完成的過程,都對應著一個棧幀在虛擬機棧中入棧到出棧的過程。棧幀包含以下內容:
-
- 局部變量表:用于存儲方法參數和方法內部定義的局部變量。
- 操作數棧:在方法執行過程中,用于存儲中間計算結果和臨時數據。
- 動態連接:將符號引用轉換為直接引用,實現方法的動態綁定。
- 方法出口:記錄方法執行完畢后,從哪個位置繼續執行調用該方法的后續代碼。
- 問題:運行時常量池的動態性體現在哪里?
參考答案:運行時常量池的動態性體現在它并不局限于編譯期生成的常量。Java 語言允許在運行期間將新的常量放入池中,例如 String 類的 intern () 方法。當調用 intern () 方法時,如果運行時常量池中已經包含一個等于此 String 對象的字符串,則返回常量池中的字符串;否則,將此 String 對象添加到常量池中,并返回該 String 對象的引用。
異常處理類
- 問題:在 JVM 運行時數據區域中,哪些區域可能會拋出 OutOfMemoryError 異常?
參考答案:可能拋出 OutOfMemoryError 異常的區域有 Java 堆、方法區、Java 虛擬機棧(當棧容量可以動態擴展時)、本地方法棧(當棧容量可以動態擴展時)和直接內存。例如,當 Java 堆中沒有足夠的內存來分配新的對象實例,并且堆也無法再擴展時,會拋出 OutOfMemoryError 異常;方法區無法滿足新的內存分配需求時,也會拋出該異常。 - 問題:StackOverflowError 和 OutOfMemoryError 有什么區別?
參考答案:StackOverflowError 通常是由于線程請求的棧深度大于虛擬機所允許的深度而拋出的異常,一般是在遞歸調用方法時沒有正確的終止條件,導致棧幀不斷入棧,最終棧空間耗盡。而 OutOfMemoryError 是在無法申請到足夠的內存時拋出的異常,它可能發生在 Java 堆、方法區、虛擬機棧(動態擴展時)、本地方法棧(動態擴展時)和直接內存等區域。
應用場景類
- 問題:在實際開發中,如何優化 JVM 運行時數據區域的使用?
參考答案:可以從以下幾個方面進行優化:
-
- 對于 Java 堆:合理設置堆的大小,避免堆空間過大導致內存浪費或過小導致頻繁的垃圾回收。可以根據應用程序的特點,調整新生代和老年代的比例。
- 對于方法區:避免加載過多不必要的類,及時卸載不再使用的類。可以通過設置合適的方法區大小,避免方法區內存溢出。
- 對于 Java 虛擬機棧:合理控制方法調用的深度,避免遞歸調用過深導致 StackOverflowError。可以適當調整棧的大小。
- 對于直接內存:合理使用 DirectByteBuffer,避免過度分配直接內存。在使用完后,及時釋放直接內存資源。
- 問題:請舉例說明 JVM 運行時數據區域在多線程環境下的應用和可能遇到的問題。
參考答案:在多線程環境下,每個線程都有自己獨立的程序計數器、Java 虛擬機棧和本地方法棧,這些線程私有區域保證了線程之間的獨立性。例如,多個線程同時執行不同的方法時,每個線程的棧幀操作互不干擾。而 Java 堆和方法區是線程共享的,多個線程可能會同時訪問和修改堆中的對象和方法區中的類信息。可能遇到的問題包括:
-
- 線程安全問題:多個線程同時訪問和修改堆中的對象時,可能會導致數據不一致的問題,需要使用同步機制來保證線程安全。
- 內存泄漏問題:如果線程持有對堆中對象的引用,而這些對象不再使用,但線程沒有及時釋放這些引用,可能會導致內存泄漏。
- 競爭問題:多個線程同時競爭方法區中的類加載鎖時,可能會導致性能下降。