目錄
- 1. 什么是 JVM、JDK 和 JRE?它們之間的關系是什么?
- 2. Java 內存區域(運行時數據區)有哪些?
- 3. 說說你對 JVM 垃圾回收機制的理解。
- 4. 常用的垃圾回收算法有哪些?
- 5. 什么是 Minor GC、Major GC 和 Full GC?
- 6. JVM 調優的常用參數有哪些?
- 7. 說說 Java 對象的創建過程。
- 8. 什么是 JVM 類加載機制?
- 9. 什么是雙親委派模型?
- 10. 常見的 JVM 垃圾回收器有哪些?
- 11. 為什么說 CMS 會產生內存碎片?
- 12. 什么是 JIT 編譯器?它的作用是什么?
- 13. 什么是逃逸分析?
- 14. 談談你對強引用、軟引用、弱引用、虛引用的理解。
- 15. 什么是 OOM(Out of Memory)?如何排查?
- 16. JVM 發生 GC 時,STW(Stop-The-World)是什么?
- 17. 為什么說 JVM 堆是分代的?
- 18. 對象在 JVM 中的內存布局是怎樣的?
- 19. JVM 中的線程死鎖如何排查?
- 20. 簡述 JVM 的執行引擎。
JVM(Java Virtual Machine)作為 Java 語言的核心,是每個 Java 開發者都繞不開的話題。在面試中,JVM 相關問題幾乎是必考項。本文整理了 20 道常見的 JVM 面試題,并附帶詳細解答,幫助你更好地準備面試。
1. 什么是 JVM、JDK 和 JRE?它們之間的關系是什么?
解答:
- JVM(Java Virtual Machine):Java 虛擬機,是運行 Java 字節碼的虛擬機。它負責將編譯好的
.class
文件翻譯成機器碼并執行。JVM 只是一個規范,不同的廠商可以有不同的實現,比如 HotSpot。 - JRE(Java Runtime Environment):Java 運行時環境,它包含了 JVM 和運行 Java 程序所需的核心類庫(如
java.lang
、java.util
等)。如果你只需要運行一個 Java 程序,安裝 JRE 就足夠了。 - JDK(Java Development Kit):Java 開發工具包,是提供給 Java 開發人員使用的,它包含了 JRE、編譯器(
javac
)、調試工具(jdb
)等開發工具。如果你需要編寫和編譯 Java 程序,必須安裝 JDK。
關系:
JDK > JRE > JVM。JDK 包含 JRE,而 JRE 包含 JVM 和核心類庫。
2. Java 內存區域(運行時數據區)有哪些?
解答:
根據《Java 虛擬機規范》,Java 虛擬機運行時數據區分為以下幾個部分:
- 程序計數器(Program Counter Register):一塊較小的內存空間,是當前線程所執行的字節碼的行號指示器。每個線程都有獨立的程序計數器,它是線程私有的。
- Java 虛擬機棧(Java Virtual Machine Stacks):每個方法在執行時都會創建一個棧幀,用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。線程私有。
- 本地方法棧(Native Method Stacks):與虛擬機棧類似,但是為虛擬機使用到的 Native 方法服務。線程私有。
- Java 堆(Java Heap):虛擬機所管理的內存中最大的一塊。所有線程共享,用于存放對象實例和數組。它是垃圾回收的主要區域。
- 方法區(Method Area):用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。所有線程共享。在 JDK 1.8 之后,方法區被 元空間(Metaspace) 取代,元空間在本地內存中,不受 JVM 內存限制。
3. 說說你對 JVM 垃圾回收機制的理解。
解答:
垃圾回收(Garbage Collection, GC)是 JVM 自動管理內存的一種機制。它的主要任務是回收堆內存中不再使用的對象,釋放內存空間。
GC 的基本思想是:找到那些不再被任何引用所指向的對象,然后將其占用的內存回收。為了判斷對象是否“存活”,JVM 采用了兩種主要算法:
- 引用計數算法:給每個對象添加一個引用計數器。當有地方引用它時,計數器加 1;引用失效時,計數器減 1。當計數器為 0 時,說明該對象可以被回收。但是,它無法解決對象之間循環引用的問題,所以現代 JVM 不使用此算法。
- 可達性分析算法:通過一系列稱為 “GC Roots” 的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain)。當一個對象到 GC Roots 沒有任何引用鏈相連時,則證明此對象是不可用的。GC Roots 包括虛擬機棧中的引用對象、方法區中的靜態變量和常量等。
4. 常用的垃圾回收算法有哪些?
解答:
-
標記-清除(Mark-Sweep):
- 標記:從 GC Roots 開始標記所有可達對象。
- 清除:遍歷整個堆,回收所有未被標記的對象。
- 缺點:會產生大量不連續的內存碎片,導致后續需要大塊連續內存的對象無法分配,提前觸發 GC。
-
復制(Copying):
- 將內存分為大小相等的兩塊,每次只使用其中一塊。當這塊內存用完時,將存活的對象復制到另一塊上,然后清空已使用的這塊內存。
- 優點:不會產生內存碎片,實現簡單高效。
- 缺點:內存利用率只有 50%。常用于新生代。
-
標記-整理(Mark-Compact):
- 標記:同標記-清除,標記所有存活對象。
- 整理:讓所有存活對象都向一端移動,然后直接清理掉邊界以外的內存。
- 優點:不會產生內存碎片。
- 缺點:效率比標記-清除低,因為需要移動對象。常用于老年代。
-
分代收集:
- 結合了上述算法,根據對象的生命周期將堆分為新生代和老年代。
- 新生代:大部分對象“朝生夕滅”,采用復制算法,效率高。
- 老年代:對象存活率高,采用標記-整理或標記-清除算法,減少移動開銷。
5. 什么是 Minor GC、Major GC 和 Full GC?
解答:
-
Minor GC(新生代 GC):
- 指發生在新生代的垃圾回收。
- 新生代采用復制算法,因為對象存活率低,效率高。
- 觸發條件:Eden 區滿時。
-
Major GC(老年代 GC):
- 指發生在老年代的垃圾回收。
- Major GC 通常會伴隨一次 Minor GC。
-
Full GC(全堆 GC):
- 指對整個堆(新生代、老年代和方法區/元空間)進行垃圾回收。
- 觸發條件:老年代空間不足;方法區空間不足;調用
System.gc()
等。 - Full GC 的代價很高,會造成較長的 STW(Stop-The-World),應盡量避免。
6. JVM 調優的常用參數有哪些?
解答:
-Xms<size>
:設置 JVM 的初始堆內存,等價于-XX:InitialHeapSize
。-Xmx<size>
:設置 JVM 的最大堆內存,等價于-XX:MaxHeapSize
。-Xmn<size>
:設置新生代的大小。-XX:NewRatio=<ratio>
:設置新生代和老年代的比例,例如-XX:NewRatio=2
表示新生代與老年代的比例為 1:2。-XX:MaxMetaspaceSize=<size>
:設置元空間的最大大小。-XX:+PrintGCDetails
:打印詳細的 GC 日志。-XX:+UseG1GC
:使用 G1 垃圾回收器。-Xss<size>
:設置每個線程的棧大小。
7. 說說 Java 對象的創建過程。
解答:
- 類加載檢查:當 JVM 遇到
new
指令時,首先檢查指令的參數是否能在常量池中定位到一個類的符號引用,并檢查這個符號引用代表的類是否已被加載、解析和初始化。 - 分配內存:在類加載檢查通過后,為新對象分配內存。
- 初始化零值:內存分配完成后,JVM 會將分配到的內存空間都初始化為零值(不包括對象頭),這保證了對象的實例字段在不賦初值時可以直接使用。
- 設置對象頭:JVM 會設置對象頭中的元數據,比如哈希碼、GC 年齡、鎖信息、對象所屬的類等。
- 執行
<init>
方法:執行對象的構造方法,按照代碼中的邏輯進行初始化。
8. 什么是 JVM 類加載機制?
解答:
類加載機制是 JVM 將 class
文件加載到內存,并對其進行校驗、準備、解析和初始化,最終形成可以被虛擬機直接使用的 Java 類型。
加載過程:
- 加載(Loading):通過類的全限定名獲取二進制字節流,將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構,并在內存中生成一個代表該類的
java.lang.Class
對象。 - 驗證(Verification):確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,不會危害虛擬機自身的安全。
- 準備(Preparation):為類的靜態變量分配內存,并設置默認初始值(例如
int
類型為 0,boolean
類型為false
)。 - 解析(Resolution):將常量池中的符號引用替換為直接引用。
- 初始化(Initialization):執行
<clinit>()
方法,對類的靜態變量和靜態代碼塊進行賦值。
9. 什么是雙親委派模型?
解答:
雙親委派模型(Parent Delegation Model)是 Java 類加載器的一種工作機制。當一個類加載器收到類加載請求時,它并不會自己先去加載,而是先把這個請求委派給它的父類加載器去執行。如果父類加載器還存在父類加載器,則繼續向上委派,直到最頂層的啟動類加載器。只有當父類加載器在它的搜索范圍內找不到所需的類時,子類加載器才會嘗試自己去加載。
優點:
- 避免重復加載:確保每個類在 JVM 中只加載一次。
- 保證安全性:防止惡意代碼替換核心類庫,例如,用戶不能自己寫一個
java.lang.String
類來欺騙 JVM。
10. 常見的 JVM 垃圾回收器有哪些?
解答:
- Serial(串行):單線程 GC,簡單高效,但會造成較長的 STW。適用于單核 CPU 或內存較小的客戶端應用。
- ParNew:Serial 的多線程版本,用于新生代。
- Parallel Scavenge:關注吞吐量(Throughput),即 CPU 用于執行用戶代碼的時間與 GC 時間的比值。可以有效利用多核 CPU。
- CMS(Concurrent Mark Sweep):以獲取最短停頓時間為目標的 GC,采用“標記-清除”算法。在并發階段,GC 線程和用戶線程可以同時運行。
- G1(Garbage First):分代收集器,將堆劃分為一個個的區域(Region),通過維護一個優先列表,優先回收垃圾最多的區域,從而實現可預測的停頓時間。適用于大內存服務器。
- ZGC / Shenandoah:新一代的低延遲垃圾回收器,旨在實現幾乎不中斷的 GC 停頓時間(小于 10ms),適用于超大內存的應用。
11. 為什么說 CMS 會產生內存碎片?
解答:
CMS 垃圾回收器采用了標記-清除算法,這個算法的特點是:
- 標記:遍歷堆,標記所有存活對象。
- 清除:直接清除所有未標記對象占用的內存。
這個過程中,存活對象的位置不會改變,因此被清除的對象所占用的空間就成了不連續的“空洞”,也就是內存碎片。當一個需要大塊連續內存的新對象需要分配時,如果現有空閑內存雖然總量足夠,但是沒有足夠大的連續空間,就會導致分配失敗,從而不得不觸發一次 Full GC。
12. 什么是 JIT 編譯器?它的作用是什么?
解答:
JIT(Just-In-Time)編譯器,又稱即時編譯器,是 HotSpot JVM 中的一個重要組成部分。它的作用是在程序運行時,將頻繁執行的熱點代碼(Hot Spot Code)編譯為本地機器碼,從而提高代碼的執行效率。
Java 程序最初是解釋執行的,即由解釋器逐行翻譯字節碼。JIT 編譯器的出現彌補了這一缺點。當 JVM 發現某段代碼被多次調用或者是一個循環時,就會將其識別為熱點代碼,并交由 JIT 編譯器編譯。編譯后的本地代碼可以直接運行在操作系統上,效率更高。
13. 什么是逃逸分析?
解答:
**逃逸分析(Escape Analysis)**是 JVM 編譯器的一項優化技術,它分析對象是否會被方法外部訪問。
- 不逃逸:一個對象只在方法內部使用,不會被外部引用。
- 方法逃逸:對象作為方法的返回值,或者作為參數傳遞給其他方法。
- 線程逃逸:對象被多個線程共享,例如作為靜態變量或者被添加到公共集合中。
逃逸分析的優化:
- 棧上分配:如果一個對象不逃逸,可以直接在棧上分配內存。棧上的內存隨著方法結束自動回收,減輕了 GC 壓力。
- 同步消除:如果一個對象只在一個線程中使用,即使它被
synchronized
包裹,JVM 也可以消除這個鎖,因為不會發生競爭。 - 標量替換:如果一個對象不逃逸,并且可以拆分為基本類型,那么可以不創建這個對象,直接創建它的字段,節省內存。
14. 談談你對強引用、軟引用、弱引用、虛引用的理解。
解答:
- 強引用(Strong Reference):最常見的引用類型,如
Object obj = new Object()
。只要強引用存在,垃圾回收器永遠不會回收被引用的對象。 - 軟引用(Soft Reference):用于描述一些還有用但非必需的對象。當內存空間不足時,JVM 會回收這些對象。常用于緩存。
- 弱引用(Weak Reference):用于描述那些非必需的對象。只要發生垃圾回收,無論內存是否充足,都會回收被弱引用關聯的對象。常用于
WeakHashMap
。 - 虛引用(Phantom Reference):最弱的引用,無法通過虛引用獲取對象實例。它唯一的用途是,在對象被回收時收到一個系統通知。常用于管理直接內存。
15. 什么是 OOM(Out of Memory)?如何排查?
解答:
OOM 指程序在申請內存時,JVM 沒有足夠的內存空間來分配。
常見的 OOM 類型:
java.lang.OutOfMemoryError: Java heap space
:Java 堆內存不足,可能是創建了太多大對象,或者內存泄漏。java.lang.OutOfMemoryError: Metaspace
:元空間不足,可能是加載了太多類。java.lang.OutOfMemoryError: unable to create new native thread
:無法創建新的本地線程,可能是線程創建過多,或者操作系統對線程數有限制。
排查方法:
- 分析錯誤日志:查看 OOM 錯誤的具體類型和信息。
- 分析堆轉儲文件:使用
jmap
或HeapDumpOnOutOfMemoryError
生成.hprof
文件,然后使用 MAT(Memory Analyzer Tool) 或 VisualVM 等工具分析堆中對象的分布,找出導致 OOM 的“大對象”或對象數量異常增長。 - 查看 GC 日志:通過
PrintGCDetails
等參數打印 GC 日志,分析 GC 頻率和耗時,判斷是否頻繁 GC 導致內存不足。
16. JVM 發生 GC 時,STW(Stop-The-World)是什么?
解答:
**STW(Stop-The-World)**是指在進行垃圾回收時,JVM 停止所有的應用線程,直到 GC 過程結束。所有用戶線程都被暫停,無法響應請求,就像整個世界都停止了一樣。
STW 的目的是為了保證 GC 過程中的數據一致性。如果在 GC 時,用戶線程還在不斷創建新對象、修改引用關系,那么 GC 線程將無法準確地判斷哪些對象是存活的,導致回收錯誤。
現代的垃圾回收器(如 CMS、G1、ZGC)都在努力減少 STW 的時間,甚至實現并發 GC,讓 GC 線程和用戶線程同時運行,從而減少對應用程序的影響。
17. 為什么說 JVM 堆是分代的?
解答:
**分代(Generational)**是 JVM 堆內存的一種管理策略,基于一個重要的假設:絕大多數對象都是“朝生夕滅”的。
- 新生代(Young Generation):用于存放新創建的對象。這里的大多數對象在 Minor GC 后都會被回收。采用復制算法,效率很高。
- 老年代(Old Generation):用于存放經過多次 Minor GC 仍然存活的對象。這些對象生命周期較長。采用標記-整理或標記-清除算法,減少移動開銷。
這種分代管理可以根據不同區域對象的特點,采用最適合的 GC 算法,從而提高 GC 效率。
18. 對象在 JVM 中的內存布局是怎樣的?
解答:
一個 Java 對象在堆內存中主要包含三部分:
-
對象頭(Object Header):
- Mark Word:存儲對象的哈希碼、GC 年齡、鎖信息等。
- Klass Pointer:指向對象所屬類的元數據指針。
-
實例數據(Instance Data):
- 存儲對象的所有成員變量(包括父類的成員變量)。
-
對齊填充(Padding):
- 保證對象的大小是 8 字節的倍數。這不是必需的,只是為了方便 CPU 訪問。
19. JVM 中的線程死鎖如何排查?
解答:
- 使用
jps
命令:找到 Java 進程 ID。 - 使用
jstack
命令:jstack <pid>
。jstack
會打印出 JVM 中所有線程的堆棧信息,如果存在死鎖,它會明確地在日志中報告死鎖信息,并列出涉及死鎖的線程和鎖。 - 分析
jstack
結果:仔細閱讀jstack
的輸出,找到Found one Java-level deadlock
的信息,然后根據堆棧信息分析是哪些線程、哪些鎖導致了死鎖。 - 使用圖形化工具:如 VisualVM,它可以直觀地顯示線程狀態、CPU 使用情況,并能自動分析死鎖。
20. 簡述 JVM 的執行引擎。
解答:
**執行引擎(Execution Engine)**是 JVM 的核心組成部分,它負責執行被加載到內存中的字節碼。執行引擎的工作方式有兩種:
- 解釋執行:由**解釋器(Interpreter)**逐條讀取和翻譯字節碼指令,然后執行。這種方式啟動快,但執行效率低。
- 編譯執行:由 JIT 編譯器將熱點代碼編譯成機器碼。這種方式啟動慢,但一旦編譯完成,執行效率高。
現代的 JVM 普遍采用解釋器和 JIT 編譯器并存的混合模式。程序剛啟動時,解釋器快速執行;當代碼被多次調用后,JIT 編譯器介入,將熱點代碼編譯成高效的本地代碼,以達到最佳性能。