文章目錄
- 第一部分:JVM基礎概念與架構
- JVM是什么?
- JVM整體架構
- 運行時數據區
- 類加載機制
- 執行引擎
- 第二部分:Java內存模型(JMM)
- 什么是Java內存模型
- JMM的核心問題
- 主內存與工作內存
- 內存間交互操作
- 重排序與happens-before原則
- volatile關鍵字
- synchronized關鍵字
- 第三部分:Java 8到Java 21的JVM和JMM演進
- Java 8到Java 11的變化
- JVM架構變化
- JMM相關變化
- Java 11到Java 17的變化
- JVM架構變化
- JMM相關變化
- Java 17到Java 21的變化
- JVM架構變化
- JMM相關結構化并發
- 第四部分:垃圾回收器詳解
- G1垃圾回收器
- 核心特性
- G1的工作流程
- 適用場景
- 調優參數
- ZGC垃圾回收器
- 核心特性
- ZGC的工作流程
- 適用場景
- 調優參數
- Shenandoah垃圾回收器
- 核心特性
- Shenandoah的工作流程
- 適用場景
- 調優參數
- 垃圾回收器性能對比
- 停頓時間對比
- 吞吐量對比
- 內存開銷對比
- 第五部分:虛擬線程與結構化并發
- 虛擬線程簡介
- 虛擬線程與平臺線程的區別
- 虛擬線程的使用場景
- 虛擬線程的使用示例
- 結構化并發API
- 核心概念
- 結構化并發的優勢
- 結構化并發的使用示例
- 第六部分:面試常見問題與回答
- Q1: G1、ZGC和Shenandoah的主要區別是什么?
- Q2: 為什么ZGC能實現低于10ms的停頓時間?
- Q3: G1收集器的"可預測停頓"是如何實現的?
- Q4: Java 17相比Java 8在GC方面有哪些重要改進?
- Q5: 什么是雙親委派模型?如何破壞雙親委派模型?
- Q6: 什么是JIT編譯器?它如何提高Java程序的性能?
- Q7: Java內存模型中的happens-before原則是什么?
- Q8: 虛擬線程與傳統線程的區別是什么?
- Q9: 元空間與永久代的區別是什么?
- Q10: 如何選擇合適的垃圾回收器?
- 總結
- 參考資料
在Java開發者的職業生涯中,理解JVM(Java虛擬機)和JMM(Java內存模型)是進階的必經之路。無論是日常開發還是面試,這都是繞不開的話題。本文將從實用角度出發,帶你深入了解JVM和JMM的核心概念,并對比Java 8、Java 11、Java 17和Java 21各版本的實現差異,幫助大家更好的了解JVM和JMM
第一部分:JVM基礎概念與架構
JVM是什么?
簡單來說,JVM是一個抽象的計算機,它提供了一個平臺無關的執行環境,讓Java程序可以"一次編寫,到處運行"。當我們編寫Java代碼時,編譯器會將源代碼(.java文件)編譯成字節碼(.class文件),而JVM則負責解釋執行這些字節碼。
這種設計帶來了兩個關鍵優勢:
- 平臺獨立性:Java程序可以在任何安裝了JVM的設備上運行
- 安全性:JVM提供了一個受控的執行環境,限制了程序對系統資源的直接訪問
JVM整體架構
JVM的架構可以分為三大部分:
- 類加載子系統:負責加載、鏈接和初始化類
- 運行時數據區:JVM內存模型,包括方法區、堆、Java棧、本地方法棧和程序計數器
- 執行引擎:包括即時編譯器(JIT)、解釋器和垃圾回收器
運行時數據區
JVM的內存區域劃分是面試的高頻考點,包括:
-
程序計數器:
- 當前線程執行的字節碼指令地址
- 線程私有,是唯一不會發生OOM的內存區域
-
Java虛擬機棧:
- 線程私有,生命周期與線程相同
- 每個方法執行時會創建一個棧幀,包含局部變量表、操作數棧、動態鏈接和方法出口等信息
- 可能拋出StackOverflowError和OutOfMemoryError
-
本地方法棧:
- 為本地(Native)方法服務
- 也可能拋出StackOverflowError和OutOfMemoryError
-
Java堆:
- 線程共享,存儲對象實例
- 垃圾收集器管理的主要區域,也稱為"GC堆"
- 可能拋出OutOfMemoryError
-
方法區:
- 存儲已被虛擬機加載的類信息、常量、靜態變量等
- 在HotSpot虛擬機中,JDK8前稱為"永久代",JDK8后稱為"元空間"
- 可能拋出OutOfMemoryError
類加載機制
類加載過程分為三個階段:
- 加載:查找并加載類的二進制數據
- 鏈接:
- 驗證:確保加載的類符合JVM規范
- 準備:為類的靜態變量分配內存并設置初始值
- 解析:將符號引用轉換為直接引用
- 初始化:執行類構造器
<clinit>()
方法
類加載器遵循三個重要原則:
- 委托機制:先讓父加載器嘗試加載
- 可見性:子加載器可以看到父加載器加載的類,反之不行
- 唯一性:同一個類(由類名和加載器共同決定)只會被加載一次
JVM提供了三種內置的類加載器:
-
啟動類加載器(Bootstrap ClassLoader):
- 負責加載Java核心類庫
- 由C++實現,是JVM的一部分
-
擴展類加載器(Extension ClassLoader):
- 負責加載Java擴展類庫
- JDK 9后改名為平臺類加載器(Platform ClassLoader)
-
應用類加載器(Application ClassLoader):
- 負責加載應用程序classpath下的類
執行引擎
執行引擎是JVM的核心組件,負責執行字節碼指令:
- 解釋器:逐條解釋執行字節碼指令
- JIT編譯器:將熱點代碼編譯成本地機器碼,提高執行效率
- 垃圾回收器:自動回收不再使用的內存
JVM采用混合模式執行:
- 解釋執行:啟動快,執行慢
- 編譯執行:啟動慢,執行快
通過熱點代碼探測,JVM能夠在解釋執行和編譯執行之間取得平衡,實現"解釋+編譯"的混合執行模式。
第二部分:Java內存模型(JMM)
什么是Java內存模型
Java內存模型(Java Memory Model,簡稱JMM)是Java虛擬機規范中定義的一種內存模型,用于屏蔽各種硬件和操作系統的內存訪問差異,讓Java程序在各種平臺下都能達到一致的內存訪問效果。
JMM不是真實存在的物理內存區域,而是一種抽象概念,是一組規范,定義了程序中各個變量的訪問方式。
JMM的核心問題
JMM主要解決了三個核心問題:
- 可見性:一個線程對共享變量的修改,能夠及時被其他線程看到
- 原子性:一個操作或多個操作要么全部執行并且不會被中斷,要么都不執行
- 有序性:程序執行的順序按照代碼的先后順序執行
主內存與工作內存
在JMM中,所有的變量都存儲在主內存(Main Memory)中,每個線程還有自己的工作內存(Working Memory):
- 主內存:所有線程共享的內存區域,存儲所有變量的主副本
- 工作內存:每個線程私有的內存區域,存儲該線程使用到的變量的副本
線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量。不同線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主內存來完成。
內存間交互操作
JMM定義了8種操作來完成主內存和工作內存的交互:
- lock(鎖定):作用于主內存的變量,把變量標識為一條線程獨占狀態
- unlock(解鎖):作用于主內存的變量,釋放變量的鎖定狀態
- read(讀取):作用于主內存的變量,把變量的值從主內存傳輸到線程的工作內存
- load(載入):作用于工作內存的變量,把read操作從主內存中得到的變量值放入工作內存的變量副本中
- use(使用):作用于工作內存的變量,把工作內存中的變量值傳遞給執行引擎
- assign(賦值):作用于工作內存的變量,把執行引擎接收到的值賦給工作內存的變量
- store(存儲):作用于工作內存的變量,把工作內存中的變量值傳送到主內存中
- write(寫入):作用于主內存的變量,把store操作從工作內存中得到的變量值放入主內存的變量中
重排序與happens-before原則
為了提高性能,編譯器和處理器常常會對指令進行重排序,分為三種類型:
- 編譯器優化重排序:編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序
- 指令級并行重排序:現代處理器采用了指令級并行技術來將多條指令重疊執行
- 內存系統重排序:由于處理器使用緩存和讀/寫緩沖區,使得加載和存儲操作看上去可能是在亂序執行
這些重排序可能導致多線程程序出現內存可見性問題。為此,JMM定義了happens-before原則,用于描述操作之間的內存可見性。如果一個操作happens-before另一個操作,那么第一個操作的結果對第二個操作可見。
主要的happens-before規則包括:
- 程序順序規則:一個線程中的每個操作,happens-before于該線程中的任意后續操作
- 監視器鎖規則:對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖
- volatile變量規則:對一個volatile變量的寫,happens-before于任意后續對這個volatile變量的讀
- 傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C
- 線程啟動規則:Thread對象的start()方法happens-before于該線程的任何動作
- 線程終止規則:線程中的所有操作都happens-before于其他線程檢測到該線程已經終止
volatile關鍵字
volatile是Java提供的最輕量級的同步機制,它保證了:
- 可見性:對一個volatile變量的寫,對其他線程立即可見
- 有序性:禁止指令重排序優化
volatile的內存語義:
- 當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值刷新到主內存
- 當讀一個volatile變量時,JMM會把該線程對應的本地內存置為無效,從主內存中讀取共享變量
volatile適用于一寫多讀的場景,但不保證原子性。例如,volatile++操作不是原子的,因為它包含讀取、計算、寫入三個步驟。
synchronized關鍵字
synchronized是Java中的重量級鎖,它可以保證被修飾的代碼塊或方法在同一時刻只能被一個線程執行,從而保證了:
- 原子性:確保多個操作作為一個整體不可分割
- 可見性:synchronized的可見性是由"對一個變量執行unlock操作之前,必須先把此變量同步回主內存中(執行store、write操作)"這條規則獲得的
- 有序性:synchronized保證了一個變量在同一個時刻只能由一個線程對其進行lock操作
synchronized的內存語義:
- 進入synchronized塊時,會清空工作內存中的共享變量值,從主內存中重新讀取
- 退出synchronized塊時,會將工作內存中的共享變量值刷新到主內存中
第三部分:Java 8到Java 21的JVM和JMM演進
Java 8到Java 11的變化
JVM架構變化
-
永久代到元空間的轉變
- Java 8 將永久代(PermGen)完全移除,取而代之的是元空間(Metaspace)
- 元空間使用本地內存而非JVM堆內存,默認情況下可以動態增長,減少了"java.lang.OutOfMemoryError: PermGen space"錯誤
- 類的元數據信息放到本地內存,常量池和靜態變量放到Java堆中
-
默認垃圾收集器變更
- Java 8 默認使用Parallel垃圾收集器
- Java 11 默認使用G1垃圾收集器,提供更好的延遲可預測性和更高的吞吐量
-
G1收集器的改進
- Java 11 中G1收集器實現了并行Full GC,大幅提高了Full GC的性能
- 引入了基于NUMA感知的內存分配優化
-
統一JVM日志系統
- Java 9 引入,Java 11 完善了統一的JVM日志系統
- 提供了一致的命令行選項和日志輸出格式,便于問題診斷
JMM相關變化
-
變量句柄(VarHandle)
- Java 9 引入,Java 11 完善
- 提供比反射更精細的內存訪問控制,支持原子操作和內存屏障
- 可以替代Unsafe類的部分功能,提供類型安全的方式訪問對象字段和數組元素
-
內存管理優化
- Java 10 開始,JVM能夠識別容器的內存限制(如Docker容器)
- 改進了內存分配策略,更好地適應現代云環境
Java 11到Java 17的變化
JVM架構變化
-
ZGC的引入與改進
- Java 11 引入ZGC作為實驗性功能
- Java 15 將ZGC從實驗特性轉為產品特性
- Java 17 進一步優化ZGC,提供更低的延遲和更高的吞吐量
- ZGC支持并發類卸載、并發堆收縮,大幅減少停頓時間
-
Shenandoah GC
- Java 12 引入Shenandoah GC作為實驗性功能
- Java 15 將Shenandoah GC從實驗特性轉為產品特性
- 專注于低停頓時間,適用于對響應時間敏感的應用
-
CMS收集器的移除
- Java 14 正式移除了CMS(Concurrent Mark Sweep)垃圾收集器
-
彈性元空間
- Java 16 引入彈性元空間能力
- 更高效地將未使用的HotSpot類元數據(即元空間)內存返回給操作系統
- 減少了元空間內存占用,提高了內存利用率
JMM相關變化
-
增強的同步機制
- Java 15 引入了改進的偏向鎖實現
- Java 16 開始棄用偏向鎖,并在Java 18中完全移除
-
內存管理優化
- 改進了G1和ZGC的內存回收策略
- 優化了對大對象和長壽命對象的處理
Java 17到Java 21的變化
JVM架構變化
-
虛擬線程的引入
- Java 19 引入虛擬線程作為預覽特性
- Java 21 正式發布虛擬線程
- 虛擬線程是輕量級線程實現,由JVM而不是操作系統管理
- 支持高吞吐量的并發應用,可以創建數百萬個虛擬線程
-
GC改進
- 進一步優化ZGC和G1收集器
- 改進了垃圾收集器的并行和并發能力
- 降低了Full GC的頻率和停頓時間
-
JIT編譯器優化
- 改進了即時編譯器的優化策略
- 增強了逃逸分析和內聯優化
- 提高了代碼執行效率
JMM相關結構化并發
-
結構化并發API
- Java 19 引入結構化并發API作為預覽特性
- Java 21 繼續改進結構化并發API
- 提供了更簡單、更可靠的并發編程模型
- 更好地支持異步編程和錯誤處理
-
外部內存訪問API
- Java 14 引入外部內存訪問API作為孵化器特性
- Java 21 將其作為正式特性發布
- 允許Java程序安全高效地訪問Java堆外的內存
第四部分:垃圾回收器詳解
G1垃圾回收器
G1(Garbage-First)是一種面向服務端應用的垃圾回收器,目標是替代CMS。
核心特性
- 分區(Region)設計:將堆內存劃分為多個大小相等的區域,每個區域可以是Eden、Survivor、Old或Humongous區域
- 可預測的停頓時間:通過
-XX:MaxGCPauseMillis
參數設置目標停頓時間 - 混合收集:既有年輕代收集,也有老年代收集
- 增量收集:不需要一次性收集整個堆,而是選擇部分區域進行收集
G1的工作流程
- 初始標記(Initial Marking):標記GC Roots能直接關聯到的對象,需要STW(Stop The World)
- 并發標記(Concurrent Marking):對堆中對象進行可達性分析,與應用線程并發執行
- 最終標記(Final Marking):處理并發標記階段遺留的少量SATB(Snapshot At The Beginning)記錄,需要STW
- 篩選回收(Live Data Counting and Evacuation):對各個Region的回收價值和成本進行排序,根據用戶期望的停頓時間制定回收計劃,需要STW
適用場景
- 需要較低GC延遲的大型應用(堆內存4GB-32GB)
- 電商、金融等對響應時間敏感的在線業務系統
- 多核CPU環境
調優參數
-XX:+UseG1GC // 使用G1垃圾收集器
-XX:MaxGCPauseMillis=200 // 設置目標最大停頓時間為200毫秒
-XX:InitiatingHeapOccupancyPercent=45 // 設置觸發標記周期的堆占用率閾值
-XX:G1NewSizePercent=5 // 設置年輕代最小占堆的百分比
-XX:G1MaxNewSizePercent=60 // 設置年輕代最大占堆的百分比
ZGC垃圾回收器
ZGC(Z Garbage Collector)是一款低延遲垃圾收集器,目標是在任何堆大小下都能將停頓時間控制在10ms以內。
核心特性
- 并發處理:幾乎所有GC操作都與應用線程并發執行,包括標記、轉移和重定位
- 基于區域的內存管理:類似G1,但區域大小動態可變(2MB-4TB)
- 標記-復制算法:使用復制算法進行內存整理
- 顏色指針:利用64位指針中的一些位作為標記位,實現快速對象定位和并發轉移
- 支持超大堆:最高支持16TB的堆內存
ZGC的工作流程
- 并發標記:標記存活對象,與應用線程并發執行
- 并發轉移準備:選擇要清理的區域,與應用線程并發執行
- 并發轉移:將存活對象從選定區域復制到新區域,與應用線程并發執行
- 并發重映射:更新引用,指向對象的新位置,與應用線程并發執行
適用場景
- 對延遲極為敏感的應用(如金融交易、游戲服務器)
- 大內存應用(堆內存>32GB)
- 需要穩定響應時間的實時系統
調優參數
-XX:+UseZGC // 使用ZGC垃圾收集器
-XX:ZAllocationSpikeTolerance=2 // 設置內存分配峰值容忍度
-XX:+UnlockExperimentalVMOptions // 解鎖實驗性VM選項(Java 15之前需要)
-XX:ConcGCThreads=2 // 設置并發GC線程數
Shenandoah垃圾回收器
Shenandoah是一款低延遲垃圾收集器,與ZGC類似,但實現方式不同。
核心特性
- 并發整理:與ZGC類似,支持并發標記和并發整理
- Brooks轉發指針:每個對象都有一個額外的引用字段,指向對象的當前位置
- 不分代:默認不使用分代收集
- 連接矩陣:替代G1的記憶集,降低內存開銷
Shenandoah的工作流程
- 初始標記:標記GC Roots直接引用的對象,需要STW
- 并發標記:遞歸標記整個對象圖,與應用線程并發執行
- 最終標記:處理剩余的SATB緩沖區,需要STW
- 并發清理:回收確定為垃圾的區域,與應用線程并發執行
- 并發整理:將存活對象復制到新的內存區域,與應用線程并發執行
適用場景
- 需要低延遲但內存不是特別大的應用(堆內存8GB-32GB)
- 對延遲敏感的Web應用和服務
- 混合工作負載的應用
調優參數
-XX:+UseShenandoahGC // 使用Shenandoah垃圾收集器
-XX:ShenandoahGCHeuristics=adaptive // 設置啟發式策略
-XX:+UnlockExperimentalVMOptions // 解鎖實驗性VM選項(Java 15之前需要)
-XX:ConcGCThreads=2 // 設置并發GC線程數
垃圾回收器性能對比
停頓時間對比
- Parallel GC:百毫秒級,與堆大小成正比
- G1 GC:目標通常為100-200毫秒,但可能出現偶發的長時間停頓
- Shenandoah:通常在10-100毫秒之間,與堆大小關系較小
- ZGC:通常小于10毫秒,幾乎與堆大小無關
吞吐量對比
- Parallel GC:最高(約99%)
- G1 GC:中高(約95-98%)
- Shenandoah:中等(約90-95%)
- ZGC:中等(約90-95%),但在Java 17后有顯著提升
內存開銷對比
- Parallel GC:最低
- G1 GC:中等(記憶集占用額外內存)
- Shenandoah:中高(Brooks轉發指針占用額外內存)
- ZGC:中高(顏色指針機制占用額外內存)
更詳細的介紹可以參考另一篇博文
第五部分:虛擬線程與結構化并發
虛擬線程簡介
虛擬線程是Java 21引入的重要特性,它是一種輕量級線程實現,由JVM而不是操作系統管理。虛擬線程的設計目標是提高應用程序的吞吐量,特別是在處理大量并發任務時。
虛擬線程與平臺線程的區別
- 創建成本:虛擬線程的創建成本遠低于平臺線程
- 內存占用:虛擬線程的內存占用很小,可以創建數百萬個
- 調度方式:虛擬線程由JVM調度,而平臺線程由操作系統調度
- 阻塞行為:虛擬線程阻塞時不會阻塞底層的平臺線程
虛擬線程的使用場景
- 高并發服務器:處理大量并發連接
- 微服務架構:每個請求使用一個虛擬線程處理
- 異步編程:簡化異步編程模型,使代碼更易于理解和維護
虛擬線程的使用示例
// 創建并啟動一個虛擬線程
Thread vThread = Thread.startVirtualThread(() -> {System.out.println("Running in a virtual thread");
});// 使用虛擬線程執行器
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {IntStream.range(0, 10_000).forEach(i -> {executor.submit(() -> {// 任務代碼return i;});});
}
結構化并發API
結構化并發API是Java 19引入的預覽特性,在Java 21中繼續改進。它提供了一種更簡單、更可靠的并發編程模型,特別適合與虛擬線程配合使用。
核心概念
- 結構化任務作用域:定義任務的生命周期和邊界
- 子任務:在作用域內創建的任務
- 作用域關閉:確保所有子任務在作用域關閉前完成
結構化并發的優勢
- 簡化錯誤處理:子任務的異常會傳播到父作用域
- 自動取消:當一個子任務失敗時,其他子任務會被自動取消
- 避免資源泄漏:確保所有子任務在作用域關閉前完成或取消
- 提高代碼可讀性:使并發代碼的結構更加清晰
結構化并發的使用示例
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {Future<String> user = scope.fork(() -> findUser(userId));Future<Integer> order = scope.fork(() -> fetchOrder(orderId));scope.join(); // 等待所有任務完成scope.throwIfFailed(); // 如果有任務失敗,拋出異常// 使用結果processUserOrder(user.resultNow(), order.resultNow());
}
第六部分:面試常見問題與回答
Q1: G1、ZGC和Shenandoah的主要區別是什么?
回答:
- G1采用分區設計,兼顧吞吐量和停頓時間,適合中大型堆內存應用
- ZGC專注于極低延遲,使用顏色指針技術,適合大內存和對延遲極為敏感的應用
- Shenandoah也專注于低延遲,使用Brooks轉發指針,適合中等內存和混合工作負載
Q2: 為什么ZGC能實現低于10ms的停頓時間?
回答:ZGC通過顏色指針技術和并發處理實現極低停頓。顏色指針利用64位指針中的一些位作為標記位,使得對象定位和轉移能夠并發執行,幾乎所有GC操作(包括標記、轉移和重定位)都與應用線程并發進行,只有極少數操作需要STW。
Q3: G1收集器的"可預測停頓"是如何實現的?
回答:G1通過以下機制實現可預測停頓:
- 將堆劃分為多個大小相等的區域(Region)
- 維護每個區域的垃圾密度信息
- 根據用戶設置的目標停頓時間(-XX:MaxGCPauseMillis)
- 在回收時優先選擇回收價值最高(垃圾最多)的區域
- 動態調整年輕代大小和回收區域數量,以接近目標停頓時間
Q4: Java 17相比Java 8在GC方面有哪些重要改進?
回答:
- 默認GC從Parallel變為G1,更注重延遲而非單純吞吐量
- 引入并完善了ZGC和Shenandoah兩款低延遲收集器
- 移除了老舊的CMS收集器
- G1收集器得到多項優化,包括并行Full GC、NUMA感知等
- 引入彈性元空間,優化了元空間的內存管理
- 改進了GC的日志和監控能力
Q5: 什么是雙親委派模型?如何破壞雙親委派模型?
回答:雙親委派模型是指當一個類加載器收到類加載請求時,它首先不會自己嘗試加載這個類,而是把這個請求委派給父類加載器去完成。只有當父加載器無法完成加載請求時,子加載器才會嘗試自己加載。
破壞雙親委派模型的方式:
- 重寫
loadClass()
方法:直接覆蓋loadClass()
方法,不再調用父類加載器 - 使用線程上下文類加載器:通過
Thread.currentThread().getContextClassLoader()
獲取上下文類加載器,打破了類加載器的層次結構 - 使用OSGi等模塊化系統:OSGi實現了自己的類加載器架構,允許同一個類在不同的模塊中加載
Q6: 什么是JIT編譯器?它如何提高Java程序的性能?
回答:JIT(Just-In-Time)編譯器是JVM的一個組件,它在運行時將熱點代碼(頻繁執行的代碼)編譯成本地機器碼,從而提高執行效率。
JIT編譯器提高性能的方式:
- 熱點代碼識別:通過計數器或采樣識別頻繁執行的代碼
- 即時編譯:將字節碼編譯成優化的本地機器碼
- 內聯優化:將方法調用直接內聯到調用點,減少方法調用開銷
- 逃逸分析:分析對象的作用域,優化內存分配和同步
- 循環優化:提取循環不變量,減少計算量
- 分層編譯:結合解釋執行和不同級別的編譯優化,平衡啟動速度和峰值性能
Q7: Java內存模型中的happens-before原則是什么?
回答:happens-before是Java內存模型中的一個核心概念,用于描述操作之間的內存可見性。如果操作A happens-before操作B,那么A的結果對B可見。
主要的happens-before規則包括:
- 程序順序規則:一個線程中的每個操作,happens-before于該線程中的任意后續操作
- 監視器鎖規則:對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖
- volatile變量規則:對一個volatile變量的寫,happens-before于任意后續對這個volatile變量的讀
- 線程啟動規則:Thread對象的start()方法happens-before于該線程的任何動作
- 線程終止規則:線程中的所有操作都happens-before于其他線程檢測到該線程已經終止
- 傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C
Q8: 虛擬線程與傳統線程的區別是什么?
回答:
- 實現方式:虛擬線程由JVM實現和管理,傳統線程(平臺線程)由操作系統實現和管理
- 資源消耗:虛擬線程非常輕量,可以創建數百萬個;平臺線程較重,通常只能創建數千個
- 調度方式:虛擬線程使用協作式調度,平臺線程使用搶占式調度
- 阻塞行為:虛擬線程阻塞時會讓出底層的載體線程,而不會阻塞操作系統線程
- 棧大小:虛擬線程的棧可以動態增長,平臺線程的棧大小是固定的
- 適用場景:虛擬線程適合IO密集型任務,平臺線程適合CPU密集型任務
Q9: 元空間與永久代的區別是什么?
回答:
- 存儲位置:永久代位于JVM堆內存中,而元空間使用本地內存(Native Memory)
- 內存限制:永久代有固定大小限制,元空間默認可以動態增長,只受系統內存限制
- 垃圾回收:元空間的垃圾回收效率更高,主要針對類元數據的回收
- OOM風險:永久代容易發生"PermGen space"的OOM錯誤,元空間降低了這種風險
- 內容存儲:永久代存儲類元數據、常量池和靜態變量,元空間只存儲類元數據,常量池和靜態變量移到了堆中
Q10: 如何選擇合適的垃圾回收器?
回答:選擇垃圾回收器應考慮以下因素:
-
應用特性:
- 批處理、離線計算類應用:選擇Parallel GC,獲得最高吞吐量
- 一般在線業務系統:選擇G1 GC,平衡吞吐量和延遲
- 對延遲敏感的在線服務:選擇ZGC或Shenandoah,獲得更低的延遲
-
內存大小:
- 小內存(<4GB):Parallel GC或G1 GC
- 中等內存(4GB-32GB):G1 GC或Shenandoah
- 大內存(>32GB):ZGC
-
Java版本:
- Java 8:Parallel GC(默認)或CMS
- Java 11:G1 GC(默認)或嘗試實驗性的ZGC
- Java 17及以上:G1 GC(默認)、ZGC或Shenandoah(均為產品特性)
總結
JVM和JMM是Java開發者必須掌握的核心知識,它們直接影響著Java程序的性能、穩定性和可靠性。從Java 8到Java 21,JVM和JMM經歷了多次重要更新,包括永久代到元空間的轉變、垃圾回收器的演進、虛擬線程的引入等。
在實際開發和面試中,理解這些概念和變化不僅有助于編寫高質量的Java代碼,還能幫助我們更好地進行性能調優和問題排查。希望本文能夠幫助你在技術面試中脫穎而出,同時也能在日常工作中更加得心應手地處理JVM相關問題。
參考資料
- Oracle官方文檔:JDK 11 Release Notes
- Oracle官方文檔:JDK 17 Release Notes
- OpenJDK Wiki:ZGC
- Oracle官方文檔:Java Language Changes
- 《深入理解Java虛擬機》第3版,周志明著
- 《Java并發編程的藝術》,方騰飛、魏鵬、程曉明著