垃圾回收
引用計數(shù)法和可達性分析
- 引用計數(shù)法
即記錄對象的 reference count 若≠0則保留
a, b對象相互引用, 不可回收, 造成內(nèi)存泄露
- 可達性分析(JVM主流使用)
從GC Root出發(fā)的樹狀結(jié)構(gòu)
若對象不可達則回收
- 存在的問題
在多線程環(huán)境下,其他線程可能會更新已經(jīng)訪問過的對象中的引用,從而造成誤報(將引用設(shè)置為 null)或者漏報(將引用設(shè)置為未被訪問過的對象)
Stop-the-world 以及安全點
當 Java 虛擬機收到 Stop-the-world 請求,它便會等待所有的線程都到達安全點,才允許請求 Stop-the-world 的線程進行獨占的工作。(安全詞??????)
垃圾回收的三種方式

- 清除(sweep)

- 壓縮(compact)

- 復制(copy)
Java虛擬機的堆劃分

- new 指令執(zhí)行時將對象存儲在Eden區(qū)
- Eden區(qū)滿進行一次Minor GC
Minor GC收集新生代的垃圾, 同時掃描老年代的對象以確保所有被引用的年輕代對象都被正確標記為存活對象。
- 仍存活于JVM eden區(qū)的對象轉(zhuǎn)移到from區(qū)
- Survivors區(qū)通過
copy進行垃圾回收 - 在Survivors區(qū)長期存活對象轉(zhuǎn)移到老年代
TLAB(acquire lock())
每個線程可以向 Java 虛擬機申請一段連續(xù)的內(nèi)存
線程維護內(nèi)存頭尾指針
卡表
維護每張卡的dirty位
卡中存在寫入, 則dirty位置1
卡: 將堆劃分為多個512字節(jié)的卡
if (CARD_TABLE [this address >> 9] != DIRTY) //減少重復寫的不必要開銷
CARD_TABLE [this address >> 9] = DIRTY;
為避免掃描所有的老年代對象 ,導致新生代的對象重復掃描,導致的重復掃描對象堆
GC通過尋找dirty卡掃描, 掃描完dirty清零
Java內(nèi)存模型
在單線程中由于 as-if-serial 原則 不會改變程序重排序的運行結(jié)果
多線程就要涉及到 happen-before
happens-before (java 5)
描述兩個操作的內(nèi)存可見性
如果操作 X happens-before 操作 Y,那么 X 的結(jié)果對于 Y 可見
Java 內(nèi)存模型的底層實現(xiàn)
對于即時編譯器來說,它會針對前面提到的每一個 happens-before 關(guān)系,向正在編譯的目標方法中插入相應的讀讀、讀寫、寫讀以及寫寫內(nèi)存屏障。
內(nèi)存屏障
即時編譯器將根據(jù)具體的底層體系架構(gòu),將這些內(nèi)存屏障替換成具體的 CPU 指令
如6.S081的__sync_synchronize()
以我們?nèi)粘=佑|的 X86_64 架構(gòu)來說,讀讀、讀寫以及寫寫內(nèi)存屏障是空操作(no-op),只有寫讀內(nèi)存屏障會被替換成具體指令
volatile字段與安全發(fā)布
Java虛擬機實現(xiàn)sync
當聲明 synchronized 代碼塊時,編譯而成的字節(jié)碼將包含 monitorenter 和 monitorexit 指令
public void foo(Object lock) {
synchronized (lock) {
lock.hashCode();
}
}
// 上面的 Java 代碼將編譯為下面的字節(jié)碼
public void foo(java.lang.Object);
Code:
0: aload_1
1: dup
2: astore_2 //復制lock對象
3: monitorenter
4: aload_1
5: invokevirtual java/lang/Object.hashCode:()I
8: pop
9: aload_2
10: monitorexit
11: goto 19
14: astore_3
15: aload_2
16: monitorexit
17: aload_3
18: athrow
19: return
Exception table:
from to target type
4 11 14 any
14 17 14 any
- ACC_SYNCHRONIZED
public synchronized void foo(Object lock) {
lock.hashCode();
}
// 上面的 Java 代碼將編譯為下面的字節(jié)碼
public synchronized void foo(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: (0x0021) ACC_PUBLIC, **ACC_SYNCHRONIZED**
Code:
stack=1, locals=2, args_size=2
0: aload_1
1: invokevirtual java/lang/Object.hashCode:()I
4: pop
5: return
該標記表示在進入該方法時,Java 虛擬機需要進行 monitorenter 操作。而在退出該方法時,不管是正常返回,還是向調(diào)用者拋異常,Java 虛擬機均需要進行 monitorexit 操作
鎖
|-----------------------------------------------------------|------------------|
| Thread ID (偏向鎖) / HashCode / GC Age (其他狀態(tài)) | 鎖標志 | epoch |
|-----------------------------------------------------------|------------------|
| 54 bits | 3 bits | 2 bits |
- 重量級鎖
Java 虛擬機會阻塞加鎖失敗的線程,并且在目標鎖被釋放的時候,喚醒這些線程
在被阻塞前, 線程先進入自旋狀態(tài)
自旋 在處理器上空跑并且輪詢鎖是否被釋放
Java 虛擬機給出的方案是自適應自旋,根據(jù)以往自旋等待時是否能夠獲得鎖,來動態(tài)調(diào)整自旋的時間(循環(huán)數(shù)目)
- 輕量級鎖
通過CAS判斷鎖的標記字段是否為01
- 01 → 替換為剛分布過鎖記錄ID(持有線程) 并替換鎖標志00
- else →
- 該線程重復獲取同一把鎖。此時,Java 虛擬機會將鎖記錄清零,以代表該鎖被重復獲取
- 其他線程持有該鎖。此時,Java 虛擬機會將這把鎖膨脹為重量級鎖,并且阻塞當前線程
偏向鎖
超級無敵樂觀鎖 偏好從始至終只有一個線程請求某一把鎖
- 鎖對象初始化
JVM通過 CAS 操作,將當前線程的地址記錄在鎖對象的標記字段之中,并且將鎖標志設(shè)置為 101(偏向鎖狀態(tài)), 同時設(shè)置鎖對象的epoch = 全局epoch
- 請求鎖
- 判斷鎖指向的對應線程地址是否為該線程,若不是, 全局epoch++ 撤銷偏向鎖
- 判斷 epoch 值是否和鎖對象的類的 epoch 值相同 若不是 撤銷偏向鎖
- 若一切正常
- 撤銷偏向鎖
- 鎖對象可以重新偏向其他線程(若無競爭)
- 悲觀化 turn blue
- 撤銷需在安全點執(zhí)行,成本較高,因此JVM通過epoch優(yōu)化管理
即時編譯(JIT)
HotSpot 虛擬機包含多個即時編譯器 C1、C2 和 Graal
分層編譯

- 初始階段:
- 代碼首先由解釋器執(zhí)行,以快速啟動應用程序。
- 熱點檢測:
- JVM通過熱點檢測(HotSpot)技術(shù)識別出頻繁執(zhí)行的代碼塊(熱點代碼)。
- C1編譯:
- 一旦檢測到熱點代碼,JVM會使用C1編譯器將其編譯為本地代碼。
- 此時,代碼的執(zhí)行效率有所提高,但優(yōu)化程度有限。
- C2編譯:
- 如果熱點代碼繼續(xù)被頻繁執(zhí)行,JVM會進一步使用C2編譯器對其進行編譯。
- C2編譯器會進行更高級的優(yōu)化,生成高效的本地代碼。
- 動態(tài)優(yōu)化:
- 在運行過程中,JVM可能會根據(jù)運行時信息動態(tài)調(diào)整編譯策略,甚至重新編譯某些代碼。
熱點代碼: JVM通過識別代碼塊的循環(huán)回邊數(shù)(循環(huán)體調(diào)用次數(shù))和調(diào)用次數(shù)兩者取和
OSR編譯
(on- stack - replacement)
顧名思義就是在程序運行過程中(暫停線程)將通過JIT編譯后的本地代碼棧幀替換字節(jié)碼棧幀
Profiling(性能分析)
收集能夠反映程序執(zhí)行狀態(tài)的數(shù)據(jù)
- 分支profile
- 數(shù)據(jù)收集:
- JIT編譯器會記錄每次分支指令的執(zhí)行情況,包括分支是否被取和分支的目標地址。
- 收集的數(shù)據(jù)包括分支的取向(taken或not taken)和分支的執(zhí)行頻率。
- 數(shù)據(jù)分析:
- 通過分析分支的執(zhí)行歷史,JIT編譯器可以預測分支的未來行為。
- 如果某個分支總是被取或總是被忽略,JIT編譯器可以調(diào)整分支預測邏輯,減少分支預測錯誤的開銷。
- 數(shù)據(jù)收集:
- 類型profile
- 數(shù)據(jù)收集:
- JIT編譯器會記錄每次對象創(chuàng)建和方法調(diào)用時的類型信息。
- 收集的數(shù)據(jù)包括對象的實際類型、方法的接收者類型等。
- 數(shù)據(jù)分析:
- 通過分析類型使用歷史,JIT編譯器可以推斷出對象類型的常見模式。
- 如果某個方法總是被特定類型的對象調(diào)用,JIT編譯器可以進行類型專用化(type specialization),生成針對該類型的優(yōu)化代碼。
- 數(shù)據(jù)收集:
其中,最為基礎(chǔ)的便是方法的調(diào)用次數(shù)以及循環(huán)回邊的執(zhí)行次數(shù)。它們被用于觸發(fā)即時編譯
去優(yōu)化
當發(fā)現(xiàn)profile優(yōu)化后的代碼沒有按預期執(zhí)行, 線程進入trap 暫停線程通過OSR進行執(zhí)行代碼棧幀轉(zhuǎn)換
當JVM調(diào)用去優(yōu)化方法時,它會根據(jù)去優(yōu)化的原因來決定對即時編譯器生成的機器碼采取什么行動。具體來說,有三種可能的行動:
- Action_None
示例:假設(shè)某個方法的去優(yōu)化是因為出現(xiàn)了異常,但這個異常與優(yōu)化無關(guān),重新編譯也不會改變生成的機器碼。在這種情況下,JVM可以選擇保留當前的機器碼,下次調(diào)用該方法時直接使用。
- Action_Recompile
示例:假設(shè)某個方法的去優(yōu)化是因為類層次分析的結(jié)果發(fā)生了變化,例如新加載了一個子類,導致之前的優(yōu)化假設(shè)不再成立。在這種情況下,JVM可以選擇不保留當前的機器碼,但直接重新編譯該方法。
- Action_Reinterpret
示例:假設(shè)某個方法的去優(yōu)化是因為基于性能分析的激進優(yōu)化失敗了,例如某個假設(shè)的執(zhí)行路徑不再成立。在這種情況下,JVM需要重新收集性能數(shù)據(jù),以更好地反映程序的新的執(zhí)行狀態(tài)。
public void method() {
try {
// 可能引發(fā)異常的代碼
} catch (Exception e) {
// 處理異常
}
}
public void method() {
for (int i = 0; i < 1000000; i++){
// 熱點代碼
}
}
public class Parent {
public void method() {
// 方法體
}
}
public class Child extends Parent {
@Override
public void method() {
// 重寫的方法體
}
}public class Main {
public void caller(Parent p) {
p.method(); // 假設(shè)這里被內(nèi)聯(lián)了
// Parent的method
}
}
即時編譯器的中間表達形式
中間表達形式(Intermediate Representation)
如果不考慮解釋執(zhí)行的話,從 Java 源代碼到最終的機器碼實際上經(jīng)過了兩輪編譯:Java 編譯器將 Java 源代碼編譯成 Java 字節(jié)碼,而即時編譯器則將 Java 字節(jié)碼編譯成機器碼。
Java 字節(jié)碼本身并不適合直接作為可供優(yōu)化的 IR
現(xiàn)代編譯器一般采用靜態(tài)單賦值(Static Single Assignment,SSA)IR
SSA
每個變量只能被賦值一次,而 且只有當變量被賦值之后才能使用
y = 1; SSA偽代碼 y1 = 1;
y = 2; - - - - -> y2 = 2;
x = y; x1 = y2;
int x = 0;
if (condition) {
x = 1;
} else {
x = 2;
}
// 使用x
int x_1 = 0;
if (condition) {
x_1 = 1; // 重用x_1
} else {
x_1 = 2; // 重用x_1
}
// 使用x_1
int x_1 = 0;
if (condition){
x_2 = 1;
}else{
x_3 = 2;
}x_4 = φ(x_2, x_3);
// 使用x4
Sea-of-nodes
去除了變量的概念,直接采用變量所指向的值,來進行運算

節(jié)點調(diào)度需根據(jù)節(jié)點間的依賴關(guān)系進行
GVN優(yōu)化
Global Value Numbering
發(fā)現(xiàn)并消除等價計算的優(yōu)化技術(shù)
public static int foo(int count) {
int sum = 0;
for (int i = 0; i < count; i++) {
sum += i;
}
return sum;
}

public static int foo(int a, int b) {
int sum = a * b;
if (a > 0) {
sum += a * b;
}
if (b > 0) {
sum += a * b;
}
return sum;
}
浙公網(wǎng)安備 33010602011771號