文章目錄
- 內存結構
- 程序計數器
- 什么是程序計數器?
- 核心作用:為什么需要程序計數器?
- 實現原理
- 主要特點
- 示例:PC 寄存器如何工作
- 總結
- Java 虛擬機棧
- 什么是 Java 虛擬機棧?
- 棧幀的內部結構
- 主要特點
- 總結
- 線程診斷
- 本地方法棧
- 堆
- 堆內存診斷
- 方法區
- 1. 什么是方法區?
- 2. 方法區的演進:從永久代到元空間
- a) Java 8 之前:永久代
- b) Java 8 及之后:元空間
- 3. 方法區(元空間)里到底存了什么?
- 4. 一個特殊區域:字符串常量池
- StringTable
- 直接內存 - 系統系統內存
- 1. 什么是直接內存?
- 2. 為什么要使用直接內存?(核心動機)
- a) 傳統的 I/O 操作(使用堆內存)
- b) 使用直接內存的 I/O 操作 (NIO)
- 3. 直接內存的管理
- 分配與釋放
- 潛在風險
- 4. 直接內存的特點總結
- 5. 什么時候使用直接內存?
- 垃圾回收
- 如何判斷對象可以回收
- 四種引用
- 1. 強引用(Strong Reference)
- 2. 軟引用(Soft Reference)
- 3. 弱引用(Weak Reference)
- 弱引用中,若 new Object() 被回收后,weakRef 的值是不是為 null
- 4. 虛引用(Phantom Reference)
- 總結對比
- 垃圾回收算法
- 分代垃圾回收
- 垃圾回收器
- 串行
- 吞吐量優先
- 響應時間優先
- G1
- ZGC
- 類加載與字節碼技術
- 類文件結構
- 多態原理
- catch
- 類加載
- 加載
- 連接
- 初始化
- 類加載器
- 線程上下文類加載器
- 自定義類加載器
https://www.bilibili.com/video/BV1yE411Z7AP
內存結構
程序計數器
好的,我們來詳細講解一下 Java 虛擬機(JVM)內存結構中的程序計數器(Program Counter Register)。
我會從它是什么、為什么需要它、如何工作以及它的核心特點這幾個方面來全面解釋。
什么是程序計數器?
程序計數器,也常被稱為 PC 寄存器,是 JVM 內存模型中一塊非常小的內存空間。你可以把它想象成是當前線程正在執行的字節碼指令的地址或行號指示器。
簡單來說,它就是一個“書簽”,記錄著虛擬機“讀”到了哪一行代碼,下一行應該“讀”什么。
在 JVM 的規范中,每個 Java 線程都有其自己獨立的程序計數器。這些計數器在線程創建時一同創建,并伴隨線程的整個生命周期。
核心作用:為什么需要程序計數器?
JVM 的執行引擎需要依賴程序計數器來讀取下一條需要執行的字節碼指令。它的核心作用是控制程序的執行流程。
如果缺少程序計數器,JVM 將不知道下一步該執行哪條指令。以下是它發揮作用的關鍵場景:
- 順序執行:最基本的功能,執行完當前指令后,PC 寄存器會自動指向下一條指令,保證代碼按順序執行。
- 分支與循環:當遇到
if-else
、switch
、for
、while
等控制流語句時,JVM 會根據判斷結果,修改 PC 寄存器的值,讓它跳轉到指定的分支或循環體的字節碼地址。 - 異常處理:當發生異常時,異常處理器會捕獲異常,并修改 PC 寄存器的值,將其指向異常處理代碼塊(
catch
塊)的起始地址。 - 方法調用與返回:調用一個新方法時,PC 寄存器會記錄下調用位置的下一條指令地址,然后跳轉到新方法的入口地址。方法返回時,再恢復 PC 寄存器的值,回到原來的調用位置繼續執行。
- 多線程切換:這是 PC 寄存器最重要的用途之一。在多線程環境中,CPU 會在不同線程之間快速切換。當一個線程被掛起(失去 CPU 時間片)時,它的 PC 寄存器會保存當前執行到的指令地址。當這個線程再次獲得 CPU 時間片時,它就能通過 PC 寄存器中保存的地址,準確地從上次中斷的地方繼續執行,而不會“迷路”。
實現原理
程序計數器的實現與當前線程執行的方法類型有關:
-
當執行的是一個 Java 方法時:
- PC 寄存器中存儲的是一個字節碼指令的地址。這個地址是相對于方法區中當前方法字節碼的偏移量。JVM 的執行引擎(如解釋器)就是通過這個地址來獲取下一條要執行的字節碼指令。
-
當執行的是一個本地(Native)方法時:
- 本地方法是通過 JNI (Java Native Interface) 調用 C/C++ 等本地庫實現的,其執行不歸 JVM 管理,而是直接在底層操作系統上運行。
- 在這種情況下,程序計數器的值是未定義的(Undefined)。因為 JVM 無法追蹤到本地代碼的執行位置。
從物理實現上講,JVM 中的程序計數器通常會直接利用 CPU 的物理寄存器來實現,因為這能提供最快的讀寫速度。
主要特點
程序計數器有幾個非常鮮明且重要的特點:
-
線程私有(Thread-Private)
- 這是它最核心的特性。每個線程都有自己獨立的 PC 寄存器,互不干擾。這保證了在并發環境下,線程切換后能夠正確恢復執行現場。如果所有線程共享一個 PC 寄存器,那么執行流程將徹底混亂。
-
不會發生內存溢出(No
OutOfMemoryError
)- 程序計數器是 JVM 運行時數據區中唯一一個在 Java 虛擬機規范中沒有規定任何
OutOfMemoryError
情況的區域。 - 原因很簡單:它占用的內存空間非常小(通常只占一個字長,如 32 位或 64 位),并且在線程創建時大小就已經確定,在運行期間不會改變。這點內存對于現代計算機來說幾乎可以忽略不計。
- 程序計數器是 JVM 運行時數據區中唯一一個在 Java 虛擬機規范中沒有規定任何
-
極快的訪問速度(Extremely Fast Access)
- 由于它通常由 CPU 寄存器直接實現,其讀寫操作是所有內存操作中最快的,幾乎沒有延遲。這是保證 JVM 執行效率的基礎。
-
生命周期與線程同步
- 它的生命周期與所屬的線程完全一致,隨線程的創建而創建,隨線程的銷毀而銷毀。
示例:PC 寄存器如何工作
讓我們通過一個簡單的例子來直觀感受一下。
Java 代碼:
public class PCTest {public static void main(String[] args) {int a = 10;int b = 20;int c = a + b;}
}
使用 javap -c PCTest
命令可以查看其字節碼:
public static void main(java.lang.String[]);Code:0: bipush 10 // 將 10 推到操作數棧頂2: istore_1 // 將棧頂的值存入局部變量表索引為 1 的位置 (a)3: bipush 20 // 將 20 推到操作數棧頂5: istore_2 // 將棧頂的值存入局部變量表索引為 2 的位置 (b)6: iload_1 // 從局部變量表加載 a7: iload_2 // 從局部變量表加載 b8: iadd // 執行加法9: istore_3 // 將結果存入局部變量表索引為 3 的位置 (c)10: return // 方法返回
PC 寄存器的變化過程:
- 初始狀態,PC 寄存器的值為
0
。 - 執行引擎讀取地址
0
的指令bipush 10
。執行完畢后,PC 寄存器的值更新為下一條指令的地址2
。 - 執行引擎讀取地址
2
的指令istore_1
。執行完畢后,PC 寄存器的值更新為3
。 - 執行引擎讀取地址
3
的指令bipush 20
。執行完畢后,PC 寄存器的值更新為5
。 - … 以此類推,PC 寄存器像一個指針一樣,一步步地引導著整個程序的執行流程,直到遇到
return
指令。
總結
特性 | 描述 |
---|---|
定義 | 當前線程所執行的字節碼指令的地址指示器。 |
作用 | 控制程序執行流程,實現分支、循環、跳轉、異常處理、線程恢復等功能。 |
歸屬 | 線程私有,每個線程獨享一份。 |
內存 | 占用空間極小,是唯一不會發生 OutOfMemoryError 的區域。 |
實現 | 執行 Java 方法時,存儲字節碼地址;執行 Native 方法時,值為空(Undefined)。 |
速度 | 讀寫速度極快,通常由 CPU 寄存器實現。 |
理解程序計數器是深入學習 JVM 內存模型和 Java 并發編程的基礎。它雖然簡單,卻是整個 JVM 執行引擎能夠有序工作的核心部件。
Java 虛擬機棧
棧幀:每個方法運行時需要的內存
什么是 Java 虛擬機棧?
Java 虛擬機棧是 線程私有 的內存區域,它的生命周期與線程相同。當一個線程被創建時,JVM 會為它分配一個對應的虛擬機棧;當線程結束時,這個棧也會被銷毀。
它的核心作用是為 Java 方法的執行提供內存模型。每當一個方法被調用時,JVM 就會在當前線程的虛擬機棧中創建一個稱為 “棧幀”(Stack Frame) 的數據結構,并將其推入(push)棧頂。當這個方法執行完畢后,對應的棧幀就會被彈出(pop)并銷毀。
因此,對于一個線程來說,任何時候只有位于棧頂的棧幀是活動的(Active),這個棧幀被稱為當前棧幀(Current Stack Frame),它對應的方法被稱為當前方法(Current Method)。所有字節碼指令的操作都只針對當前棧幀進行。
你可以把它想象成一個疊起來的盤子:
- 調用一個新方法 = 往盤子堆頂部放一個新盤子。
- 方法執行結束 = 從頂部拿走一個盤子。
- 你永遠只能使用最上面的那個盤子。
這種后進先出(Last-In, First-Out, LIFO)的數據結構完美地契合了方法調用的層級關系。
棧幀的內部結構
棧幀是虛擬機棧的基本元素,是方法執行期間所有數據的集合。每個棧幀都包含以下幾個關鍵部分:
-
局部變量表(Local Variable Table)
- 這是一塊用于存儲方法參數和方法內部定義的局部變量的區域。
- 它是一個以“槽”(Slot)為單位的數組,每個槽可以存放一個
boolean
,byte
,char
,short
,int
,float
,reference
(對象引用)或returnAddress
類型的數據。 long
和double
類型的數據會占用兩個連續的槽。- 對于實例方法(非
static
方法),局部變量表的第 0 個槽默認存放指向該方法所屬對象的引用,即this
。 - 局部變量表的大小在 Java 代碼編譯成字節碼時就已經確定,并在方法運行期間保持不變。
-
操作數棧(Operand Stack)
- 這也是一個后進先出(LIFO)的棧,用于存放方法執行過程中的臨時數據。它扮演著 JVM 執行引擎的工作區或計算舞臺的角色。
- 字節碼指令會從局部變量表中加載數據,推入操作數棧,然后從操作數棧中彈出數據進行運算,最后再將運算結果推入操作數棧或存回局部變量表。
- 例如,執行
a + b
時,會先把a
和b
的值依次壓入操作數棧,然后執行iadd
指令,該指令會彈出棧頂的兩個值相加,再將結果壓回棧頂。
-
動態鏈接(Dynamic Linking)
- 每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用。
- 這個引用的作用是為了支持方法調用過程中的動態鏈接。
- 在編譯時,一個方法調用另一個方法,是以符號引用(Symbolic Reference,比如方法的全限定名和描述符)的形式存在于
.class
文件中。在運行時,JVM 需要將這些符號引用轉換為可以直接調用的內存地址,即直接引用(Direct Reference)。這個轉換過程就是動態鏈接。
-
方法返回地址(Return Address)
- 當一個方法執行完畢后,需要返回到調用它的地方繼續執行。方法返回地址就存儲了這個“調用者的位置”。
- 方法退出有兩種方式:
- 正常返回(Normal Completion):執行了
return
指令。調用者的 PC 計數器會恢復,程序繼續正常執行。 - 異常返回(Abrupt Completion):方法執行過程中拋出了未被捕獲的異常。這種情況下,返回地址由異常處理器表來確定,而不會返回給調用者。
- 正常返回(Normal Completion):執行了
主要特點
-
線程私有(Thread-Private)
- 和程序計數器一樣,虛擬機棧是線程隔離的。每個線程都有自己的棧,因此棧內的局部變量等數據對其他線程是不可見的。這也是為什么局部變量是線程安全的根本原因。
-
生命周期與線程同步
- 隨線程創建而生,隨線程結束而亡。
-
可能出現的兩種異常
StackOverflowError
(棧溢出錯誤):- 原因:如果線程請求的棧深度大于虛擬機所允許的最大深度,就會拋出此錯誤。
- 常見場景:無限遞歸調用或方法調用層次過深(例如,一個方法調用自己沒有出口)。
- 棧的深度可以是固定的(通過
-Xss
參數設置),也可以是動態擴展的。
OutOfMemoryError
(內存溢出錯誤):- 原因:如果虛擬機棧允許動態擴展,但在嘗試擴展時無法申請到足夠的內存,就會拋出此錯誤。或者,當創建一個新線程時,沒有足夠的內存來為其創建對應的虛擬機棧。
- 說明:相比于
StackOverflowError
,這種 OOM 在虛擬機棧上相對少見,但理論上是可能發生的。
總結
Java 虛擬機棧是理解 Java 程序運行機制的核心。它通過棧幀來支持方法的調用和執行,管理著方法的局部變量、計算過程和返回邏輯。它的線程私有特性是 Java 實現多線程安全的重要基石之一,而 StackOverflowError
也是每個 Java 開發者都可能遇到的典型運行時錯誤。
線程診斷
本地方法棧
堆
存放創建的對象
堆內存診斷
- jvisualvm
方法區
JDK8之后,字符串常量池和靜態變量移到堆中了
1. 什么是方法區?
方法區和 Java 堆一樣,是所有線程共享的內存區域。
根據《Java虛擬機規范》的定義,方法區是一個邏輯上的概念,它用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器(JIT)編譯后的代碼緩存等數據。
可以把它理解為存放“模板”的地方:我們用 new
關鍵字創建對象實例時,對象本身放在堆里,但創建這個對象所依據的 class
定義信息,則存放在方法區里。
關鍵點:
- 線程共享:所有線程都可以訪問方法區的數據。
- 邏輯概念:它是一種規范,不同的 JVM 實現可以有不同的方式來實現它。最著名的實現就是我們接下來要講的“永久代”和“元空間”。
2. 方法區的演進:從永久代到元空間
這是理解方法區最核心的部分,因為它的具體實現隨著 Java 版本的更新發生了重大變化。我們主要討論 Oracle 的 HotSpot 虛擬機。
a) Java 8 之前:永久代
在 Java 8 之前的版本中,HotSpot 虛擬機使用永久代來實現方法區。
- 特點:永久代是 Java 堆的一部分。這意味著它和其他對象一樣,受到 JVM 堆內存大小的限制,并且由 JVM 的垃圾回收器來管理。
- 缺點:
- 大小固定:永久代有一個固定的最大尺寸(可以通過
-XX:MaxPermSize
設置)。在動態加載大量類(如使用 CGLIB 動態代理、啟動大量 Web 應用)的場景下,非常容易耗盡空間,從而拋出java.lang.OutOfMemoryError: PermGen space
異常。 - GC效率低:對永久代的垃圾回收(主要是卸載不再使用的類)條件苛刻且效率不高,一次針對永久代的 Full GC 會帶來較長的“Stop-The-World”暫停。
- 大小固定:永久代有一個固定的最大尺寸(可以通過
b) Java 8 及之后:元空間
為了解決永久代的這些問題,從 Java 8 開始,HotSpot 虛擬機移除了永久代,并引入了一個新的實現——元空間。
- 最大變化:元空間不再位于 Java 堆中,而是直接使用本地內存(Native Memory)。
- 優點:
- 解除大小限制:理論上,只要你的服務器物理內存足夠,元空間就可以一直擴展,從根本上解決了
PermGen space
的 OOM 問題。當然,你也可以通過-XX:MaxMetaspaceSize
來設定其最大值,以防它無節制地消耗系統內存。 - 更好的性能:將類的元數據從堆中移出,使得 Full GC 時需要掃描和處理的內容變少,有助于降低 GC 帶來的暫停時間。
- 解除大小限制:理論上,只要你的服務器物理內存足夠,元空間就可以一直擴展,從根本上解決了
總結一下二者的核心區別:
特性 | 永久代 (PermGen) | 元空間 (Metaspace) |
---|---|---|
存在版本 | JDK 1.7 及之前 | JDK 1.8 及之后 |
內存位置 | Java 堆 的一部分 | 本地內存 (Native Memory),獨立于堆 |
大小限制 | 固定,由 -XX:MaxPermSize 控制 | 默認只受限于物理內存,可由 -XX:MaxMetaspaceSize 控制 |
常見錯誤 | OutOfMemoryError: PermGen space | OutOfMemoryError: Metaspace |
3. 方法區(元空間)里到底存了什么?
無論是永久代還是元空間,它們作為方法區的實現,存儲的內容大體一致:
-
類型信息(Class Information):
- 類的完整有效名稱(包名.類名)。
- 類的直接父類的完整有效名稱。
- 類的修飾符(public, abstract, final 等)。
- 類的直接實現接口的有序列表。
-
字段信息(Field Information):
- 類的所有字段(成員變量)的名稱、類型和修飾符。
-
方法信息(Method Information):
- 類的所有方法的名稱、返回類型、參數數量和類型、修飾符。
- 方法的字節碼(Bytecode)、操作數棧大小、局部變量表大小等。
-
靜態變量(Static Variables):
- 被
static
關鍵字修飾的類變量。這些變量與類直接關聯,而不是與對象實例關聯。
- 被
-
運行時常量池(Runtime Constant Pool):
- 這是方法區中非常重要的一部分,是每個類或接口的常量池的運行時表示形式。
- 它包含編譯期生成的各種字面量(如文本字符串、
final
常量值)和符號引用(如類和接口的全限定名、字段和方法的名稱和描述符)。 - 當程序運行時,JVM 會將這些符號引用解析為直接引用(內存地址)。
-
JIT 編譯后的代碼緩存:
- JVM 會將頻繁執行的“熱點”字節碼編譯為本地機器碼以提升性能,這部分編譯后的代碼也存儲在方法區中。
4. 一個特殊區域:字符串常量池
這是一個經常與方法區一起討論但又比較特殊的地方。它的位置也發生了變遷:
- JDK 1.6 及之前:字符串常量池存放在永久代中。
- JDK 1.7 開始:字符串常量池被從永久代移動到了 Java 堆中。
為什么移動?
因為永久代的空間有限且垃圾回收效率低。程序中通常會創建大量的字符串,如果都放在永久代,很容易引發 PermGen space
錯誤。將它移到空間更大、GC 更頻繁的堆區,是一個更合理的設計。
因此,在 Java 8 及以后的版本中,方法區的實現是元空間(在本地內存),而字符串常量池則在堆內存中。
希望這個講解能幫你清晰地區分方法區、永久代和元空間這些概念。
StringTable
s3 和 s5 相等
字符串池去重
直接內存 - 系統系統內存
1. 什么是直接內存?
直接內存(Direct Memory)是指在 Java 堆之外,直接向操作系統申請的內存空間。它不歸 JVM 的垃圾回收器(Garbage Collector)直接管理,而是由 Java 程序通過代碼(主要是 NIO 相關 API)來分配、使用和釋放。
程序可以通過 java.nio.ByteBuffer.allocateDirect(int capacity)
方法來分配一塊直接內存。這個方法返回的是一個 DirectByteBuffer
對象,這個對象本身在 Java 堆上,但它內部引用了一塊位于堆外的、由操作系統管理的本地內存。
2. 為什么要使用直接內存?(核心動機)
使用直接內存最核心的目的是提升 I/O 操作的性能,特別是網絡和文件 I/O。為了理解這一點,我們需要對比傳統 I/O 和使用直接內存的 NIO 的區別。
a) 傳統的 I/O 操作(使用堆內存)
- Java 程序發起讀操作,需要在**堆(Heap)**上創建一個
byte[]
緩沖區。 - 操作系統從磁盤或網卡讀取數據,先將數據放入操作系統內核的緩沖區中。
- 然后,數據從內核緩沖區被復制到 Java 堆上的
byte[]
緩沖區中。 - 之后,Java 程序才能處理堆上的這些數據。
缺點:在這個過程中,數據存在一次不必要的拷貝(從內核空間 -> JVM 堆空間)。當處理大量數據時,這次拷貝會帶來明顯的性能開銷和 CPU 占用。
b) 使用直接內存的 I/O 操作 (NIO)
- Java 程序通過
ByteBuffer.allocateDirect()
在堆外分配一塊直接內存。 - Java 程序發起讀操作,操作系統直接將數據從磁盤或網卡讀入這塊直接內存中。
- Java 程序直接操作這塊內存。
優點:這個過程省去了從內核空間到 JVM 堆空間的拷貝。數據直接在操作系統和應用程序的“共享區域”(直接內存)中傳遞,大大減少了數據拷貝次數,提升了 I/O 效率。這種機制通常被稱為**零拷貝(Zero-Copy)**的一種形式。
一句話總結動機:通過在堆外分配內存,避免了 JVM 堆與操作系統內核之間的數據拷貝,從而提高了 I/O 性能。
3. 直接內存的管理
分配與釋放
- 分配:通過
ByteBuffer.allocateDirect()
。這個操作的成本比在堆上分配對象要高,因為它涉及到對操作系統的調用。 - 釋放:這是一個關鍵點。直接內存不受 JVM GC 的直接管理。它的回收依賴于一個巧妙的機制:
DirectByteBuffer
對象本身是普通的 Java 對象,存放在堆上,受 GC 管理。- 當
DirectByteBuffer
對象沒有任何引用,即將被 GC 回收時,JVM 會通過一種叫做Cleaner
(在舊版本中是PhantomReference
和Finalizer
) 的機制來觸發一個操作。 - 這個操作會調用底層的 C 代碼(例如
Unsafe.freeMemory()
),最終釋放掉DirectByteBuffer
對象所引用的那塊堆外直接內存。
潛在風險
這種間接的回收機制可能帶來一個嚴重的問題:OutOfMemoryError: Direct buffer memory
。
- 原因:如果程序大量分配直接內存,但此時 Java 堆內存的使用率很低,導致遲遲沒有觸發垃圾回收(特別是 Full GC)。那么,大量的
DirectByteBuffer
對象就不會被回收,其關聯的直接內存也得不到釋放。最終,即使堆內存還很充足,直接內存也可能被耗盡,從而拋出 OOM 異常。
4. 直接內存的特點總結
優點 👍 | 缺點 👎 |
---|---|
I/O 性能高:減少了數據拷貝,特別適合網絡編程(如 Netty)、文件讀寫等場景。 | 分配/釋放開銷大:向操作系統申請內存比在堆上分配對象更慢。 |
減少 GC 影響:由于大塊數據在堆外,Full GC 時無需掃描和移動這部分數據,可以降低 GC 暫停時間(STW)。 | 內存管理復雜:回收不及時可能導致內存泄漏或 OOM,排查問題相對困難。 |
可用于進程間共享:雖然 Java 本身不直接支持,但一些高級用法可以利用直接內存實現進程間通信。 | 不受 JVM 堆大小限制:它的上限由 -XX:MaxDirectMemorySize 參數控制(默認與 -Xmx 相同),但最終受限于物理內存。 |
5. 什么時候使用直接內存?
- 需要進行大量 I/O 操作的場景,例如:
- 高性能網絡框架(Netty、Mina 等底層都廣泛使用了直接內存)。
- 需要與本地代碼(JNI)進行大量數據交換。
- 頻繁讀寫大文件。
- 當數據需要長期駐留內存且不希望受 GC 影響時。
總而言之,直接內存是 JVM 提供的一把性能優化的“利劍”,它通過犧牲一定的安全性和易用性,換取了在特定場景下(主要是 I/O)的極致性能。
垃圾回收
如何判斷對象可以回收
- 引用計數法
- 可達性分析算法
四種引用
1. 強引用(Strong Reference)
這是我們日常編程中最常見、最普通的引用類型。
- 定義:通過
new
關鍵字創建對象,并將其賦值給一個引用變量時,這個變量就是對該對象的強引用。Object obj = new Object(); // obj 就是一個強引用,指向新創建的 Object 實例
- GC 行為:只要一個對象存在強引用,垃圾回收器就永遠不會回收它,即使系統內存嚴重不足,寧可拋出
OutOfMemoryError
異常也不會回收。 - 生命周期:對象的生命周期由強引用的作用域決定。要讓對象被回收,必須將所有指向它的強引用斷開,例如設置為
null
。obj = null; // 斷開強引用,現在對象可以被 GC 回收了
- 用途:程序中絕大多數對象的存活都依賴于強引用。
2. 軟引用(Soft Reference)
軟引用用來描述一些還有用,但非必需的對象。
-
定義:通過
java.lang.ref.SoftReference
類來實現。Object obj = new Object(); SoftReference<Object> softRef = new SoftReference<>(obj); obj = null; // 斷開強引用,現在對象只被軟引用關聯
-
GC 行為:當系統內存充足時,垃圾回收器不會回收被軟引用的對象。只有在系統內存即將耗盡(即將發生
OutOfMemoryError
)時,垃圾回收器才會回收這些對象。 -
生命周期:可以存活多次 GC,直到內存緊張時才被回收。
-
用途:最適合的場景是實現內存敏感的高速緩存(Memory-sensitive Cache)。
- 例如,一個圖片瀏覽器可以把加載過的圖片用軟引用緩存起來。內存夠用時,用戶再次打開圖片就能立刻顯示。內存不夠時,GC 會自動回收這些圖片緩存,避免程序崩潰,程序只需重新從磁盤加載即可。
-
引用隊列:配合引用隊列回收軟引用對象
3. 弱引用(Weak Reference)
弱引用的強度比軟引用更弱,它描述的是可有可無的對象。
- 定義:通過
java.lang.ref.WeakReference
類來實現。Object obj = new Object(); WeakReference<Object> weakRef = new WeakReference<>(obj); obj = null; // 斷開強引用,現在對象只被弱引用關聯
- GC 行為:無論當前內存是否充足,只要垃圾回收器開始工作,被弱引用的對象就一定會被回收。 它的生存期只能持續到下一次垃圾回收發生之前。
- 生命周期:非常短暫,一次 GC 就可能“陣亡”。
- 用途:
- 防止內存泄漏:在一些監聽器(Listener)或回調(Callback)模式中非常有用。如果一個對象被一個長生命周期的集合所持有,但我們希望在該對象沒有其他強引用時能被及時回收,就可以使用弱引用。
WeakHashMap
就是一個典型的應用。 - ThreadLocal:
ThreadLocalMap
的鍵(Key)就是對ThreadLocal
實例的弱引用。這樣,當外部不再有對ThreadLocal
實例的強引用時,即使線程還在運行,這個鍵也會被回收,從而避免了內存泄漏。
- 防止內存泄漏:在一些監聽器(Listener)或回調(Callback)模式中非常有用。如果一個對象被一個長生命周期的集合所持有,但我們希望在該對象沒有其他強引用時能被及時回收,就可以使用弱引用。
弱引用中,若 new Object() 被回收后,weakRef 的值是不是為 null
答案是:weakRef
的值不會變為 null
,但是調用 weakRef.get()
的結果會變為 null
。
我們來拆解一下這個過程,這能幫助你更深刻地理解引用的概念。
深入解析
我們回顧一下代碼:
// 1. obj 是對 new Object() 的強引用
Object obj = new Object();// 2. weakRef 是對 new WeakReference<>() 這個“包裝盒”對象的強引用
// 這個“包裝盒”里面用弱引用的方式裝著最初的 new Object()
WeakReference<Object> weakRef = new WeakReference<>(obj);// 3. 斷開唯一的強引用
obj = null;
在這里,你需要區分兩個東西:
- 被引用的對象 (Referent):即
new Object()
創建的那個實例。我們稱它為“目標對象”。 - 引用對象本身 (Reference Object):即
new WeakReference<>()
創建的那個實例。它像一個包裝盒,里面裝著目標對象的地址。我們這里的變量weakRef
就是對這個“包裝盒”的強引用。
當垃圾回收發生時:
- GC 發現“目標對象” (
new Object()
) 已經沒有任何強引用指向它了,只有一個來自weakRef
這個“包裝盒”的弱引用。 - 根據弱引用的規則,GC 決定回收“目標對象”,釋放它占用的內存。
- 在回收的同時,GC 會清空
weakRef
這個“包裝盒”里的內容(即目標對象的地址)。 - 此時,
weakRef
這個變量仍然存在,它仍然強引用著那個WeakReference
“包裝盒”對象。所以,weakRef
本身絕對不是null
。 - 但是,因為“包裝盒”里的東西已經被清空了,所以當你試圖通過
weakRef.get()
去獲取“目標對象”時,就會得到null
。
一個生動的比喻
new Object()
是一個氣球。Object obj = ...
是你手里緊緊攥著氣球的繩子(強引用)。WeakReference<Object> weakRef = ...
是你把繩子的另一端輕輕地掛在一個鉤子上(弱引用),而weakRef
變量是你對這個鉤子本身的引用。
現在,你松開了手里緊攥的繩子 (obj = null;
)。
一陣風吹來(GC 執行),風的力量足以吹走那個只是被輕輕掛在鉤子上的氣球。
結果是:
- 氣球 (
new Object()
) 飛走了(被回收)。 - 鉤子 (
weakRef
這個WeakReference
對象) 還好好地在墻上 (weakRef != null
)。 - 但你再去看鉤子上時,已經沒有氣球了 (
weakRef.get() == null
)。
代碼驗證
我們可以寫一小段代碼來清晰地驗證這一點:
import java.lang.ref.WeakReference;public class WeakReferenceTest {public static void main(String[] args) {// 創建一個對象,并用強引用指向它Object myObject = new Object();System.out.println("1. 目標對象剛創建: " + myObject);// 創建一個弱引用,指向這個對象WeakReference<Object> weakRef = new WeakReference<>(myObject);System.out.println("2. weakRef 包裝盒對象: " + weakRef);System.out.println(" 通過 get() 獲取目標對象: " + weakRef.get());// 斷開唯一的強引用myObject = null;System.out.println("\n3. 斷開強引用后...");// 建議 JVM 進行垃圾回收System.gc();System.out.println("\n4. GC 之后...");System.out.println(" weakRef 包裝盒對象是否為 null? " + (weakRef == null)); // 輸出 falseSystem.out.println(" 通過 get() 獲取目標對象: " + weakRef.get()); // 輸出 null}
}
輸出結果:
1. 目標對象剛創建: java.lang.Object@1b6d3586
2. weakRef 包裝盒對象: java.lang.ref.WeakReference@4554617c通過 get() 獲取目標對象: java.lang.Object@1b6d35863. 斷開強引用后...4. GC 之后...weakRef 包裝盒對象是否為 null? false通過 get() 獲取目標對象: null
這個結果清晰地證明了我們的結論:weakRef
變量本身不為 null
,但其內部引用的對象在 GC 后無法再通過 get()
方法訪問到。
4. 虛引用(Phantom Reference)
虛引用也稱“幻影引用”或“幽靈引用”,是所有引用類型中最弱的一種。
- 定義:通過
java.lang.ref.PhantomReference
類來實現。它必須和一個**引用隊列(ReferenceQueue)**聯合使用。Object obj = new Object(); ReferenceQueue<Object> queue = new ReferenceQueue<>(); PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue); obj = null;
- GC 行為:
- 一個對象是否有虛引用,完全不影響其生命周期。在任何時候,對象都可能被垃圾回收器回收。
- 通過虛引用的
get()
方法永遠無法獲取到對象實例,phantomRef.get()
總是返回null
。
- 唯一作用:跟蹤對象被垃圾回收的狀態。當一個被虛引用關聯的對象即將被回收時,JVM 會在回收該對象之前,將這個虛引用對象本身(
phantomRef
)放入與之關聯的引用隊列(queue
)中。 - 用途:主要用于管理堆外內存(Direct Memory)。
- 例如
DirectByteBuffer
,當它的 Java 對象被回收時,我們需要一個可靠的通知機制來釋放它所占用的本地內存(Native Memory)。通過為其設置一個虛引用和引用隊列,一個專門的線程可以監視這個隊列。一旦從隊列中獲取到了虛引用,就說明其關聯的 Java 對象已被回收,此時就可以安全地執行本地內存的釋放操作。這種方式比finalize()
方法更可靠、更高效。
- 例如
總結對比
引用類型 | 強度 | 回收時機 | 主要用途 |
---|---|---|---|
強引用 (Strong) | 最強 | 從不回收(除非引用斷開) | 普通的對象引用,程序的基石 |
軟引用 (Soft) | 較強 | 內存不足時回收 | 實現內存敏感的緩存 |
弱引用 (Weak) | 較弱 | 下一次 GC 時必定回收 | 防止內存泄漏,如 WeakHashMap |
虛引用 (Phantom) | 最弱 | get() 永遠返回 null ,只作通知 | 跟蹤對象回收狀態,管理堆外內存 |
垃圾回收算法
-
標記清除
-
標記整理
-
復制
to
總是空閑的一塊空間
分代垃圾回收
垃圾回收器
串行
吞吐量優先
響應時間優先
初始標記,指的是尋找所有被 GCRoots 引用的對象,該階段需要「Stop the World」。這個步驟僅僅只是標記一下 GC Roots 能直接關聯到的對象,并不需要做整個引用的掃描,因此速度很快。
并發標記,指的是對「初始標記階段」標記的對象進行整個引用鏈的掃描,該階段不需要「Stop the World」。 對整個引用鏈做掃描需要花費非常多的時間,因此通過垃圾回收線程與用戶線程并發執行,可以降低垃圾回收的時間。
這也是 CMS 能極大降低 GC 停頓時間的核心原因,但這也帶來了一些問題,即:并發標記的時候,引用可能發生變化,因此可能發生漏標(本應該回收的垃圾沒有被回收)和多標(本不應該回收的垃圾被回收)了。
重新標記,指的是對「并發標記」階段出現的問題進行校正,該階段需要「Stop the World」。正如并發標記階段說到的,由于垃圾回收算法和用戶線程并發執行,雖然能降低響應時間,但是會發生漏標和多標的問題。所以對于 CMS 來說,它需要在這個階段做一些校驗,解決并發標記階段發生的問題。
并發清除,指的是將標記為垃圾的對象進行清除,該階段不需要「Stop the World」。 在這個階段,垃圾回收線程與用戶線程可以并發執行,因此并不影響用戶的響應時間。
G1
G1(Garbage-First Garbage Collector)在 JDK 1.7 時引入,在 JDK 9 時取代 CMS 成為了默認的垃圾收集器。G1 有五個屬性:分代、增量、并行、標記整理、STW。
①、分代:相信大家還記得我們上一講中的年輕代和老年代,G1 也是基于這個思想進行設計的。它將堆內存分為多個大小相等的區域(Region),每個區域都可以是 Eden 區、Survivor 區或者 Old 區。
可以通過 -XX:G1HeapRegionSize=n 來設置 Region 的大小,可以設定為 1M、2M、4M、8M、16M、32M(不能超過)。
G1 有專門分配大對象的 Region 叫 Humongous 區,而不是讓大對象直接進入老年代的 Region 中。在 G1 中,大對象的判定規則就是一個大對象超過了一個 Region 大小的 50%,比如每個 Region 是 2M,只要一個對象超過了 1M,就會被放入 Humongous 中,而且一個大對象如果太大,可能會橫跨多個 Region 來存放。
G1 會根據各個區域的垃圾回收情況來決定下一次垃圾回收的區域,這樣就避免了對整個堆內存進行垃圾回收,從而降低了垃圾回收的時間。
②、增量:G1 可以以增量方式執行垃圾回收,這意味著它不需要一次性回收整個堆空間,而是可以逐步、增量地清理。有助于控制停頓時間,尤其是在處理大型堆時。
③、并行:G1 垃圾回收器可以并行回收垃圾,這意味著它可以利用多個 CPU 來加速垃圾回收的速度,這一特性在年輕代的垃圾回收(Minor GC)中特別明顯,因為年輕代的回收通常涉及較多的對象和較高的回收速率。
④、標記整理:在進行老年代的垃圾回收時,G1 使用標記-整理算法。這個過程分為兩個階段:標記存活的對象和整理(壓縮)堆空間。通過整理,G1 能夠避免內存碎片化,提高內存利用率。
年輕代的垃圾回收(Minor GC)使用復制算法,因為年輕代的對象通常是朝生夕死的。
⑤、STW:G1 也是基于「標記-清除」算法,因此在進行垃圾回收的時候,仍然需要「Stop the World」。不過,G1 在停頓時間上添加了預測機制,用戶可以指定期望停頓時間。
G1 中存在三種 GC 模式,分別是 Young GC、Mixed GC 和 Full GC。
當 Eden 區的內存空間無法支持新對象的內存分配時,G1 會觸發 Young GC。
當需要分配對象到 Humongous 區域或者堆內存的空間占比超過 -XX:G1HeapWastePercent 設置的 InitiatingHeapOccupancyPercent 值時,G1 會觸發一次 concurrent marking,它的作用就是計算老年代中有多少空間需要被回收,當發現垃圾的占比達到 -XX:G1HeapWastePercent 中所設置的 G1HeapWastePercent 比例時,在下次 Young GC 后會觸發一次 Mixed GC。
Mixed GC 是指回收年輕代的 Region 以及一部分老年代中的 Region。Mixed GC 和 Young GC 一樣,采用的也是復制算法。
在 Mixed GC 過程中,如果發現老年代空間還是不足,此時如果 G1HeapWastePercent 設定過低,可能引發 Full GC。-XX:G1HeapWastePercent 默認是 5,意味著只有 5% 的堆是“浪費”的。如果浪費的堆的百分比大于 G1HeapWastePercent,則運行 Full GC。
在以 Region 為最小管理單元以及所采用的 GC 模式的基礎上,G1 建立了停頓預測模型,即 Pause Prediction Model 。這也是 G1 非常被人所稱道的特性。
我們可以借助 -XX:MaxGCPauseMillis 來設置期望的停頓時間(默認 200ms),G1 會根據這個值來計算出一個合理的 Young GC 的回收時間,然后根據這個時間來制定 Young GC 的回收計劃。
ZGC
ZGC 很牛逼,它的目標是:
- 停頓時間不超過 10ms;
- 停頓時間不會隨著堆的大小,或者活躍對象的大小而增加;
- 支持 8MB~4TB 級別的堆,未來支持 16TB。
前面講 G1 垃圾收集器的時候提到過,Young GC 和 Mixed GC 均采用的是復制算法,復制算法主要包括以下 3 個階段:
①、標記階段,從 GC Roots 開始,分析對象可達性,標記出活躍對象。
②、對象轉移階段,把活躍對象復制到新的內存地址上。
③、重定位階段,因為轉移導致對象地址發生了變化,在重定位階段,所有指向對象舊地址的引用都要調整到對象新的地址上。
標記階段因為只標記 GC Roots,耗時較短。但轉移階段和重定位階段需要處理所有存活的對象,耗時較長,并且轉移階段是 STW 的,因此,G1 的性能瓶頸就主要卡在轉移階段。
與 G1 和 CMS 類似,ZGC 也采用了復制算法,只不過做了重大優化,ZGC 在標記、轉移和重定位階段幾乎都是并發的,這是 ZGC 實現停頓時間小于 10ms 的關鍵所在。
ZGC 是怎么做到的呢?
- 指針染色(Colored Pointer):一種用于標記對象狀態的技術。
- 讀屏障(Load Barrier):一種在程序運行時插入到對象訪問操作中的特殊檢查,用于確保對象訪問的正確性。
這兩種技術可以讓所有線程在并發的條件下就指針的顏色 (狀態) 達成一致,而不是對象地址。因此,ZGC 可以并發的復制對象,這大大的降低了 GC 的停頓時間。
指針染色
在一個指針中,除了存儲對象的實際地址外,還有額外的位被用來存儲關于該對象的元數據信息。這些信息可能包括:
- 對象是否被移動了(即它是否在回收過程中被移動到了新的位置)。
- 對象的存活狀態。
- 對象是否被鎖定或有其他特殊狀態。
通過在指針中嵌入這些信息,ZGC 在標記和轉移階段會更快,因為通過指針上的顏色就能區分出對象狀態,不用額外做內存訪問。
ZGC僅支持64位系統,它把64位虛擬地址空間劃分為多個子空間,如下圖所示:
其中,0-4TB 對應 Java 堆,4TB-8TB 被稱為 M0 地址空間,8TB-12TB 被稱為 M1 地址空間,12TB-16TB 預留未使用,16TB-20TB 被稱為 Remapped 空間。
當創建對象時,首先在堆空間申請一個虛擬地址,該虛擬地址并不會映射到真正的物理地址。同時,ZGC 會在 M0、M1、Remapped 空間中為該對象分別申請一個虛擬地址,且三個虛擬地址都映射到同一個物理地址。
不過,三個空間在同一時間只有一個空間有效。ZGC 之所以設置這三個虛擬地址,是因為 ZGC 采用的是“空間換時間”的思想,去降低 GC 的停頓時間。
與上述地址空間劃分相對應,ZGC實際僅使用64位地址空間的第0-41位,而第42-45位存儲元數據,第47-63位固定為0。
由于僅用了第 0~43 位存儲對象地址,
= 16TB,所以 ZGC 最大支持 16TB 的堆。
至于對象的存活信息,則存儲在42-45位中,這與傳統的垃圾回收并將對象存活信息放在對象頭中完全不同。
讀屏障
當程序嘗試讀取一個對象時,讀屏障會觸發以下操作:
- 檢查指針染色:讀屏障首先檢查指向對象的指針的顏色信息。
- 處理移動的對象:如果指針表示對象已經被移動(例如,在垃圾回收過程中),讀屏障將確保返回對象的新位置。
- 確保一致性:通過這種方式,ZGC 能夠在并發移動對象時保持內存訪問的一致性,從而減少對應用程序停頓的需要。
ZGC讀屏障如何實現呢?
來看下面這段偽代碼,涉及 JVM 的底層 C++ 代碼:
// 偽代碼示例,展示讀屏障的概念性實現
Object* read_barrier(Object* ref) {if (is_forwarded(ref)) {return get_forwarded_address(ref); // 獲取對象的新地址}return ref; // 對象未移動,返回原始引用
}
- read_barrier 代表讀屏障。
- 如果對象已被移動(is_forwarded(ref)),方法返回對象的新地址(get_forwarded_address(ref))。
- 如果對象未被移動,方法返回原始的對象引用。
讀屏障可能被GC線程和業務線程觸發,并且只會在訪問堆內對象時觸發,訪問的對象位于GC Roots時不會觸發,這也是掃描GC Roots時需要STW的原因。
下面是一個簡化的示例代碼,展示了讀屏障的觸發時機。
Object o = obj.FieldA // 從堆中讀取引用,需要加入屏障
<Load barrier>
Object p = o // 無需加入屏障,因為不是從堆中讀取引用
o.dosomething() // 無需加入屏障,因為不是從堆中讀取引用
int i = obj.FieldB //無需加入屏障,因為不是對象引用
ZGC 的工作過程
ZGC 周期由三個 STW 暫停和四個并發階段組成:標記/重新映射( M/R )、并發引用處理( RP )、并發轉移準備( EC ) 和并發轉移( RE )。
Stop-The-World 暫停階段
-
標記開始(Mark Start)STW 暫停:這是 ZGC 的開始,進行 GC Roots 的初始標記。在這個短暫的停頓期間,ZGC 標記所有從 GC Root 直接可達的對象。
-
重新映射開始(Relocation Start)STW 暫停:在并發階段之后,這個 STW 暫停是為了準備對象的重定位。在這個階段,ZGC 選擇將要清理的內存區域,并建立必要的數據結構以進行對象移動。
-
暫停結束(Pause End)STW 暫停:ZGC 結束。在這個短暫的停頓中,完成所有與該 GC 周期相關的最終清理工作。
并發階段
-
并發標記/重新映射 (M/R) :這個階段包括并發標記和并發重新映射。在并發標記中,ZGC 遍歷對象圖,標記所有可達的對象。然后,在并發重新映射中,ZGC 更新指向移動對象的所有引用。
-
并發引用處理 (RP) :在這個階段,ZGC 處理各種引用類型(如軟引用、弱引用、虛引用和幽靈引用)。這些引用的處理通常需要特殊的考慮,因為它們與對象的可達性和生命周期密切相關。
-
并發轉移準備 (EC) :這是為對象轉移做準備的階段。ZGC 確定哪些內存區域將被清理,并準備相關的數據結構。
-
并發轉移 (RE) :在這個階段,ZGC 將存活的對象從舊位置移動到新位置。由于這一過程是并發執行的,因此應用程序可以在大多數垃圾回收工作進行時繼續運行。
ZGC 的兩個關鍵技術:指針染色 和 讀屏障,不僅應用在并發轉移階段,還應用在并發標記階段:將對象設置為已標記,傳統的垃圾回收器需要進行一次內存訪問,并將對象存活信息放在對象頭中;而在ZGC中,只需要設置指針地址的第42-45位即可,并且因為是寄存器訪問,所以速度比訪問內存更快。
類加載與字節碼技術
類文件結構
好的,我們來深入探討一下 Java 虛擬機(JVM)中一個非常基礎且核心的概念——類文件結構(Class File Structure)。
每一個 .java
文件經過 Java 編譯器(javac
)編譯后,都會生成一個對應的 .class
文件。這個 .class
文件并不只是簡單的字節碼指令集合,而是一個遵循著《Java虛擬機規范》嚴格定義的、高度結構化的二進制文件。正是這種與平臺無關的嚴謹結構,才使得 Java 能夠實現“一次編譯,到處運行”(Write Once, Run Anywhere)。
可以把 .class
文件想象成一份詳細的“建筑藍圖”,JVM 就是根據這份藍圖來加載類、創建對象并執行代碼的。
類文件是一個由 8 位字節組成的二進制流。它的內部數據項嚴格按照預定義的順序排列,沒有任何分隔符。整個文件結構就像一個緊湊的C語言結構體。
文件中的數據類型可以分為兩類:
- 無符號數:以
u1
,u2
,u4
,u8
分別代表 1、2、4、8 個字節的無符號數。它們用來表示數字、索引、數量等。 - 表(Table):由多個無符號數或其他表作為數據項構成的復合結構。整個類文件本質上就是一張大表。
一個標準的類文件結構按順序包含以下部分:
數據項名稱 | 數據類型 | 描述 |
---|---|---|
magic | u4 | 魔數,固定為 0xCAFEBABE |
minor_version | u2 | 次版本號 |
major_version | u2 | 主版本號 |
constant_pool_count | u2 | 常量池大小 |
constant_pool[] | cp_info | 常量池內容 |
access_flags | u2 | 類的訪問標志 |
this_class | u2 | 類索引 |
super_class | u2 | 父類索引 |
interfaces_count | u2 | 接口數量 |
interfaces[] | u2 | 接口索引集合 |
fields_count | u2 | 字段數量 |
fields[] | field_info | 字段表集合 |
methods_count | u2 | 方法數量 |
methods[] | method_info | 方法表集合 |
attributes_count | u2 | 屬性數量 |
attributes[] | attribute_info | 屬性表集合 |
二、核心組件詳解
1. 魔數 (Magic Number)
- 作用:每個
.class
文件的頭 4 個字節都是0xCAFEBABE
。這是一個十六進制的“魔數”,用于快速地識別一個文件是否是可能被 JVM 接受的類文件。如果不是這個值,JVM 會直接拒絕加載。 - 趣聞:這個詞是 Java 創始人 James Gosling 創造的,據說是他在一家咖啡館時想到的,所以包含了 Cafe(咖啡)和 Babe(寶貝)的組合。
2. 版本號 (Version)
- 作用:緊跟魔數的是次版本號和主版本號,它們共同標識了該類文件的編譯器版本。JVM 會拒絕加載版本號高于自身的類文件,但可以兼容執行版本號較低的類文件。
3. 常量池 (Constant Pool)
- 作用:這是類文件結構中最核心、最龐大的數據項目。可以把它理解為這個類的“資源倉庫”或“符號表”。
- 內容:它存儲了兩大類常量:
- 字面量 (Literals):如文本字符串(
"Hello, World!"
)、final
常量值(final int a = 10;
)等。 - 符號引用 (Symbolic References):這是它的主要部分,包含了對類、接口、字段和方法的描述信息。例如:
- 類的全限定名 (
java/lang/Object
) - 字段的名稱和描述符 (
name
,Ljava/lang/String;
) - 方法的名稱和描述符 (
main
,([Ljava/lang/String;)V
)
- 類的全限定名 (
- 字面量 (Literals):如文本字符串(
- 意義:在加載時,JVM 通過這些符號引用來找到對應的實際內存地址(這個過程叫動態鏈接),從而將各個類聯系起來。
4. 訪問標志 (Access Flags)
- 作用:這是一個 2 字節的位掩碼,用于表示這個類或接口的訪問權限和屬性,比如
ACC_PUBLIC
(是否為 public)、ACC_FINAL
(是否為 final)、ACC_INTERFACE
(是否為接口)、ACC_ABSTRACT
(是否為抽象類) 等。
5. 類、父類和接口索引
- 作用:這三項分別指向常量池中的一個
CONSTANT_Class_info
常量,通過這個索引可以找到本類、父類和所實現接口的全限定名。
6. 字段表 (Fields Table)
- 作用:描述類中聲明的所有字段(成員變量),包括靜態變量和實例變量。
- 內容:每個字段的信息包括:訪問標志(public, private, static, final 等)、字段名索引、字段描述符索引(例如
I
代表int
,Ljava/lang/String;
代表String
類型)以及可能的屬性(如ConstantValue
屬性用于 final 靜態變量)。
7. 方法表 (Methods Table)
- 作用:描述類中聲明的所有方法。
- 內容:每個方法的信息包括:訪問標志(public, static, synchronized 等)、方法名索引、方法描述符索引(例如
()V
代表無參無返回值的構造函數)以及屬性表。 - 核心中的核心——
Code
屬性:如果一個方法不是抽象的或本地的,那么它的屬性表中必定有一個**Code
屬性**。Code
屬性里存放了該方法的Java 字節碼指令、操作數棧的最大深度、局部變量表的大小等執行代碼所需的一切信息。
8. 屬性表 (Attributes Table)
- 作用:這是最具擴展性的部分,用于描述類、字段或方法的一些額外信息。JVM 規范預定義了很多屬性,同時也允許編譯器廠商自定義屬性。
- 常見屬性:
Code
: 存放方法的字節碼。SourceFile
: 記錄生成此 class 文件的源文件名(如MyClass.java
),用于異常堆棧的顯示。LineNumberTable
: 建立字節碼行號與 Java 源碼行號的對應關系,是調試器和異常堆棧定位的關鍵。Exceptions
: 列出方法聲明中throws
的所有受查異常。
三、如何查看類文件結構?
我們不需要用十六進制編輯器去手動分析。JDK 提供了一個強大的工具——javap
(Java Class File Disassembler)。
假設有這樣一個簡單的類:
public class Test {public void sayHello() {System.out.println("Hello, JVM!");}
}
編譯后,在命令行中執行 javap -v Test.class
,你將看到一個非常詳細的、格式化的類文件結構報告,它會清晰地展示出魔數、版本、常量池、方法表、Code屬性中的字節碼等所有我們上面提到的內容。這是學習和理解類文件結構最直觀的方式。
多態原理
catch
類加載
加載
連接
- 驗證
- 準備
- 為靜態變量分配空間,分配空間在準備階段完成,賦值在初始化階段完成
- 如果 static 變量是 final 的基本類型,以及字符串常量,那么編譯階段值就確定了,賦值在準備階段完成
- 如果 static 變量是 fina l的,但屬于引用類型,那么賦值也會在初始化階段完成
- 為靜態變量分配空間,分配空間在準備階段完成,賦值在初始化階段完成
- 解析
- 將常量池中的符號引用解析為直接引用
初始化
初始化即調用<cint>()v, 虛擬機會保證這個類的『構造方法』的線程安全
懶加載
public class Singleton {private Singleton(){}public static class LazyHolder {private static final Singleton INSTANCE = new Singleton();static {System.out.println("已被實例化");}}public static Singleton getInstance() {return LazyHolder.INSTANCE;}
}
類加載器
雙親委派
線程上下文類加載器
1、我來總結下,在 jre/lib 包下有一個 DriverManager,是啟動類加載的,但是jdbc的驅動是各個廠商來實現的不在啟動類加載路徑下,啟動類無法加載,而驅動管理需要用到這些驅動
2、只能打破雙親委派,啟動類直接請求系統類加載器去classpath下加載驅動(正常是向上委托,這個反過來了),而打破雙親委派的就是這個線程上下文類加載器
3、過程就是:啟動類加載器加載 DriverManager,DriverManager 代碼里調用了線程上下文類加載器,這個加載器默認就是使用應用程序類加載器加載類,通過應用程序類加載器加載 jdbc驅動