文章目錄
- 1. 程序計數器
- 補充
- 2. 虛擬機棧
- 2.1 棧幀
- 1. 局部變量表
- 2. 操作數棧
- 3. 動態鏈接
- 4. 方法返回地址
- 補充
- 3. 本地方法棧
- 4. 堆
- 5. 方法區
- 靜態常量池(Class常量池)
- 運行時常量池
- 字符串常量池
- (1)位置變化
- (2)放入字符串常量池的3種途徑
- 1. 字面量賦值
- 2. new String("")
- 3. **intern()**
- 4. StringTable垃圾回收
- 5. StringTable調優
- 6. 直接內存
- (1)概念
- (2)特性
- (3)管理與回收機制
- 一、直接內存的分配與對象結構
- 二、釋放流程
- 三、關鍵機制深度解析
- 其他
- 1. 永久代為什么被替換為元空間
1. 程序計數器
(1)定義
一塊比較小的內存空間,可以看作是當前線程所執行的字節碼的行號指示器。
- JVM解釋器通過程序計數器讀取下一條需要執行的字節碼指令。
- 在Java虛擬機的概念模型里(代表了虛擬機的統一外觀,但各個Java虛擬機不一定一定要按照概念模型的定義來實現),字節碼解釋器的工作就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令。
- 它是程序控制流的指示器,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器實現。
(2)特點
-
線程私有
內存隔離:程序計數器屬于線程私有內存
作用:避免多線程競爭,確保線程獨立性。 -
無OOM異常
JVM規范未定義程序計數器的OOM(OutOfMemoryError)場景。
因為它不占用堆或方法區,僅存儲當前指令的地址。 -
占用較小內存
僅保存一個指令地址(或Native方法的undefined),內存消耗可忽略。 -
無垃圾回收
不涉及對象存儲,無需GC管理。
補充
-
這個線程如果正在執行的是一個Java方法,那么這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是一個本地(Native)方法,這個計數器值應該為空
-
程序計數器是否可能為null?
- Java方法執行時:始終指向有效指令地址
- Native方法執行時:值為undefined,但不為null
-
工作流程:
0: iload_1 // 加載局部變量1到操作數棧 1: iload_2 // 加載局部變量2到操作數棧 2: iadd // 執行加法 3: istore_3 // 將結果存儲到局部變量3
- 當線程執行到iadd(地址2)時,程序計數器值為2。
- 執行完iadd后,程序計數器自動更新為3,指向istore_3。
2. 虛擬機棧
(1)定義
用于存儲方法的調用和執行信息(保存方法的局部變量、操作數棧、動態鏈接、方法返回地址等)。每個方法調用對應一個棧幀(Stack Frame)。
虛擬機棧由一個個棧幀組成,每個方法在運行時,JVM都會同步創建一個棧幀,然后將棧幀壓入到虛擬機棧中。每次方法調用的數據都是通過棧傳遞的。
(2)異常
-
棧內存溢出(棧幀過多或棧幀過大):
StackOverflowError
- 觸發條件:線程請求的棧深度超過JVM允許的最大深度(如無限遞歸調用)
- 默認棧大小:不同JVM實現不同,HotSpot默認為1MB(可通過
-Xss
參數調整,如-Xss256k
)
-
OutOfMemoryError
- 觸發條件:如果Java虛擬機棧容量可以動態擴展,棧擴展時無法申請到足夠內存(如多線程場景下系統內存耗盡)
2.1 棧幀
每一個方法被調用到執行完畢的過程,就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。
每個棧幀存儲以下信息:
1. 局部變量表
存放的是編譯器可知的各種基本數據類型、對象引用和returnAddress類型。
- A. 對象引用(reference類型,不等同于對象本身)
- B. returnAddress類型(指向了一條字節碼指令的地址)
局部變量表的容量:以變量槽(Slot)為最小單位(32位類型占1個Slot,64位如long/double占2個Slot,其余的數據類型只占用1個)。
2. 操作數棧
用于執行字節碼指令的臨時數據存儲區(如算術運算、方法參數傳遞)。
i++ 和 ++i的區別:
- i++:從局部變量表取出局部變量 i 并壓入操作棧后,對局部變量表中的 i 自增 1。線程取出使用操作數棧棧頂值的自增前的值。
- ++i:先對局部變量表的 i 自增 1,然后取出并壓入操作棧,線程再從操作棧棧頂值取出自增之后的值使用。
3. 動態鏈接
4. 方法返回地址
方法退出的過程就是棧幀在虛擬機棧上的出棧過程,因此退出時的操作可能有:恢復上層方法的局部變量表和操作數棧,把返回值壓入調用者的操作數棧,每條整pc計數器的值指向調用該方法的后一條指令。
補充
-
垃圾回收是否涉及棧內存?
- 垃圾回收主要針對的是堆內存。棧內存的生命周期由方法調用和返回自動管理,因此不需要GC機制介入。
-
棧內存分配越大越好嗎?
- 優點:較大的棧內存可以允許更深的遞歸調用和更復雜的調用棧
- 缺點:每個線程占用更多內存,容易耗盡總內存資源
-
方法內的局部變量是否線程安全?
- 通常線程安全,因為每個線程有獨立的棧空間。
- 例外:若局部變量引用了可變對象并被多個線程共享,則需要同步控制。
3. 本地方法棧
(1)定義
本地方法棧(Native Method Stacks)與虛擬機棧所發揮的作用是非常相似的:
- 虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務
- 本地方法棧則是為虛擬機使用到的本地(Native)方法服務
(2)作用
為 JVM 執行 Native 方法(非 Java 代碼實現的方法,如 C/C++ 編寫的 JNI 方法)提供內存空間。
(3)歸屬
線程私有,每個線程在創建時都會分配獨立的本地方法棧。
(4)棧合并
HotSpot 將虛擬機棧與本地方法棧合并,通過一個統一的棧結構管理。
(5)異常
StackOverflowError
:棧深度超過 JVM 限制時拋出(如遞歸調用過深)OutOfMemoryError
:棧擴展失敗時拋出(如內存不足)
4. 堆
(1)定義
JVM 中最大的內存區域,用于存儲所有對象實例和數組(通過 new 關鍵字創建的對象)。
此內存區域的唯一作用就是存放對象實例。
(2)歸屬
線程共享,所有線程均可訪問堆中的對象。
(3)生命周期
對象在堆中分配內存,由垃圾回收器(GC)自動回收(無顯式釋放)。
(4)異常
OutOfMemoryError
:堆內存不足且無法擴展時拋出(如內存泄漏或對象過多)
(5)虛擬機棧
棧幀中存儲對象的引用(指向堆中的對象實例)。
5. 方法區
(1)定義
方法區是JVM規范中定義的邏輯內存區域,用于存儲類元數據、運行時常量池、靜態變量、即時編譯器(JIT)編譯后的代碼等數據。
(2)內容
- 類元數據存儲:類名、父類、字段、方法、訪問修飾符等
- 靜態變量:類級別的static變量直接存儲在方法區
- JIT熱點代碼:編譯后的本地機器碼存儲在“代碼緩存”(Code Cache)
- 運行時常量池:動態解析符號引用,支持運行時添加常量
(3)垃圾回收
- 可以不實現垃圾回收
- 回收目標主要是常量池和類型卸載
- 方法區無法滿足內存需求時拋出
OutOfMemoryError
靜態常量池(Class常量池)
(1)定義
每個Java類被編譯后形成的 .class
文件中包含常量池信息,用于存放編譯器生成的各種字面量和符號引用。
(2)位置
保存在編譯后的 .class
文件中。
(3)存儲內容
-
字面量:
- 文本字符串(用雙引號包裹的值)
- 被聲明為final的常量
- 基本數據類型值
- null
-
符號引用:
- 類符號引用(完全限定名)
- 字段/方法的名稱和描述符
運行時常量池
(1)定義
方法區的一部分,用于存儲類文件中的常量數據(字面量)和符號引用。
(2)位置
- JDK 7 及之前:永久代(PermGen)
- JDK 8 起:Metaspace
(3)作用
- 內存優化:共享重復常量
- 加速訪問:減少解析開銷
(4)來源
- 編譯期字面量與符號引用
- 運行期動態生成常量(如
String.intern()
)
(5)結構組成
- 字符串常量池(JDK 7 起移至堆中)
- 符號引用表
(6)關鍵特性
- 字符串字面量直接引用池中對象,
new String("a")
創建堆中新對象 intern()
可強制加入池(注意性能)- 包裝類常量池(如 Integer 緩存 -128~127)
- 類加載時解析符號引用為直接引用
以下是不改動文章內容,僅轉換為 Markdown 格式后的結果:
字符串常量池
(1)位置變化
- JDK 1.6及之前:作為運行時常量池的一部分,位于永久代。
- JDK 1.7:從永久代分離,移動到堆內存中。
- JDK 1.8及之后:仍保留在堆內存中。
(2)放入字符串常量池的3種途徑
1. 字面量賦值
直接存入常量池
- 字符串變量拼接原理是 StringBuilder
- 字符串常量拼接原理是編譯期優化
String s1 = "a"; // 延遲:運行到這一步才會放入串池
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append(s1).append(s2).toString() 即 new String("ab")
String s5 = "a" + "b"; // javac在編譯器的優化
System.out.println(s3 == s4); // f
System.out.println(s3 == s5); // t
流程步驟:
- 檢查常量池:JVM 解析代碼時發現字面量
"abc"
,首先檢查字符串常量池中是否存在哈希值相同的字符串。 - 存在則復用:若池中存在
"abc"
(通過equals
確認內容相同),直接返回池中對象的引用(s1 和 s2 指向同一對象)。 - 不存在則創建:若池中無
"abc"
,在常量池中創建該字符串對象,并返回引用。
底層實現:
- 字符串常量池本質是哈希表(StringTable),默認桶數為 60013(可通過
-XX:StringTableSize
調整)。 - 哈希值由字符串內容計算,沖突時通過鏈表或紅黑樹處理。
2. new String(“”)
堆對象與池交互
String s3 = new String("abc");
String s4 = s3.intern();
System.out.println(s3 == s4); // JDK 7+ 中為 false(s4 指向池對象,s3 指向堆對象)
流程步驟:
- 堆中創建對象:
new String("abc")
會在堆中創建一個新的字符串對象。 - 處理字面量:
- 若字面量
"abc"
不在常量池中,JVM 先在池中創建"abc"
,再將堆對象指向該字面量的字符數組。 - 若池中已有
"abc"
,堆對象直接引用池中的字符數組(但堆對象本身是獨立實例)。
- 若字面量
3. intern()
String s5 = new StringBuilder("ja").append("va").toString();
System.out.println(s5.intern() == s5); // JDK 7+ 輸出 true(池中無 "java" 時)
流程步驟(以 JDK 7+ 為例):
- 檢查常量池:調用
intern()
時,檢查池中是否存在與當前字符串內容相同的對象。 - 存在則返回引用:直接返回池中對象的引用。
- 不存在則駐留:
- JDK 1.8:將當前字符串對象自身的引用直接加入常量池,并返回這個引用。
- JDK 1.6:在常量池中復制(也就是在永久代中創建一份新的字符串對象)后返回該副本的引用,這樣原來在堆中創建的字符串對象和常量池中的對象就不是同一個引用了。
4. StringTable垃圾回收
- JDK6及之前:StringTable 位于永久代(PermGen),僅在 Full GC 時回收,且永久代空間有限,易導致 OutOfMemoryError。
- JDK7及之后:StringTable 移至堆內存,隨堆的 Minor GC 或 Major GC 觸發回收,內存管理更靈活。
- StringTable 中的字符串對象會參與垃圾回收,但其哈希表結構本身不受 GC 影響。
StringTable 的底層結構
- 哈希表存儲:
StringTable 本質是一個固定大小的哈希表(HashTable),采用數組 + 鏈表(或紅黑樹)結構,默認桶數為 60013(可通過-XX:StringTableSize
調整)。
5. StringTable調優
-
調整哈希表桶數量
- StringTable 底層由 HashTable 實現,其性能與桶(Bucket)數量直接相關。桶數量越大,哈希沖突概率越低,查詢效率越高。
- 默認值:
- JDK6及之前:1009(永久代)
- JDK7+:60013(堆內存)
- 調整桶數:
-XX:StringTableSize=<size>
(JDK8+最小值為 1009)
-
主動控制字符串入池(intern()方法)
-
適用場景:大量重復字符串(如地址、配置項)通過
intern()
復用,減少堆內存占用。List<String> address = new ArrayList<>(); address.add(line.intern()); // 入池后重復字符串引用同一對象
-
6. 直接內存
(1)概念
直接內存(Direct Memory)是 Java 通過 java.nio.ByteBuffer.allocateDirect()
分配的堆外內存,由操作系統直接管理,不屬于 JVM 運行時數據區,但可通過 JVM 參數 -XX:MaxDirectMemorySize
限制其大小。
(2)特性
- 高性能:減少數據在 JVM 堆與操作系統內核之間的拷貝,適用于高頻 I/O 操作(如文件讀寫、網絡通信)。
- 手動管理:內存分配和釋放需開發者控制,但通過
DirectByteBuffer
對象間接管理,實際回收依賴 JVM 垃圾回收機制和虛引用。 - 零拷貝:支持直接與本地 I/O 交互,避免傳統 I/O 的雙緩沖區復制。
(3)管理與回收機制
一、直接內存的分配與對象結構
- 分配入口
調用ByteBuffer.allocateDirect()
時,底層通過DirectByteBuffer
構造函數觸發內存分配:DirectByteBuffer(int cap) {super(...);// 通過 Unsafe 類分配直接內存long base = unsafe.allocateMemory(cap);unsafe.setMemory(base, cap, (byte) 0);// 創建 Cleaner 虛引用,綁定釋放邏輯cleaner = Cleaner.create(this, new Deallocator(base, cap)); }
- 關鍵對象關系
- DirectByteBuffer 對象:位于堆內存,作為直接內存的引用句柄。
- Cleaner 對象:繼承自
PhantomReference
,維護釋放內存的回調邏輯。 - Deallocator 對象:實現
Runnable
,最終調用Unsafe.freeMemory()
釋放內存。
二、釋放流程
階段 1:DirectByteBuffer 對象被標記為不可達
- 觸發條件:堆中的 DirectByteBuffer 對象不再被任何 GC Roots 引用(如局部變量失效、強引用置為 null)。
- GC 掃描:在 Young GC 或 Full GC 時,垃圾回收器識別該對象為垃圾。
階段 2:Cleaner 虛引用入隊
- 引用隊列:Cleaner 作為虛引用(PhantomReference),當 DirectByteBuffer 對象被回收時,JVM 將其關聯的 Cleaner 對象加入
ReferenceQueue
。 - 內部線程處理:JVM 的
ReferenceHandler
線程(高優先級守護線程)監控該隊列,發現新加入的 Cleaner 對象后,觸發其清理邏輯。
階段 3:執行 Deallocator 釋放內存
- 回調邏輯:
Cleaner 對象的clean()
方法被調用,執行其綁定的Deallocator.run()
:public void run() {if (address != 0) {// 調用 Unsafe 釋放內存unsafe.freeMemory(address);address = 0;} }
- 內存釋放:
Unsafe.freeMemory()
直接向操作系統釋放對應的物理內存或虛擬內存。
三、關鍵機制深度解析
- 虛引用(PhantomReference)的作用
- 與 Finalizer 的區別:
- Finalizer 通過
finalize()
方法實現資源釋放,但存在執行延遲和不確定性。 - Cleaner 使用虛引用 +
ReferenceQueue
,確保釋放邏輯更及時、更可靠。
- Finalizer 通過
避免內存泄漏:
- 虛引用不阻止對象被回收(
get()
始終返回 null),確保DirectByteBuffer
對象可被正常 GC。
- 顯式 GC 的觸發問題
- 若 JVM 啟動參數包含
-XX:+DisableExplicitGC
,顯式調用System.gc()
將失效,或者未顯式調用System.gc()
且未觸發 Full GC,則ByteBuffer
、DirectByteBuffer
對象可能長期未被回收,導致直接內存未釋放。
- 若 JVM 啟動參數包含
其他
1. 永久代為什么被替換為元空間
- 永久代設置空間大小是很難確定的,因為可能某個實際的業務場景中有不斷的類加載等工作,但是元空間時使用本地內存,默認情況下是本地大小限制的。
- 類及方法的信息等比較難確定其大小,因此對于永久代的大小指定比較困難,太小容易出現永久代溢出,太大則容易導致老年代溢出。
- 字符串存在永久代中,容易出現性能問題和內存溢出。這些也是