一、JVM的基礎概念
1、概述
????????JVM是 Java 程序的運行基礎環境,是 Java 語言實現 “一次編寫,到處運行”?("write once , run anywhere. ")特性的關鍵組件,具體從以下幾個方面來理解:
概念層面
????????JVM 是一種抽象的計算機規范, 它定義了字節碼文件的執行環境,提供了一套虛擬的硬件架構,包括指令集、寄存器組、棧、堆、方法區等。在這個虛擬環境中,Java 字節碼可以被解釋或編譯成機器碼并執行, 就如同真實的計算機硬件執行機器指令一樣。
作用表現
- 字節碼執行:Java 源文件(.java)經過編譯器(javac)編譯成字節碼文件(.class),JVM 負責加載這些字節碼文件,并通過執行引擎將字節碼解釋或編譯成對應操作系統和硬件平臺能夠識別的機器碼 ,從而讓 Java 程序在不同的系統上都能運行。比如,同樣一份 Java 程序的字節碼,無論是在 Windows、Linux 還是 macOS 系統上,只要安裝了對應的 JVM,都可以正常執行。
- 內存管理:JVM 管理著程序運行時的內存,劃分出程序計數器、Java 虛擬機棧、本地方法棧、方法區、堆等運行時數據區 。它自動進行對象的內存分配(主要在堆上)和垃圾回收(釋放不再使用的對象占用的內存),比如 Java 程序員不需要像 C/C++ 程序員那樣手動釋放內存,減少了內存泄漏和野指針等問題。
- 類加載:JVM 的類加載子系統負責從文件系統、網絡或其他來源加載 Java 類,并且對類進行驗證、準備、解析和初始化等操作,保證類在使用前已經被正確加載和初始化,比如加載第三方庫中的類,以支持程序的功能實現。
與 Java 生態的關系
????????JVM 不只是運行 Java 語言編寫的程序,很多其他語言,如 Kotlin、Groovy、Scala 等,也可以編譯成字節碼在 JVM 上運行, 這使得 JVM 成為一個多語言的運行平臺,構建起龐大的 Java 生態系統,為企業級開發、大數據處理(如基于 JVM 的 Apache Spark)、安卓應用開發(安卓虛擬機基于 JVM 原理定制)等領域提供了強大的支持。
2、JVM的組成
JVM的組成主要包括:類加載器、運行時數據區(堆、虛擬機棧等)、執行引擎和本地方法接口四大部分組成。
執行引擎
負責執行字節碼文件中的指令,它包含以下幾個重要組件:
- 解釋器:可以將字節碼文件逐行解釋執行,但是執行效率相對較低。
- 即時編譯器(JIT):可以將熱點代碼(經常被執行的代碼)編譯成機器碼,下次可直接調用,提高代碼的執行效率。在程序運行過程中,JIT 會監測到熱點代碼,然后對其進行編譯優化。
- 垃圾回收器:負責回收堆中不再使用的對象所占用的內存空間,保證堆內存的有效利用。常見的垃圾回收算法包括標記 - 清除算法、復制算法、標記 - 整理算法、分代收集算法等。?
本地方法接口
????????它是 JVM 調用本地方法(用其他語言,如 C、C++ 編寫的代碼)的接口。通過本地方法接口,Java 程序可以調用底層操作系統的功能,實現與硬件交互等操作。
3、Java程序執行流程:
二、Java類加載機制
1、類加載器:
????????負責加載 .class 文件到內存中,即將編譯生成的字節碼文件加載進 JVM,供后續使用。JVM不會一次性加載所有類。如果一次性加載,那么會占用很多的內存。
類加載過程分為加載、驗證、準備、解析和初始化五個階段:
- 加載:通過類的全限定名獲取定義此類的二進制字節流,將字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構,并在內存中生成一個代表這個類的 java.lang.Class 對象。
- 驗證:確保 .class 文件的字節流符合 JVM 規范,從文件格式、元數據、字節碼邏輯到符號引用層層校驗,過濾非法內容(如格式錯誤、類型不匹配、惡意指令等 ),既保證字節碼能被 JVM 正確解析執行,也避免惡意代碼攻擊、破壞虛擬機運行安全。
- 準備:為類變量分配內存并設置初始值,這些變量所使用的內存都將在方法區中進行分配。
- 解析:將常量池中的符號引用轉換為直接引用的過程。
- 初始化:執行類構造器 <clinit>() 方法,對類變量進行賦值操作和執行靜態代碼塊中的語句。?
2、類加載的時機
2.1 主動引用
虛擬機規范未強制約束“何時加載類”,但嚴格規定以下六種場景必須觸發類的完整加載流程(加載→驗證→準備→解析→初始化前置步驟):
1、字節碼指令觸發(4 種核心場景)
當 JVM 執行以下字節碼指令時,會先加載目標類(若未加載):
- new:創建類實例(如 new User() ,需先加載 User 類 )
- getstatic:訪問非 final 修飾的靜態字段(如 Config.appName ,appName 是靜態變量;若為 final static String APP_NAME = "App" ,常量直接進入運行時常量池,不觸發類加載 )
- putstatic:修改類的靜態字段(如 Counter.count = 1 ,需先加載 Counter 類 )
- invokestatic:調用類的靜態方法(如 StringUtils.isEmpty("") ,需先加載 StringUtils 類 )
2、反射操作觸發
使用 java.lang.reflect 包反射調用類時(如 Class.forName("com.example.User")、User.class.newInstance() ),若類未初始化,則強制觸發加載+初始化流程(Class.forName 默認會初始化類,區別于 ClassLoader.loadClass 僅加載不初始化 )。
3、父類/父接口依賴觸發
加載一個類時,若其直接父類、間接父類或父接口(JDK8+ 含默認方法的接口)未加載,則先觸發父類/父接口的加載。
示例:加載 Student 類(繼承 Person 類,Person 實現 Serializable 接口 )時,若 Person 或 Serializable 未加載,會先加載它們。
4、JVM 啟動主類觸發
虛擬機啟動時,必須加載包含 main() 方法的主類(程序入口類 )。若主類未找到或加載失敗,會拋出 NoSuchMethodError: main 等錯誤。
5、接口默認方法依賴觸發(JDK8+)
當一個接口定義了 default 修飾的默認方法(如 interface A { default void hello() {} } ),若其實現類(如 class B implements A )被加載,則該接口需在實現類之前加載,保證默認方法的字節碼可被解析。
補充說明: JVM 判斷“類是否加載”的依據是全限定名 + 類加載器,雙親委派模型會影響加載優先級,但上述場景屬于“強制觸發加載”的規則,與委派邏輯協同保證類加載的正確性。
2.2 被動引用
- 通過子類引用父類的靜態字段,不會導致子類加載。
public class demo {public static void main(String[] args) {// 子類通過引用父類的靜態字段,不會導致子類加載(父類會加載)System.out.println(Sonclass.value);}
}class Dadclass{static int value = 123;static {System.out.println("Dadclass類被加載。。。。。");}
}class Sonclass extends Dadclass{static {System.out.println("Sonclass類被加載。。。。。");}
}
?運行結果:
- 通過數組定義類引用類,不會觸發此類的加載。該過程會對數組類進行加載,數組類是一個虛擬機自動生成的、直接繼承Object類的子類,其中包含了數組的屬性和方法。
public class demo2 {public static void main(String[] args) {// 通過數組定義來引用類,不會觸發此類的加載Data[] data = new Data[10];}
}class Data{static final int max = 100;static {System.out.println("Data類被加載了。。。。。");}
}
運行結果:
?
- 常量在編譯階段會存入調用類的常量池中,本質上并沒有直接引用到定義常量的類,因此不會觸發定義常量的類的加載
public class demo2 {public static void main(String[] args) {// 引用靜態常量不會觸發類加載System.out.println(Data.max);}
}class Data{static final int max = 100;static {System.out.println("Data類被加載了。。。。。");}
}
?運行結果:
3、類與類加載器
類加載器的分類
- 啟動類加載器:由C/C++實現的JVM原生類加載器,不屬于Java類(無法通過Java代碼直接引用)。加載核心類庫,諸如 java.lang、java.util 等位于jre/lib目錄下的核心jar包(rt.jar)。
- 擴展類加載器 :Java語言實現,繼承自 java.lang.ClassLoader。加載Java擴展類庫,位于 jre/lib/ext 目錄下或系統屬性 java.ext.dirs 指定的路徑下的類。
- 應用程序類加載器 :Java語言實現,繼承自 java.lang.ClassLoader。加載用戶類路徑(classpath)上所指定的類庫,例如:我們自己編寫的類或第三方的 jar 包。
- 自定義類加載器:?在 Java 中,開發者可以繼承 java.lang.ClassLoader 類,重寫 findClass 等方法來自定義類加載器。
4、雙親委派模型
????????類加載器之間的層次關系,稱為雙親委派模型。當一個類加載器收到類加載請求時,會先將請求委派給父加載器(遵循「啟動類加載器→擴展類加載器→應用程序類加載器」的層級,自定義加載器需手動指定父加載器,默認是應用程序類加載器 ),只有父加載器無法加載時,才由當前類加載器嘗試加載。
這一機制確保了:
- Java核心類的安全性(假設自定義一個 `java.lang.Object` 類,當類加載器收到加載請求時,會先委派給父加載器。由于核心類 `java.lang.Object` 屬于啟動類加載器的加載范圍,啟動類加載器會直接加載自己負責的 `rt.jar` 里的 `java.lang.Object` ,不會讓自定義的同名類被加載,從而防止核心類被惡意或錯誤代碼覆蓋)
- 類的唯一性(雙親委派確保核心類只會被啟動類加載器加載,避免不同加載器重復加載、造成類型混亂)?
4.1 JVM判斷兩個類是否為“同一個類”的依據:
- 完全限定名相同
- 由同一個類加載器加載
4.2 通過自定義類加載器打破雙親委派模型
????????自定義類加載器是打破雙親委派模型的常用方式,核心思路是重寫類加載器的 loadClass() 方法,改變 “先委派父加載器” 的默認邏輯:
原理:雙親委派的核心邏輯在 ClassLoader 類的 loadClass() 方法中(先檢查類是否已加載,未加載則委派父加載器,父加載器失敗才自己加載)。通過繼承 ClassLoader 并重寫 loadClass(),可跳過 “委派父加載器” 的步驟,直接由當前類加載器加載指定類,從而打破委派鏈條。
注意:重寫時需謹慎處理核心類(如 java.lang.*),JVM 對核心包有安全校驗(SecurityManager),強制加載自定義核心類可能觸發 SecurityException,確保打破委派的同時不破壞 JVM 基礎安全機制。
5、對象的創建過程
Step1:類加載檢查
當執行 new User() 這樣的代碼時,JVM首先會檢查這個類(User)是否已被加載、鏈接和初始化。
- 如果類未加載,JVM會通過類加載器執行類加載流程(加載→驗證→準備→解析→初始化)。
- 只有類成功加載到元空間后,才能創建其對象。
Step2:分配內存
????????類加載完成后,JVM會為新對象在堆內存中分配一塊內存空間,大小在類加載時已確定。(類的字段、方法等元數據決定對象大小)
內存分配的兩種方式:
- 指針碰撞:當堆內存是連續規整的,JVM 會通過移動指針的方式,直接劃分出對應大小的內存給新對象。
- 空閑列表:若堆內存碎片化,JVM 會維護一張 “空閑內存塊列表”,從中挑選合適大小的內存塊,分配給新對象。
????????至于采用哪種分配方式,取決于 Java 堆內存是否規整。而堆內存是否規整,又由 GC 收集器的算法決定 —— 若用 “標記 - 清除” 算法,會產生內存碎片,堆內存不規整;若用 “標記 - 整理” 算法,會整理內存碎片,讓堆內存恢復規整 。
Step3:初始化零值
內存分配完成后,JVM會將分配到的內存空間(對象的實例字段)初始化零值。(如 int 為0,Object引用為 null 等)
這一步保證了:即使對象未顯式初始化字段,也能訪問到零值(符合Java語言規范)。
Step4:設置對象頭
初始化零值完成后,JVM會在對象內存的對象頭進行必要的設置。
例如:這個對象是哪個類的實例、如何才能找到累的元數據信息、對象的哈希碼和對象的GC分代年齡等信息。
Step5:執行init構造方法
最后,JVM會執行對象的初始化方法:實例字段的顯式初始化→實例代碼塊→構造函數,當構造方法執行完畢,new 指令會返回堆內存中對象的引用(可理解為對象在堆中的地址標識 ),此時一個完整對象創建完成,可通過引用操作對象。
三、Java內存模型(JMM)?
JMM,全稱 Java Memory Model(Java內存模型)
1、概述
Java 內存模型是 Java 虛擬機規范中定義的一套抽象規則,它圍繞多線程并發場景下的數據可見性、原子性、有序性等問題,規范了 Java 程序中各種變量(實例字段、靜態字段、數組元素等,不包含局部變量和方法參數,因后者線程私有 )的訪問方式。
核心目標
解決多線程環境里,由于 CPU 緩存、編譯器優化等因素,導致不同線程對共享變量操作出現的可見性紊亂、執行順序混亂問題,為 Java 并發編程提供內存訪問的底層保證,讓開發者基于 JMM 規則編寫代碼,能在不同硬件和操作系統的 JVM 實現上,獲得一致的并發表現。
2、運行時數據區域劃分
JDK1.8以后分為:線程共享(Heap 堆區、MetaSpace 元空間)、線程私有(虛擬機棧、本地方法棧、程序計數器)
3、程序計數器
程序執行過程中會不斷切換當前執行線程,切換后為了能讓當前線程恢復到正確的執行位置,每一條線程都需要一個獨立的程序計數器。(是當前線程所執行的字節碼的行號指示器)
作用:
- 字節碼解釋器通過改變其值來一次讀取指令,從而實現代碼的流程控制(如順序直接、選擇、循環等)
- 程序計數器是唯一一個不會出現OutOfMemoryError 的內存區域,它隨著線程的創建而創建,也隨線程的結束而死亡。
4、Java虛擬機棧(VM Stack)
Java 虛擬機棧是由一個個棧幀組成,而每個棧幀中都擁有:局部變量表、操作數棧、動態鏈接、方法出口信息。每一次方法調用都會有一個對應的棧幀被壓入VM?Stack 虛擬機棧,每一個方法調用結束后,代表該方法的棧幀會從 VM Stack 虛擬機棧中彈出。
????????在活動線程中, 只有位于棧頂的幀才是有效的,稱為當前活動棧幀,代表正在執行的當前方法。在 JVM 執行引擎運行時,所有指令都只能針對當前棧幀進行操作。虛擬機棧通過 pop 和 push 的方式,對每個方法對應的活動棧幀進行運算處理,方法正常執行結束,肯定會跳轉到另一個棧幀上。?
VM Stack(虛擬機棧)的棧幀彈出主要發生在以下幾種情況:
- 方法正常執行完畢:當方法執行到末尾的 } 或 return 語句時,當前方法的棧幀會從 VM Stack 中彈出,程序回到上層調用方法繼續執行。
- 方法拋出未捕獲的異常:若方法執行中拋出異常且未被自身的 try-catch 捕獲,該方法的棧幀會彈出,異常向上層調用鏈傳播,直至被捕獲或導致程序終止。
- 線程執行結束:當線程的所有任務執行完畢,該線程對應的 VM Stack 中所有棧幀會被依次彈出,釋放資源。
- 遞歸調用終止:遞歸方法達到終止條件后,從最內層遞歸開始,棧幀會逐層彈出,直到回到最初的調用點。
這些情況本質上都是方法生命周期結束的體現,棧幀的彈出確保了 VM Stack 的內存資源能被正確回收和復用。
5、本地方法棧(Native Method Stack)
????????native 關鍵字修飾的本地方法被執行的時候,在本地方法棧中也會創建一個棧幀,用于存放該 native 本地方法的局部變量表、操作數棧、動態鏈接、方法出口信息。方法執行完畢后,相應的棧幀也會出棧并釋放內存空間。也會出現 StackOverFlowError 和 OutOfMemoryError 兩種錯誤。
6、堆(Heap)
????????Heap 堆是JVM所管理的內存中最大的一塊區域,被所有線程共享的一塊內存區域。堆區中存放對象實例,“幾乎”所有的對象實例以及數組都在這里分配內存。
6.1 新生代和老年代
????????Heap 堆是垃圾收集器GC(Garbage Collectde)管理的主要區域,因此堆區也被稱為GC堆。從垃圾回收的角度,由于現在收集器基本都采用分代垃圾收集算法,所以JVM中的堆區往往進行分代劃分。例如:新生代和老年代。目的是為了更好地回收內存,或者更快地分配內存。
6.2 創建對象的內存分配
當創建新對象時,內存分配起始于堆空間,核心流程圍繞新生代(Young Generation)的 Eden 區、Survivor 區,及對象晉升老年代的機制展開:
- Eden 區:對象初始誕生地,大部分對象優先在 Eden 區 分配內存。當 Eden 區被對象 “填滿”(達到內存閾值),會觸發 Young Garbage Collection(YGC,新生代垃圾回收 )。回收時,Eden 區執行 “清除策略”:無引用關聯的對象,直接被標記回收,釋放內存。
- Survivor 區:對象存活的 “過渡站”,YGC 后仍存活的對象,會被轉移至 Survivor 區(由 s0、s1 兩塊等大內存組成,同一時間僅一塊被使用 )。每次 YGC 執行時,存活對象會被復制到未被使用的 Survivor 空間(s0 或 s1 ),隨后清空當前正在使用的 Survivor 區域,同時交換兩塊空間的使用狀態(下次 YGC 切換目標 Survivor 區 )。伴隨每次 Survivor 區的 “復制 - 交換”,對象的年齡計數器 +1(記錄對象在新生代經歷的 YGC 次數 )。
- 老年代:長期存活對象的歸宿
- 直接晉升條件:若 YGC 中待轉移的對象大小,超過 Survivor 區剩余容量上限,會跳過 Survivor 區,直接 “移交” 到老年代(避免因多次復制浪費性能 )。
- 閾值晉升條件:對象不會永久停留在新生代。JVM 默認 “新生代晉升老年代的年齡閾值為 15”,即對象在 Survivor 區經歷 14 次 “復制 - 交換” 后,第 15 次 YGC 時會晉升到老年代,進入長期內存管理階段。
????????這樣的流程設計,通過 “分代回收” 策略,讓短期存活對象(大部分對象在 Eden 區一次 YGC 就被回收 )和長期存活對象(逐步晉升老年代 )的內存管理更高效,是 JVM 垃圾回收機制適配實際應用場景(對象 “朝生夕死” 特性 )的核心體現。
7、元空間(Meta Space)
????????元空間是 JDK 8+ 中 HotSpot 虛擬機對方法區的實現,替代原永久代。它基于本地內存分配,主要存儲類元數據(類結構信息)、運行時常量池(類相關常量與符號引用 )、靜態變量(static 修飾),以及 JIT 即時編譯器生成的熱點方法機器碼。其內存可通過 MetaspaceSize MaxMetaspaceSize 等參數調控,支持類卸載時的垃圾回收,解決永久代內存限制僵化問題,適配 Java 動態類加載場景,是 JVM 內存管理架構演進的關鍵部分 。
四、Java垃圾收集器
1、判斷對象是否存活
1.1 引用計數算法
核心原理
- 為每個對象設置一個引用計數器,記錄該對象被其他對象引用的次數。
- 當對象被新的引用指向時,計數器 + 1;當引用失效(如引用指向其他對象或超出作用域)時,計數器 - 1。
- 當計數器的值為0時,認為該對象不再被使用,可被回收。
引用計數算法存在的問題:對象循環引用
????????假設有兩個對象 A 和 B,A 持有 B 的引用,B 同時持有 A 的引用,且兩者都沒有被其他外部對象引用。此時:在引用計數算法中,A 和 B 的引用計數器始終為 1(互相引用),永遠不會變為 0,因此無法被回收,造成內存泄漏。
1.2 可達性分析算法
核心原理:
????????通過定義一系列稱為“ GC Roots ”的根對象作為起始節點集,從 GC Roots 開始,根據引用關系往下進行搜索,查找的路徑我們把它稱為“引用鏈”。每當一個對象到?GC Roots 之間沒有任何引用鏈相連時(對象與 GC Roots 之間不可達),那么該對象就是可被GC回收的垃圾對象。
2、Java中的四種引用類型
2.1 強引用(Strong Reference)
????????強引用時使用最普遍的引用。如果一個對象具有強引用,垃圾回收器就絕對不會回收它。當內存不足時,GC 寧愿拋出 OutOfMemoryError 錯誤,是程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足的問題。
Object strongRef = new Object();
// strongRef 是強引用,指向 new Object() 創建的對象
?如果強引用對象不使用時,需要弱化從而使 GC 能夠回收。
方式1:顯式置空強引用
主動把強引用變量賦值為 null,切斷引用和堆中對象的關聯。這樣 GC 就能識別出對象無有效引用,待 GC 執行時(具體時機由 GC 算法決定),回收對象釋放內存。
關鍵場景:
當強引用是全局變量、靜態變量(生命周期與類 / 程序一致)時,若不手動置空,對象會一直占內存。比如工具類的靜態緩存對象,不用時及時 staticRef = null ,避免內存泄漏。
// 強引用變量
Object strongRef = new Object();
// 不再使用對象時,顯式置空
strongRef = null;
// GC 運行時,可回收原對象(無其他強引用時)
?方式2:讓強引用隨作用域結束失效
利用 Java 虛擬機棧(VM Stack)的棧幀機制:方法執行時,會在虛擬機棧創建棧幀,存儲局部變量(含強引用);方法執行完畢,棧幀彈出銷毀,局部強引用也失效。若對象無其他強引用,就會被 GC 回收 。
應用場景
方法內的局部強引用(如臨時變量),無需手動置空,方法結束后自動解除引用。適合處理短期使用的對象,減少手動管理成本。
public void test() {// 方法內局部強引用,存于 test 方法的棧幀Object strongReference = new Object(); // 執行方法邏輯...
}
// 方法執行完,棧幀彈出,strongReference 失效
// 若對象無其他強引用,GC 可回收
2.2 軟引用(Soft Reference)
????????軟引用是一種 “相對靈活” 的引用,關聯的對象在 內存足夠時,不會被 GC 回收;當內存不足,GC 會回收軟引用關聯的對象 。它常用來實現內存敏感的緩存,比如圖片緩存,內存充足時緩存圖片,內存緊張時釋放緩存,避免 OOM。
import java.lang.ref.SoftReference;public class SoftRefDemo {public static void main(String[] args) {Object obj = new Object();SoftReference<Object> softRef = new SoftReference<>(obj);obj = null; // 清除強引用try {int size = 2048 * 4096;List<int[]> list = new ArrayList<>();while (true) {int[] array = new int[size];list.add(array);System.out.println("已分配 " + list.size() + " 個數組,軟引用狀態:" +(softRef.get() != null ? "存在" : "已回收"));System.gc(); // 建議GC}} catch (OutOfMemoryError e) {System.out.println("發生內存溢出!最終軟引用狀態:" +(softRef.get() != null ? "未回收" : "已回收"));}}
}
調用System.gc() 方法只是起通知作用,最終何時回收,由JVM決定。(當內存不足時,JVM首先將軟引用中的對象置為 null,然后通知垃圾回收器進行回收。)
?運行結果:
2.3 弱引用(Weak Reference)
????????弱引用的 “生存門檻” 更低,只要發生 GC,不管內存是否充足,弱引用關聯的對象都會被回收 。它常用于實現那些 “存在時可用,回收也不影響核心邏輯” 的場景,比如 ThreadLocal、弱引用哈希表(WeakHashMap )。
結合WeakHashMap的示例:
public class WeakHashMapDemo{public static void main(String[] args) {WeakHashMap<Key, Value> map = new WeakHashMap<>();Key key = new Key("key1");Value value = new Value("value1");map.put(key, value);key = null;System.gc();// 大概率已被回收,size 可能為 0System.out.println("WeakHashMap size: " + map.size());}
}class Key {private String key;public Key(String key) {this.key = key;}
}class Value {private String value;public Value(String value) {this.value = value;}
}
運行結果:
2.4 虛引用(Phantom Reference)
????????虛引用是最 “弱” 的引用,它的存在不影響對象的生命周期,主要用于跟蹤對象被 GC 回收的狀態 。虛引用必須和 引用隊列(ReferenceQueue) 配合使用,當 GC 準備回收對象時,會把虛引用加入關聯的引用隊列,程序可通過隊列感知對象回收時機,做一些資源釋放的收尾工作(比如釋放直接內存)。
public static void main(String[] args) throws InterruptedException {ReferenceQueue<Object> refQueue = new ReferenceQueue<>();Object obj = new Object();PhantomReference<Object> phantomRef = new PhantomReference<>(obj, refQueue);obj = null;System.gc();Thread.sleep(100);// 檢查引用隊列,若有元素,說明對象將被回收if (refQueue.poll() != null) {System.out.println("虛引用對象即將被 GC 回收");}}
3、垃圾收集算法
3.1 分代收集理論
目前主流 JVM 虛擬機中的垃圾收集器,都遵循分代收集理論:
- 弱分代:絕大多數對象都是朝生夕滅。
- 強分代:經歷越多次垃圾收集過程的對象,越難以回收,難以消亡。
?按照分代收集理論設計的“分代垃圾收集器,所采用的設計原則:收集器應該將 Java 堆劃分成不同的區域,然后將回收對象依據其年齡(年齡即對象經歷過垃圾收集過程的次數)分配到不同的區域存儲。
3.1.1 分代存儲
?????????如果一個區域中大多數對象都是朝生夕滅(新生代),難以熬過垃圾收集過程的話,把它們集中存儲在一起,每次回收時,只關注如何保留少量存活對象,而不是去標記大量將要回收的對象,就能以較低代價回收到大量的空間。
如果一個區域中大多數對象都是難以回收(老年代),那么把它們集中放在一起,VM 虛擬機就可以使用較低的頻率,來對這個區域進行回收。
這樣設計的好處是,兼顧垃圾收集的時間開銷和內存空間的有效利用。
3.1.2 分代收集
堆區按照分代存儲的好處:
????????在 Java 堆區劃分成不同區域后,垃圾收集器才可以每次只回收其中某一個或者某些區域,所以才有 MinorGC、MajorGC、FullGC?等垃圾收集類型劃分。
????????在 Java 堆區劃分成不同區域后,垃圾收集器才可以針對不同的區域,安排與該區域存儲對象存亡特征相匹配的垃圾收集算法:復制算法、標記-清除算法、標記-整理算法等。
垃圾收集類型劃分:
1. 部分收集(Partial GC):沒有完整收集整個 Java 堆的垃圾收集,其中又分為:
- 新生代收集(Minor GC?/ Young GC)
- 老年代收集(Major GC?/ Old GC)
- 混合收集(Mixed GC):收集整個新生代和部分老年代的垃圾收集。
2. 整堆收集(Full?GC):收集整個 Java 堆的垃圾收集。?
3.2?垃圾收集算法的種類
標記-清除算法(Mark-Sweep)
“標記-清除”? 算法實現思路:
該算法分為 “標記”?和 “清除”?階段:首先標記出所有不需要回收的對象,在標記完成后統一回收掉所有沒有被標記的對象。它是最基礎的收集算法,后續的算法都是對其不足進行改進得到。
“標記-清除”? 算法會帶來兩個明顯的問題:
- 執行效率不穩定問題:如果執行垃圾收集的區域,大部分對象是需要被回收的,則需要大量的標記和清除動作,導致效率變低。
- 內存空間碎片化問題:標記清除后會產生大量不連續的碎片,空間碎片太多,會導致分配較大對象時,無法找到足夠的連續空間,從而會觸發新的垃圾收集動作。
復制算法(Copying)
核心原理:將內存劃分為兩塊大小相等的區域,每次只使用其中一塊。當這一塊內存用完時,垃圾收集器將存活的對象復制到另一塊區域,然后將使用過的區域一次性清理掉,這樣就實現了垃圾回收。下次使用時,再切換到剛剛清理過的區域。
“復制”算法的問題 :
- 對象存活率較高,需要進行較多的內存間復制,效率降低。
- 浪費過多的內存,但使現有的可用空間變為原先的一半。
“復制”算法特點:
如果內存中多數對象都是存活的,這種算法將會產生大量的內存間復制的開銷。所以,復制算法適合僅需要復制少數存活對象的場景,而且每次都是針對整個半區進行內存回收,分配內存時也就不用考慮有空間碎片的復雜情況,只要移動堆頂指針,按順序分配即可。這樣實現簡單運行高效。?
標記整理算法(Mark-Compact)
?核心原理:該算法在標記 - 清除算法的基礎上進行了改進,同樣先進行標記階段,標記出所有存活的對象。然后在整理階段,將存活的對象向內存的一端移動,最后直接清理掉邊界以外的內存,即垃圾對象所占用的內存。
優點:
- 解決內存碎片問題:通過將存活對象移動到連續的內存區域,避免了標記 - 清除算法產生的內存碎片問題,提高了內存的利用率。
- 相對較高的效率:雖然比復制算法多了移動對象的操作,但在對象存活率較高的情況下,比標記 - 清除算法的效率要高,因為不需要反復掃描內存來處理碎片。
缺點:移動對象的過程需要額外的開銷,需要修改對象的引用地址,并且在移動對象時,可能需要暫停程序的執行,對應用程序的響應性有一定影響。
應用場景:適用于對象存活率較高的場景,比如 Java 中的老年代,老年代中的對象生命周期較長,存活的對象較多,使用標記 - 整理算法可以較好地管理內存。
3.3? 綜上所述?
?????????當前虛擬機的垃圾收集都基于分代收集思想,根據對象存活周期的不同,將內存分為幾個不同的區域,在不同的區域使用不同的垃圾收集算法。
例如: Heap 堆分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法。
????????在新生代中,每次收集都會有大量垃圾對象被回收,所以可以選擇“標記-復制”算法,只需要付出少量對象的復制成本就可以完成每次垃圾收集。
在老年代中,對象存活幾率是比較高的,而且沒有額外的空間對它進行分配擔保,所以選擇“標記-清除”或“標記-整理”算法進行垃圾收集。
4、垃圾收集器
4.1 Serial 收集器(新生代)
????????Serial(串行)收集器是最基本、歷史最悠久的垃圾收集器,采用 “標記-復制” 算法負責新生代的垃圾收集。它是 Hotspot 虛擬機運行在客戶端模式下的默認新生代收集器。
它是一個單線程收集器。它會使用一條垃圾收集線程去完成垃圾收集工作,并且它在進行垃圾收集工作的時候,必須暫停其他所有的工作線程(“Stop The World”),直到收集結束。
這樣的設計,帶來的好處就是:簡單高效。對于內存資源受限制的環境,它是所有收集器中額外內存消耗最小的收集器。適合單核處理器或處理器核心數較少的環境,每次收集幾十 MB 甚至一兩百 MB 的新生代內存,垃圾收集的停頓時間完全可以控制在十幾毫秒或幾十毫秒,最多一百多毫秒。?
4.2 Serial Old 收集器(老年代)
????????Serial Old 收集器同樣是一個單線程收集器,采用“標記-整理”算法負責老年代的垃圾收集,主要用于客戶端模式下的Hotspot虛擬機使用。
如果在服務器端使用,"它主要有兩種用途:
- 在 JDK5 及以前版本,與 Parallel Scavenge 收集器搭配使用。
- 作為 CMS 收集器發生失敗時的后備預案;
4.3 ParNew 收集器(新生代)
?????????ParNew 收集器是一個多線程的垃圾收集器。它是運行在 Server 模式下的虛擬機的首要選擇,可以與Serial Old,CMS?垃圾收集器一起搭配工作,采用“復制”算法。
4.4 Parallel Scavenge 收集器(新生代)
????????Parallel Scavenge:收集器是也是一款新生代收集器,使用“標記-復制”算法實現的多線程收集器。
Parallel Scavenge 收集器與其它收集器的目標不同,CMS 等其它收集器目標是盡可能縮短垃圾收集時用戶線程的停頓時間。但是 Parallel Scavenge 收集器的目標則是達到一個可控制的吞吐量。所謂吞吐量就是處理器用于運行用戶代碼的時間與處理器總消耗時間的比值。
????????若虛擬機執行某任務時,用戶代碼執行與垃圾收集的總耗時為 100 分鐘,其中垃圾收集耗時 1 分鐘,那么吞吐量即為 99% 。當停頓時間較短時,程序對用戶交互場景或服務響應質量要求高的情況適配性更好,良好的響應速度有助于提升用戶體驗;而高吞吐量可讓處理器資源得到最高效利用,能快速完成程序運算任務,更適合無需頻繁交互、以后臺運算為主的分析類任務 。
4.5 Parallel Old 收集器(老年代)
????????Parallel Old 收集器是一款多線程垃圾收集器,采用 “標記 - 整理” 算法,可視為 Parallel Scavenge 收集器的老年代版本。
在對吞吐量要求較高,或者處理器資源相對稀缺的應用場景中,Parallel Scavenge 收集器搭配 Parallel Old 收集器的組合,往往是優先之選。不過,這個組合直到 JDK 6 時才得以提供。在此之前,新生代的 Parallel Scavenge 收集器處境頗為尷尬。因為當新生代選用 Parallel Scavenge 收集器時,老年代除了 Serial Old 收集器外,便沒有其他可選。而像 CMS 這類表現出色的老年代收集器,無法與之協同工作。由于老年代 Serial Old 收集器在服務端應用性能上存在局限性,即便使用 Parallel Scavenge 收集器,也難以從整體上實現吞吐量的最大化。
此外,在老年代內存空間充裕且硬件規格較高的運行環境中,Serial Old 收集器單線程的特性,使其無法充分發揮服務器多處理器的并行處理能力。在這種情況下,Parallel Scavenge 與 Serial Old 的組合,其總吞吐量甚至可能比不上 ParNew 搭配 CMS 的組合。
4.6 CMS 收集器(老年代)
4.6.1 簡介
????????CMS(Concurrent Mark Sweep)收集器以 縮短垃圾回收停頓時間 為核心目標,基于 “標記 - 清除” 算法實現,是 HotSpot 虛擬機中首款真正意義的 并發垃圾收集器 。它開創性地讓垃圾收集線程與用戶線程(基本)并行工作,打破傳統垃圾回收 “全程停頓業務” 的局限。
在當前 Java 生態里,大量基于瀏覽器(或移動端)的 B/S 架構服務端應用,對 服務響應速度 極為關注 —— 需盡可能壓縮系統停頓時間,保障用戶交互體驗流暢。而 CMS 收集器的設計,恰好 適配這類應用場景,成為關注低延遲場景的優選方案。
4.6.2 工作流程
CMS 垃圾回收過程拆解為 4 個核心步驟 ,各環節分工與協同邏輯如下:
- 初始標記(CMS initial mark):快速標記與 GC Roots 直接關聯的對象,需短暫暫停用戶線程(STW)。由于僅處理 “根對象直接關聯” 范圍,此階段耗時極短,為后續分析奠定基礎。
- 并發標記(CMS concurrent mark):從 GC Roots 直接關聯對象出發,遍歷整個堆對象圖 ,精準標記所有存活對象。該過程耗時較長,但可與用戶線程 并行執行 ,不阻塞業務邏輯,讓垃圾回收 “后臺化” 運行。
- 重新標記(CMS remark):修正并發標記階段因用戶線程操作(如對象引用變更),產生的標記偏差。需短暫 STW,停頓時間通常 長于初始標記、遠短于并發標記 ,保障標記結果準確,為最終清理做準備。
- 并發清除(CMS concurrent sweep):清理并刪除標記階段判定的 “死亡對象” 。因無需移動存活對象,可與用戶線程 并行執行 ,利用空閑 CPU 資源完成垃圾回收,降低對業務的影響。
4.6.3 優點和缺點
核心優勢
- 并發回收,低停頓:通過 “并發標記、并發清除” 與用戶線程并行,大幅壓縮 STW 時長,適配對延遲敏感的應用場景(如 Web 服務、交互系統 )。
主要不足
- CPU 資源競爭:并發標記、并發清除階段,垃圾回收線程與用戶線程共享 CPU。默認回收線程數為 (CPU 數量 +3)/4 ,當 CPU≥4 時,回收線程至少占用 25% CPU 資源,會 間接降低用戶線程執行效率 。
- 浮動垃圾問題:并發清除階段,用戶線程持續產生新垃圾(“浮動垃圾” ),無法被當前回收周期處理,需等待 下次垃圾回收 才能清理,可能加劇內存壓力。
- 內存碎片風險:基于 “標記 - 清除” 算法,回收后會產生大量 不連續內存空間 。若長期運行,可能導致大對象分配失敗,迫使虛擬機提前觸發 Full GC ,反而增加停頓時間。
4.6.4特殊機制與風險
由于垃圾回收與用戶線程并行,需 預留足夠內存 供用戶線程運行,因此 CMS 無法像其他收集器(如 Serial Old )“等老年代滿了再回收” 。
- 觸發閾值:JDK6 默認老年代使用率達 92% 時啟動 CMS ,提前回收避免內存耗盡。
- 并發失敗風險:若 CMS 運行中,無法滿足程序 “分配新對象” 的內存需求,會觸發 “并發失敗” —— 臨時啟用 Serial Old 收集器(單線程、全程 STW )回收老年代,可能導致業務出現明顯卡頓。
4.6.5總結
????????CMS 收集器以 “并發回收、低停頓” 為核心優勢,適配對響應速度敏感的服務端場景,但因 CPU 資源競爭、浮動垃圾、內存碎片等問題,實際使用需結合業務特性(如 CPU 資源、內存分配模式 )權衡。在追求低延遲的 Web 服務、交互系統中,它是經典方案;但面對高吞吐量、內存碎片敏感場景,需謹慎評估或結合其他優化策略(如內存整理機制 )。
4.7 G1 收集器(老年代)
4.7.1 簡述
????????大吞吐量垃圾收集的痛點在于,若整個垃圾收集過程耗時過長,會觸發 “Stop The World(簡稱 STW )”。并且,STW 的時間很難精準預估,甚至可能在某次垃圾收集時,因標記階段耗時超預期,導致問題難以通過常規手段規避(比如讓 JVM 程序線程執行主動退讓)。這是因為隨著垃圾對象不斷增多,標記工作本身不可避免會耗時,所以 G1 垃圾收集器的發展,很大程度上圍繞優化 STW 時間展開。
4.7.2 什么是 G1 收集器
????????G1(Garbage - First)是面向服務端的垃圾收集器,專注于多處理器、大內存場景,無需嚴格按分代思想劃分內存處理對象。
它把堆內存劃分為多個大小相同的獨立區域(Region),Region 數量不超 2048 個。每個 Region 有 “角色” 區分,像 E(Eden,新生代區域 )、S(Survivor,新生代 Survivor 區 )、H(Humongous,大對象區域 ,用于存放大對象,若大對象放不下單個 Region,會跨 Region 連續分配 ),未被使用的 Region 為空閑狀態。這種靈活的內存布局,讓 G1 能依據各內存分區垃圾分布情況,動態調整收集策略,優先處理垃圾多的分區(即 “Garbage - First” 理念 “垃圾優先”),以此降低垃圾回收對整體應用的影響。當回收后存活對象少,回收收益就高,這也對應了 G1 收集器的 Mixed GC(混合回收模式 ),也就是部分區域的 GC 模式。
4.7.3 G1 垃圾收集器工作流程
- 初始標記(Initial Marking):標記與 GC Roots 直接關聯的對象,需暫停用戶線程(STW 短暫停頓 )。此階段為后續并發標記做基礎,確定 “根對象” 關聯的初始范圍。
- 并發標記(Concurrent Marking):從 GC Roots 出發,對堆中對象進行可達性分析,遍歷整個堆查找存活對象。耗時較長,但可與用戶線程并行,不阻塞業務。
- 最終標記(Final Marking):處理并發標記階段因用戶線程操作,產生的 “寫屏障” 記錄,需短暫 STW,保證標記結果準確(用于處理并發階段結束后遺留的記錄)。
- 篩選回收(Live Data Counting and Evacuation):統計每個 Region 存活對象數量、回收價值,按策略選擇回收的 Region 集合(即 “回收集” )。把選中 Region 里的存活對象,移動到空 Region,最后清空原 Region,實現內存整理與回收,該階段會 STW 。
4.7.4 G1 垃圾收集器的特點
- 并行與并發:利用多 CPU 多核優勢,多線程并行執行部分 GC 操作(如初始標記、最終標記 );并發標記階段與用戶線程并行,減少 STW 總時長,提升應用吞吐量。
- 分代收集兼容:雖弱化嚴格分代,但仍保留分代概念。能區分新生代、老年代對象,針對不同 “年齡” 對象(經歷 GC 次數不同 ),用不同策略處理,適配應用對象生命周期變化。
- 空間整合:G1 從整體來看是基于“標記-整理”算法實現的收集器,從局部(兩個 Region 之間)上來看是基于“標記-復制”算法實現的。這意味著 G1 運行期間不會產生內存空間碎片,收集后能提供規整的可用內存。此特性有利于程序長時間運行,分配大對象時不會因為無法找到連續內存空間而提前觸發下一次GC。
- 可預測的停頓:對比 CMS 等收集器,G1 除控制 STW 時間,還能通過 “Region 回收優先級”,讓用戶設置期望 STW 時長。它會根據 Region 垃圾占比,動態選擇回收集合,盡量滿足停頓時間目標,適合對延遲敏感的應用。
五、三色標記算法
1、概念及原理:
????????三色標記算法是垃圾回收領域中一種重要的追蹤式垃圾回收算法,主要用于并發或增量垃圾回收場景,能有效減少垃圾回收時的停頓時間。
其核心思想是通過三種顏色(白色、灰色、黑色)標記對象的狀態,來追蹤哪些對象是可達的(存活的),哪些是不可達的(需要回收的):?
【1】白色對象
- 定義:尚未被垃圾回收器訪問到的對象
- 初始狀態:在標記開始節點,所有對象均為白色,表示“未被發現”或“待處理”狀態。
- 最終狀態:標記結束后,仍然是白色的對象被視為垃圾對象,會在后續的清楚階段被回收。
【2】灰色對象
- 定義:已經被垃圾回收器訪問到,但該對象引用的其他對象還沒有被完全掃描,表示“已發現但未處理完”狀態。
- 中間狀態:表示該狀態正在被處理中,其部分引用已經被掃描,但還有一些引用未被掃描。
【3】黑色對象
- 定義:已經被垃圾回收器訪問過,并且該對象引用的所有其他對象也都已經被掃描過,表示“已處理完成”狀態。
- 終結狀態:黑色對象不會再被掃描,垃圾回收器認為其引用的所有對象都已經被標記。
2、基本工作流程:
【1】初始階段:所有對象都被標記為白色
【2】標記階段:
- 根對象標記:垃圾回收器從GCRoots根對象(如靜態變量引用、棧引用等)開始掃描,將根對象標記為灰色。
- 灰色對象處理:依次處理每個灰色對象,將其引用的所有白色對象標記為灰色,并將該灰色對象自身標記為黑色。
- 循環處理:重復上述步驟,直到所有灰色對象都變為黑色對象為止。
【3】完成階段:所有對象的顏色均為黑色或白色。白色對象即為垃圾對象,會在后續的清除階段被回收。
3、漏標問題
3.1 漏標的產生條件
漏標問題的發生需要同時滿足以下兩個條件:
- 黑色對象引用白色對象:一個已經被標記為黑色的對象(認為其引用的子對象都已處理完畢)新增了對一個白色對象的引用。
- 灰色對象丟失對白色對象的引用:原本引用該白色對象的灰色對象(正在處理中的對象),在標記過程中失去了對它的引用。
????????當這兩個條件同時滿足時,白色對象會因為沒有任何灰色對象指向它,且黑色對象不會再被重新處理,導致該對象最終被判定為垃圾(白色)而被錯誤回收。
3.2 解決漏標的核心思路
1. 寫屏障(Write Barrier)
當對象引用發生變化時(如 A 對象引用了 B 對象),通過寫屏障攔截這一操作,并根據策略對相關對象進行處理:
- 增量更新(Incremental Update):當黑色對象新增對白色對象的引用時,將該黑色對象重新標記為灰色,確保其新引用的對象能被掃描到(打破第一個條件)。
- 原始快照(Snapshot At The Beginning, SATB):在并發標記開始時記錄對象引用的快照,當灰色對象要刪除對白色對象的引用時,保留該引用的記錄,確保白色對象能被正確標記(打破第二個條件)。
2. 讀屏障(Read Barrier)
較少使用,主要在讀取對象引用時觸發檢查,確保引用的對象處于正確的標記狀態。???????