一、JVM運行流程
如圖:
JVM由四個部分構成:
- 1.類加載器
加載類文件到內存 - 2.運行時數據區
寫的程序需要加載到這里才能運行 - 3.執行引擎
負責解釋命令,提交操作系統執行 - 4.本地接口
融合不同編程語言為java所用,如Java程序驅動打印機
二、JVM內存區域
其中, 方法區 是各個線程共享的一塊邏輯內存區域,它用于存儲已經被虛擬機加載的類型信息、常量、靜態變量、即使編譯器編譯后的代碼緩存等數據。方法區的實現在 Java8后的HotSpot虛擬機采用的是元空間(元空間在本地內存)。
在Java8之前,HotSpot虛擬機采用的是永久代(永久代在堆內存)來實現的方法區,這樣HotSpot的垃圾收集器能夠像管理Java堆內存一樣管理這部分內存,省去了為方法區編寫內存管理代碼的工作。但是其他虛擬機實現方法區時不存在永久代的概念。
三、類加載機制
Java程序不是一次性加載所有類到內存中的。JVM在運行時按需加之類。類加載機制負責將.class文件從磁盤、網絡等地方的資源加載到JVM的方法區,并最終在堆內存中創建對應Class對象,作為訪問該類型元數據的入口。
1.類加載的生命周期
- 1.加載
- 任務:查找并加載類的.class文件
- 結果:在方法區(jdk1.7)/元空間(jdk1.8)創建類運行時數據結構,并在堆內創建一個代表該類的一個java.lang.Class對象作為訪問這些數據的入口。
java.lang.Class 對象是 Java 類的運行時表示。它包含了類的元數據(如類名、字段、方法、構造函數等信息),并且提供了訪問這些元數據的方法。通過 Class 對象,程序可以在運行時動態地獲取類的信息、創建類的實例、調用方法、訪問字段等。
- 2.驗證:
- 目的:確保被加載類的字節碼文件是安全、合法的,符合JVM規范的,不會危害JVM安全的。
- 檢查內容:
- 文件格式驗證
- 元數據驗證(語義分析:是否有父類、是否繼承 final 類、方法覆蓋是否合法等)
- 字節碼驗證(最復雜:檢查方法體中的指令邏輯是否合法、類型轉換是否安全、跳轉指令是否指向合理位置等)
- 符號引用驗證(發生在解析階段,檢查符號引用能否被正確解析)
- 3.準備:
- 目的:為類的靜態變量分配內存(在方法區/元空間)并設置初始零值。
- 4.解析:
- 目的:將常量池的符號引用改成直接引用
- 符號引用:一組符號描述引用的目標
- 直接引用:可以是直接指向目標的指針、相對偏移量或能間接定義到目標的句柄。
- 解析目標:類或接口、字段、類方法、接口方法、方法類型、方法句柄、調用點限定符。
- 5.初始化:
- 目標:執行類的初始化代碼(() 方法)。
- () 方法:由編譯器自動收集類中所有類變量的賦值動作和靜態語句塊 (static{} 塊) 中的語句合并生成。
- 觸發時機:首次主動創建一個類時
- 父類優先: JVM 保證一個類的 () 方法執行前,其父類的 () 方法必須已執行完畢。
- 線程安全:JVM 會確保 () 方法在多線程環境下被正確地加鎖同步執行(只有一個線程能執行)。
- 6.使用
- 7.卸載
2.類加載器
負責實現“加載”階段。JVM 內置三類重要的類加載器,它們之間存在層次關系(雙親委派模型的基礎):
- 1.啟動類加載器(Bootstrap ClassLoader):
- 是JVM自身的一部分
- 加載 JAVA_HOME/lib 目錄下的核心類庫(如 rt.jar, resources.jar, charsets.jar)或 -Xbootclasspath 參數指定的路徑下的類。
- 2.擴展類加載器(Extension ClassLoader / Platform ClassLoader - JDK9+):
- 加載 JAVA_HOME/lib/ext 目錄或 java.ext.dirs 系統變量指定路徑下的類庫。
- 3.應用程序加載器(Application ClassLoader / System ClassLoader):
- 加載用戶類路徑 (ClassPath) 下的類庫。開發者編寫的類通常由此加載器加載。
- 是程序中 ClassLoader.getSystemClassLoader() 的默認返回值。
四、雙親委派
當類加載器收到類加載請求時:
- 1.委派父加載器:不會立刻加載職工類,而是先委派給父加載器來加載。
- 2.遞歸委派:直到訂層的啟動類加載器
- 3.父加載器嘗試加載:
- 如果父加載器可以完成加載任務(在其負責的范圍內找到了該類),則成功返回該類。
- 如果父加載器無法完成加載任務(在其負責范圍內找不到該類),則子加載器才會嘗試自己去加載。
- 如果子加載器也找不到,則拋出 ClassNotFoundException。
優勢:
- 1.避免重復加載
- 2.保證核心類庫安全:例如,即使你在 ClassPath 下寫了一個 java.lang.Object 類,由于雙親委派,請求會最終委派給 Bootstrap Loader,它加載了真正的核心 Object 類,你的自定義 Object 不會被加載。
- 3.保證基礎類的統一性:確保核心類庫(如 java.lang.String)對所有子加載器可見且一致。
打破雙親委派的情況:
- SPI (Service Provider Interface): 如 JDBC。核心接口在 rt.jar 由 Bootstrap Loader 加載,但數據庫廠商的實現類在 ClassPath 下需要由 AppClassLoader 加載。為了解決這個矛盾,引入了線程上下文類加載器 (Thread Context ClassLoader, TCCL),它通常默認是 AppClassLoader。核心代碼(如 DriverManager)在需要加載 SPI 實現時,使用 TCCL 來加載。
- 熱部署/熱替換: 如 Tomcat, OSGi。需要同一個類的不同版本共存。自定義類加載器需要能獨立加載同一類名的不同版本,不遵循父優先原則,而是先自己嘗試加載。
- 代碼熱替換 (HotSwap): 在調試時替換修改過的類,需要類加載器能重新加載類。
五、垃圾回收機制
1.判斷對象是否存活
1.引用計數法:
- 原理:為每個對象維護一個計數器,當對象被引用時計數器+1,引用失效時計數器-1,計數器為0時對象可回收。
- 缺點:無法解決循環引用問題(A 引用 B,B 引用 A,但 A 和 B 都不再被外部引用,計數器不為0)。
2.可達性分析算法
- 原理: 通過一系列稱為 “GC Roots” 的根對象作為起始點集,從這些節點開始向下搜索,搜索過程走過的路徑稱為 “引用鏈 (Reference Chain)”。如果一個對象到 GC Roots 沒有任何引用鏈相連(即不可達),則證明此對象是不可用的,可以被回收。
- GC Roots 對象包括:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象。
- 本地方法棧中 JNI(即 Native 方法)引用的對象。
- 方法區中類靜態屬性引用的對象。
- 方法區中常量引用的對象(如字符串常量池里的引用)。
- Java 虛擬機內部的引用(如基本數據類型對應的 Class 對象,常駐的異常對象 NullPointerException、OutOfMemoryError 等,系統類加載器)。
- 所有被同步鎖(synchronized 關鍵字)持有的對象。
- 反映 Java 虛擬機內部情況的 JMXBean、JVMTI 中注冊的回調、本地代碼緩存等。
- 主流 JVM 使用此方法。
3.引用類型:
- 1.強引用:強引用不能被回收。
- 2.軟引用:當內存不足時才會被回收。
- 3.弱引用:下一次GC就被回收。
- 4.虛引用:最弱的引用。無法通過虛引用獲取對象實例。唯一目的是對象被回收時收到一個系統通知。用于跟蹤對象被垃圾回收的活動。
4.垃圾回收算法
- 1.標記清除法:
- 步驟:遍歷GC Roots標記所有可達對象,清除所有未被標記的對象。
- 缺點:慢,空間碎片化。
- 2.復制算法:
- 步驟:將可用內存按容量劃分為大小相等的兩塊(A 和 B)。每次只使用其中一塊(如 A)。當 A 用完了,就將 A 中還存活的對象復制到 B 上,然后一次性清理掉 A 上的所有空間。交換角色(現在使用 B)。
- 優點:塊,無大量不連續碎片內存。
- 缺點:內存縮小為原來的一半,代價高昂。適用于對象存活率低的場景(如新生代的 Eden 和 Survivor 區)。
- 3.標記整理法:
- 步驟:遍歷GC Roots標記所有可達對象,清除所有未被標記的對象,并且將存活對象都移動到空間的一端。
- 優點:沒有內存碎片。
- 缺點:移動存活對象并更新引用地址需要 STW (Stop-The-World) 時間較長。適用于對象存活率高的場景(如老年代)。
- 4.分代收集算法:
- 核心思想:根據對象存活周期的不同將堆劃分為新生代 (Young Generation) 和老年代 (Old Generation)。
- 新生代特點: 對象創建頻繁,存活率低。
- 回收算法: 主要采用復制算法(Minor GC / Young GC)。
- 老年代特點: 對象存活周期長,存活率高。
- 回收算法: 主要采用標記-清除或標記-整理算法(Major GC / Full GC)。