個(gè)人筆記,深入理解 JVM,很全!
點(diǎn)擊上方藍(lán)色字體,選擇“設(shè)為星標(biāo)”

01、前言
刷豆瓣看到《深入理解 JVM》出第三版了,遂買之更新 JVM 知識,本文為筆記,僅供個(gè)人 Review
02、Java 內(nèi)存區(qū)域與內(nèi)存溢出
03、運(yùn)行時(shí)數(shù)據(jù)區(qū)域

參考:JVM 規(guī)范,Memories of a Java Runtime
堆:JVM 啟動時(shí)按-Xmx, -Xms大小創(chuàng)建的內(nèi)存區(qū)域,用于分配對象、數(shù)組所需內(nèi)存,由 GC 管理和回收
方法區(qū):存儲被 JVM 加載的類信息(字段、成員方法的字節(jié)碼指令等)、運(yùn)行時(shí)常量池(字面量、符號引用等)、JIT 編譯后的 Code Cache 等信息;JDK8 前 Hotspot 將方法區(qū)存儲于永久代堆內(nèi)存,之后參考 JRockit 廢棄了永久代,存儲于本地內(nèi)存的 Metaspace 區(qū)
直接內(nèi)存:JDK1.4 引入 NIO 使用 Native/Unsafe 庫直接分配系統(tǒng)內(nèi)存,使用 Buffer,Channel 與其交互,避免在系統(tǒng)內(nèi)存與 JVM 堆內(nèi)存之間拷貝的開銷
線程私有內(nèi)存
程序計(jì)數(shù)器:記錄當(dāng)前線程待執(zhí)行的下一條指令位置,上下文切換后恢復(fù)執(zhí)行,由字節(jié)碼解釋器負(fù)責(zé)更新 JVM 棧 描述 Java 方法執(zhí)行的內(nèi)存模型:執(zhí)行新方法時(shí)創(chuàng)建棧幀,存儲局部變量表、操作數(shù)棧等信息 存儲單位:變量槽 slot, long, double占 2 個(gè) slot,其他基本數(shù)據(jù)類型、引用類型占 1 個(gè),故表的總長度在編譯期可知本地方法棧:執(zhí)行本地 C/C++ 方法
04、JVM 對象
1. 創(chuàng)建對象
分配堆內(nèi)存:類加載完畢后,其對象所需內(nèi)存大小是確定的;堆內(nèi)存由多線程共享,若并發(fā)創(chuàng)建對象都通過 CAS 樂觀鎖爭奪內(nèi)存,則效率低。故線程創(chuàng)建時(shí)在堆內(nèi)存為其分配私有的分配緩沖區(qū)(TLAB:Thread Local Allocation Buffer)
內(nèi)存模型

分配流程

注:當(dāng) TLAB 剩余空間不足以分配新對象,但又小于最大浪費(fèi)空間閾值時(shí),才會加鎖創(chuàng)建新的 TLAB
零值初始化對象的堆內(nèi)存、設(shè)置對象頭信息、執(zhí)行構(gòu)造函數(shù)?()V
2. 對象的內(nèi)存布局
對象頭
Mark Word:記錄對象的運(yùn)行時(shí)信息,如 hashCode,GC 分代年齡,尾部 2 bit 用于標(biāo)記鎖狀態(tài)

Class Pointer:指向所屬的類信息
數(shù)組長度(可選,對象為數(shù)組):4 字節(jié)存儲其長度
對象數(shù)據(jù):各種字段的值,按寬度分類緊鄰存儲
對齊填充:內(nèi)存對齊為 1 個(gè)字長整數(shù)倍,減少 CPU 總線周期
驗(yàn)證:openjdk/jol 檢查對象內(nèi)存布局
public?class?User?{
private?int?age?=?-1;
private?String?name?=?"unknown";
}
//?java?-jar?~/Downloads/jol-cli-latest.jar?internals?-cp?.?com.jol.User
OFF??SZ???????????????TYPE?DESCRIPTION???????????????VALUE
0???8????????????????????(object?header:?mark)?????0x0000000000000001?(non-biasable;?age:?0)
8???4????????????????????(object?header:?class)????0xf8021e85?//?User.class?引用地址
?12???4????????????????int?User.age??????????????????-1?????????//?基本類型則直接存儲值
?16???4???java.lang.String?User.name?????????????????(object)???//?引用類型,指向運(yùn)行時(shí)常量池中的?String?對象
?20???4????????????????????(object?alignment?gap)???????????????//?有?4?字節(jié)的內(nèi)存填充
Instance?size:?24?bytes
05、內(nèi)存溢出
堆內(nèi)存:-Xms指定堆初始大小,當(dāng)大量無法被回收的對象所占內(nèi)存超出-Xmx上限時(shí),將發(fā)生內(nèi)存溢出?OutOfMemoryError
排查:通過 Eclipse MAT 分析?
-XX:+HeapDumpOnOutOfMemory生成的 *.hprof 堆轉(zhuǎn)儲文件,定位無法被回收的大對象,找出其 GC Root 引用路徑解決:若為內(nèi)存泄露,則修改代碼用
null顯式賦值、虛引用等方式及時(shí)回收大對象;若為內(nèi)存溢出,大對象都是必須存活的,則調(diào)大-Xmx、減少大對象的生命周期、檢查數(shù)據(jù)結(jié)構(gòu)使用是否合理等
//?-Xms20m?-Xmx20m?-XX:+HeapDumpOnOutOfMemoryError
public?class?HeapOOM?{
?static?class?OOMObject?{}
?public?static?void?main(String[]?args)?{
??List?vs?=?new?ArrayList<>();
??while?(true)
?????vs.add(new?OOMObject());
?}
}
分析 GC Root 發(fā)現(xiàn)com.ch02.HeapOOM對象間接引用了大量的OOMObject對象,共占用 15.4MB 堆內(nèi)存,無法回收最終導(dǎo)致 OOM

棧內(nèi)存:-Xss指定棧大小,當(dāng)棧深度超閾值(比如未觸發(fā)終止條件的遞歸調(diào)用)、本地方法變量表過大等,都可能導(dǎo)致內(nèi)存溢出?StackOverflowError
方法區(qū):-XX:MetaspaceSize指定元空間初始大小,-XX:MaxMetaspaceSize指定最大大小,默認(rèn) -1 無限制,若在運(yùn)行時(shí)動態(tài)生成大量的類,則可能觸發(fā) OOM
運(yùn)行時(shí)常量池:strObj.intern()動態(tài)地將首次出現(xiàn)的字符串對象放入字符串常量池并返回,JDK7 前會拷貝到永久代,之后則直接引用堆對象
String?s1?=?"java";?//?類加載時(shí),從字節(jié)碼常量池中拷貝符號到了運(yùn)行時(shí)常量池,在解析階段初始化的字符串對象
String?s2?=?"j";
String?s3?=?s2?+?"ava";?//?堆上動態(tài)分配的字符串對象
println(s3?==?s1);??????????//?false
println(s3.intern()?==?s1);?//?true?//?已在字符串常量池中存在
直接內(nèi)存:-XX:MaxDirectMemorySize指定大小,默認(rèn)與-Xmx一樣大,不被 GC 管理,申請內(nèi)存超閾值時(shí) OOM
06、垃圾回收與內(nèi)存分配
GC 可分解為 3 個(gè)子問題:which(哪些內(nèi)存可被回收)、when(什么時(shí)候回收)、how(如何回收)
07、GC 條件
1. 引用計(jì)數(shù)算法(reference counting)
原理:每個(gè)對象都維護(hù)一個(gè)引用計(jì)數(shù)器rc,當(dāng)通過賦值、傳參等方式引用它時(shí)rc++,當(dāng)引用變量修改指向、離開函數(shù)作用域等方式解除引用時(shí)rc--,遞減到 0 時(shí)說明對象無法再被使用,可回收。偽代碼:
assign(var,?obj):
incr_ref(obj)?#?self?=?self?#?先增再減,避免引用自身導(dǎo)致內(nèi)存提前釋放
decr_ref(var)
var?=?obj
?
incr(obj):
obj.rc++
decr(obj):
obj.rc--
if?obj.rc?==?0:
??remove_ref(obj)?#?斷開?obj?與其他對象的引用關(guān)系
??gc(obj)?????????#?回收?obj?內(nèi)存
優(yōu)點(diǎn):思路簡單,對象無用即回收,延遲低,適合內(nèi)存少的場景
缺點(diǎn):此算法中對象是孤立的,無法在全局視角檢查對象的真實(shí)有效性,循環(huán)引用的雙方對象需引入外部機(jī)制來檢測和回收,如下圖紅色圈(圖源:what-is-garbage-collection)

2. 可達(dá)性分析算法(reachability analysis)
原理:從肯定不會被回收的對象(GC Roots)出發(fā),向外搜索全局對象圖,不可達(dá)的對象即無法再被使用,可回收;常見可作為 GC Root 的對象有:
執(zhí)行上下文:JVM 棧中參數(shù)、局部變量、臨時(shí)變量等引用的堆對象 全局引用:方法區(qū)中類的靜態(tài)引用、常量引用(如 StringTable 中的字符串對象)所指向的對象
優(yōu)點(diǎn):無需對象維護(hù) GC 元信息,開銷小;單次掃描即可批量識別、回收對象,吞吐高
缺點(diǎn):多線程環(huán)境下對象間的引用關(guān)系隨時(shí)在變化,為保證 GC Root 標(biāo)記的準(zhǔn)確性,需在不變化的 snapshot 中進(jìn)行,會產(chǎn)生 Stop The World(以下簡稱 STW) 卡頓現(xiàn)象

3. 四種引用類型
| 引用類型 | 類 | 回收時(shí)機(jī) |
|---|---|---|
| 強(qiáng)引用 | - | 只要與 GC Root 存在引用鏈,則不被回收 |
| 軟引用 | SoftReference | 只被軟引用所引用的對象,當(dāng) GC 后內(nèi)存依然不足,才被回收 |
| 弱引用 | WeakReference | 只被弱引用所引用的對象,無論內(nèi)存是否足夠,都將被回收 |
| 虛引用 | PhantomReference | 被引用的對象無感知,進(jìn)行正常 GC,僅在回收時(shí)通知虛引用(回調(diào)) |
示例:限制堆內(nèi)存 50MB,其中新生代 30MB,老年代 20MB;依次分配 5 次 10MB 的byte[]對象,僅使用軟引用來引用,觀察 GC 過程
public?static?void?main(String[]?args)?{
//?softRefList?-->?SoftReference?-->?10MB?byte[]?
List>?softRefList?=?new?ArrayList<>();
ReferenceQueue?softRefQueue?=?new?ReferenceQueue<>();?//?無效引用隊(duì)列
for?(int?i?=?0;?i?5;?i++)?{
??SoftReference?softRef?=?new?SoftReference<>(new?byte[10*1024*1024],?softRefQueue);
??softRefList.add(softRef);
??for?(SoftReference?ref?:?softRefList)?//?dump?所有軟引用指向的對象,檢查是否已被回收
??????System.out.print(ref.get()?==?null???"gced?"?:?"ok?");
??System.out.println();
}
Reference?extends?byte[]>?ref?=?softRefQueue.poll();
while?(ref?!=?null)?{
??softRefList.remove(ref);?//?解除對軟引用對象本身的引用
??ref?=?softRefQueue.poll();
}
System.out.println("effective?soft?ref:?"?+?softRefList.size());?//?2
}
//?java?-verbose:gc?-XX:NewSize=30m?-Xms50m?-Xmx50m?-XX:+PrintGCDetails?com.ch02.DemoRef
ok?
ok?ok?
//?分配第三個(gè)?[]byte?時(shí),Eden?GC?無效,觸發(fā)?Full?GC?將一個(gè)?[]byte?晉升到老年區(qū)
//?此時(shí)三個(gè)?byte[]?都只被軟引用所引用,被標(biāo)記為待二次回收(若為弱引用,此時(shí)?Eden?已被回收)
[GC?(Allocation?Failure)?--[PSYoungGen:?21893K->21893K(27136K)]?21893K->32141K(47616K),?0.0046324?secs]
[Full?GC?(Ergonomics)?[PSYoungGen:?21893K->10527K(27136K)]?[ParOldGen:?10248K->10240K(20480K)]?32141K->20767K(47616K),?[Metaspace:?2784K->2784K(1056768K)],?0.004?secs]
ok?ok?ok
//?再次?GC,前三個(gè)?byte[]?全部被回收
[GC?(Allocation?Failure)?--[PSYoungGen:?20767K->20767K(27136K)]?31007K->31007K(47616K),?0.0007963?secs]
[Full?GC?(Ergonomics)?[PSYoungGen:?20767K->20759K(27136K)]?[ParOldGen:?10240K->10240K(20480K)]?31007K->30999K(47616K),?[Metaspace:?2784K->2784K(1056768K)],?0.003?secs]
[GC?(Allocation?Failure)?--[PSYoungGen:?20759K->20759K(27136K)]?30999K->30999K(47616K),?0.0007111?secs]
[Full?GC?(Allocation?Failure)?[PSYoungGen:?20759K->0K(27136K)]?[ParOldGen:?10240K->267K(20480K)]?30999K->267K(47616K),?[Metaspace:?2784K->2784K(1056768K)],?0.003?secs]
gced?gced?gced?ok
gced?gced?gced?ok?ok
4. finalize
原理:若對象不可達(dá),被標(biāo)記為可回收后,會進(jìn)行finalize()是否被重寫、是否已執(zhí)行過等條件篩選,若通過則對象會被放入 F-Queue 隊(duì)列,等待低優(yōu)先級的后臺 Finalizer 線程觸發(fā)其finallize()?的執(zhí)行(不保證執(zhí)行結(jié)束),對象可在finalize中建立與 GC Root 對象圖上任一節(jié)點(diǎn)的引用關(guān)系,來逃脫 GC
使用:finalize 機(jī)制與 C++ 中的析構(gòu)函數(shù)并不等價(jià),其執(zhí)行結(jié)果并不確定,不推薦使用,可用try-finally替代
08、GC 算法
分代收集理論
兩個(gè)分代假說:符合大多數(shù)程序運(yùn)行的實(shí)際情況
對應(yīng)地,JVM 堆被劃分為 2 個(gè)不同區(qū)域,將對象按年齡分類,兼顧了 GC 耗時(shí)與內(nèi)存利用率
跨代引用
問題:老年代會引用新生代,新生代 GC 時(shí)需遍歷老年代中大量的存活對象,分析可達(dá)性,時(shí)間復(fù)雜度高 背景:相互引用的對象傾向于同時(shí)存亡,比如跨代引用關(guān)系中的新生代必然會逐步晉升,最終消除跨代關(guān)系 假說:跨代引用相比同代引用只占極少數(shù),無需全量掃描老年代 實(shí)現(xiàn):新生代維護(hù)全局?jǐn)?shù)據(jù)結(jié)構(gòu):記憶集(Remembered Set),將老年代分為多個(gè)子塊,標(biāo)記存在跨代引用的子塊,等待后續(xù)掃描;代價(jià):為保證記憶集的正確性,需在跨代引用建立或斷開時(shí)保持同步

09、標(biāo)記清除:Mark-Sweep
原理:標(biāo)記不可達(dá)對象,統(tǒng)一清理回收,反之亦可 缺點(diǎn):執(zhí)行效率不穩(wěn)定,回收耗時(shí)取決于活躍對象的數(shù)量;內(nèi)存碎片多,會出現(xiàn)內(nèi)存充足但無法分配過大的連續(xù)內(nèi)存(數(shù)組)

10、標(biāo)記復(fù)制:Mark-Copy
理論:將堆內(nèi)存切為兩等份 A, B,每次僅使用 A,用完后標(biāo)記存活對象復(fù)制到 B,清空 A 后執(zhí)行 swap 優(yōu)點(diǎn):直接針對半?yún)^(qū)回收,無內(nèi)存碎片問題;分配內(nèi)存只需移動堆頂指針,高效順序分配 缺點(diǎn):當(dāng) A 區(qū)有大量存活對象時(shí),復(fù)制開銷大;B 區(qū)長時(shí)間閑置,內(nèi)存浪費(fèi)嚴(yán)重 實(shí)踐:對于存活對象少的新生代,無需按 1:1 分配,而是按 8:1:1 的內(nèi)存布局,其中 Eden 和 From 區(qū)同時(shí)使用,只有 To 區(qū)會被閑置(擔(dān)保機(jī)制:若 To 區(qū)不夠容納 Minor GC 后的存活對象,則晉升到老年區(qū))

11、標(biāo)記整理:Mark-Compact
原理:標(biāo)記存活對象后統(tǒng)一移動到內(nèi)存空間一側(cè),再回收邊界之外的內(nèi)存 優(yōu)點(diǎn):內(nèi)存模型簡單,無內(nèi)存碎片,降低內(nèi)存分配和訪問的時(shí)間成本,能提高吞吐 缺點(diǎn):對象移動需 STW 同步更新引用關(guān)系,會增加延遲

12、HotSpot GC 算法細(xì)節(jié)
13、發(fā)起 GC:安全點(diǎn)與安全區(qū)域
問題:為保證可達(dá)性分析結(jié)果的準(zhǔn)確性,需掛起用戶線程(STW),再從各線程的執(zhí)行上下文中收集 GC Root,如何通知線程掛起? 安全點(diǎn):HotSpot 內(nèi)部有線程中斷標(biāo)記;在各線程的方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等會長時(shí)間執(zhí)行的指令處,額外插入檢查該標(biāo)記的 test高效指令;若輪詢發(fā)現(xiàn)標(biāo)記為真,線程會主動在最近的 SafePoint 處掛起,此時(shí)其棧上對象的引用關(guān)系不再變化,可收集 GC Root 對象安全區(qū)域:引用關(guān)系不會變化的指令區(qū)域,可安全地收集 GC Root;線程離開此區(qū)域時(shí),若 GC Root 收集過程還未結(jié)束,則需等待
示意圖

14、加速 GC:CardTable
問題:非收集區(qū)域(老年代)會存在到收集區(qū)域(新生代)的跨代引用,如何避免對前者的全量掃描?
卡表:記憶集的字節(jié)數(shù)組實(shí)現(xiàn);將老年代內(nèi)存劃分為 Card Page(512KB)大小的子內(nèi)存塊,若新建跨代引用,則將對應(yīng)的 Card 標(biāo)記為 dirty,GC 時(shí)只需掃描老年代中被標(biāo)記為 dirty 的子內(nèi)存塊

寫屏障:有別于volatile禁用指令重排的內(nèi)存屏障,GC 中的寫屏障是在對象引用更新時(shí)執(zhí)行額外 hook 動作的機(jī)制。簡單實(shí)現(xiàn):
void?oop_field_store(oop*?field,?oop?new_val)?{?//?oop:?ordinary?object?pointer
// pre_write_barrier(field, new_val);?//?寫前屏障:更新前先執(zhí)行,使用 oop 舊狀態(tài)
*field?=?new_val;
post_write_barrier(field, new_val);?//?寫后屏障:更新完才執(zhí)行
}
使用寫屏障保證 CardTable 的實(shí)時(shí)更新(圖源:The JVM Write Barrier - Card Marking)

15、正確 GC:并發(fā)可達(dá)性分析
參考演講:Shenandoah: The Garbage Collector That Could by Aleksey Shipilev
**問題:**GC Roots 的對象源固定,故枚舉時(shí) STW 時(shí)間短暫且可控。但后續(xù)可達(dá)性分析的時(shí)間復(fù)雜度與堆中對象數(shù)量成正相關(guān),即堆中對象越多,對象圖越復(fù)雜,堆變大后 STW 時(shí)間不可接受
**解決:**并發(fā)標(biāo)記。引出新問題:用戶線程動態(tài)建立、解除引用,標(biāo)記過程中圖結(jié)構(gòu)發(fā)生變化,結(jié)果不可靠;證明:用三色法描述對象狀態(tài)
白色:未被回收器訪問過的對象;分析開始都是白色,分析結(jié)束還是白色則不可達(dá) 灰色:被回收器訪問過,但其上至少還有 1 個(gè)引用未被掃描(中間態(tài)) 黑色:被回收器訪問過,其上引用全部都已被掃描,存在引用鏈,為存活對象;若其他對象引用了黑色對象,則不必再掃描,肯定也存活;黑色不可能直接引用白色
STW 無并發(fā)的正確標(biāo)記:頂部 3 個(gè)對象將被回收

用戶線程并發(fā)修改引用,會導(dǎo)致標(biāo)記結(jié)果無效,分 2 種情況:
少回收,對象標(biāo)記為存活,但用戶解除了引用:產(chǎn)生浮動垃圾,可接受,等待下次 GC

誤回收,對象標(biāo)記為可回收,但用戶新建了引用:實(shí)際存活對象被回收,內(nèi)存錯誤

論文《Uniprocessor Garbage Collection Techniques - Paul R. Wilson》§3.2 證明了「實(shí)際存活的對象被標(biāo)記為可回收」必須同時(shí)滿足兩個(gè)條件(有時(shí)間序)
插入一條或多條從黑色到白色的新引用 刪除所有灰色到該白色的直接、間接引用
為正確實(shí)現(xiàn)標(biāo)記,打破其中一個(gè)條件即可(類比打破死鎖四個(gè)條件之一的思想),分別對應(yīng)兩種方案:
增量更新 Increment Update:記錄黑到白的引用關(guān)系,并發(fā)標(biāo)記結(jié)束后,以黑為根,重新掃描;A 直接存活 原始快照 SATB(Snapshot At The Begining):記錄灰到白的解引用關(guān)系,并發(fā)標(biāo)記結(jié)束后,以灰為根,重新掃描;B 為灰色,最后變?yōu)楹谏婊睢P枳⒁猓魶]有步驟 3,則 B,C 變?yōu)楦永?/section>
16、經(jīng)典垃圾回收器
搭配使用示意圖:

17、Serial, SerialOld
原理:內(nèi)存不足觸發(fā) GC 后會暫停所有用戶線程,單線程地在新生代中標(biāo)記復(fù)制,在老年代中標(biāo)記整理,收集完畢后恢復(fù)用戶線程
優(yōu)點(diǎn):全程 STW 簡單高效
缺點(diǎn):STW 時(shí)長與堆對象數(shù)量成正相關(guān),且 GC 線程只能用到 1 core 無法加速
場景:單核 CPU 且可用內(nèi)存少(如百兆級),JDK1.3 之前的唯一選擇

18、ParNew
原理:多線程并行版的 Serial 實(shí)現(xiàn),能有效減少 STW 時(shí)長;線程數(shù)默認(rèn)與核數(shù)相同,可配置
場景:JDK7 之前搭配老年代的 CMS 回收器使用

19、Parallel, Parallel Old
垃圾回收有兩個(gè)通常不可兼得的目標(biāo)
低延遲:STW 時(shí)長短,響應(yīng)快;允許高頻、短暫 GC,比如調(diào)小新生代空間,加快收集延遲(吞吐下降) 高吞吐量:用戶線程耗時(shí) /(用戶線程耗時(shí) + GC 線程耗時(shí))高,GC 總時(shí)間低;允許低頻、單次長時(shí)間 GC,(延遲增加)

原理:與 ParNew 類似都是并行回收,主要增加了 3 個(gè)選項(xiàng)(傾向于提高吞吐量)
-XX:MaxGCPauseTime:控制最大延遲-XX:GCTimeRatio:控制吞吐(默認(rèn) 99%)-XX:+UseAdaptiveSizePolicy?:啟用自適應(yīng)策略,自動調(diào)整 Eden 與 2 個(gè) Survivor 區(qū)的內(nèi)存占比-XX:SurvivorRatio,老年代晉升閾值?-XX:PretenureSizeThreshold

20、CMS
CMS:Concurrent Mark Sweep,即并發(fā)標(biāo)記清除,主要有 4 個(gè)階段
初始標(biāo)記(initial mark):STW 快速收集 GC Roots 并發(fā)標(biāo)記(concurrent mark):從 GC Roots 出發(fā)檢測引用鏈,標(biāo)記可回收對象;與用戶線程并發(fā)執(zhí)行,通過增量更新來避免誤回收 重新標(biāo)記(remark):STW 重新分析被增量更新所收集的 GC Roots 并發(fā)清除(concurrent sweep):并發(fā)清除可回收對象

優(yōu)點(diǎn):兩次 STW 時(shí)間相比并發(fā)標(biāo)記耗時(shí)要短得多,相比前三種收集器,延遲大幅降低
缺點(diǎn)
CPU 敏感:若核數(shù)較少(< 4core),并發(fā)標(biāo)記將占用大量 CPU 時(shí)間,會導(dǎo)致吞吐突降 無法處理浮動垃圾: -XX:CMSInitiatingOccupancyFration(默認(rèn) 92%)指定觸發(fā) CMS GC 的閾值;在并發(fā)標(biāo)記、并發(fā)清理的同時(shí),用戶線程會產(chǎn)生浮動垃圾(引用可回收對象、產(chǎn)生新對象),若浮動垃圾占比超過-XX:CMSInitiatingOccupancyFration;若 GC 的同時(shí)產(chǎn)生過多的浮動垃圾,導(dǎo)致老年代內(nèi)存不足,會出現(xiàn) CMS 并發(fā)失敗,退化為 Serial Old 執(zhí)行 Full GC,會導(dǎo)致延遲突增無法避免內(nèi)存碎片: -XX:CMSFullGCsBeforeCompaction(默認(rèn) 0)指定每次在 Full GC 前,先整理老年代的內(nèi)存碎片
21、G1
特點(diǎn):基于 region 內(nèi)存布局實(shí)現(xiàn)局部回收;GC 延遲目標(biāo)可配置;無內(nèi)存碎片問題

| G1 | 之前回收器 | |
|---|---|---|
| 堆內(nèi)存劃分方式 | 多個(gè)等大的 region, 各 region 分代角色并不固定,按需在 Eden, Survivor, Old 間切換 | 固定大小、固定數(shù)量的分代區(qū)域 |
| 回收目標(biāo) | 回收價(jià)值高的 region 動態(tài)組成的回收集合 | 新生代、整個(gè)堆內(nèi)存 |
跨代引用:各 region 除了用卡表標(biāo)記各卡頁是否為 dirty 之外,還用哈希表記錄了各卡頁正在被哪些 region 引用,通過這種“雙向指針”機(jī)制,能直接找到 Old 區(qū),避免了全量掃描(G1 自身內(nèi)存開銷大頭)

G1 GC 有 3 個(gè)階段(參考其 GC 日志)
新生代 GC:Eden 區(qū)占比超閾值觸發(fā);標(biāo)記存活對象并復(fù)制到 Survivor 區(qū),其內(nèi)可能有對象會晉升到 Old 區(qū)

老年代 GC:Old 區(qū)占比達(dá)到閾值后觸發(fā),執(zhí)行標(biāo)記整理
初始標(biāo)記:枚舉 GC Roots,已在新生代 GC 時(shí)順帶完成
并發(fā)標(biāo)記:并發(fā)執(zhí)行可達(dá)性分析,使用 SATB 記錄引用變更
重新標(biāo)記:SATB 分析,避免誤回收
篩選回收:將 region 按回收價(jià)值和時(shí)間成本篩選組成回收集,STW 將存活對象拷貝到空 regions 后清理舊 regions,完成回收
混合 GC

參數(shù)控制(文檔:HotSpot GC Tuning Guide)
| 參數(shù)及默認(rèn)值 | 描述 |
|---|---|
‐XX:+UseG1GC | JDK9 之前手動啟用 G1 |
-XX:MaxGCPauseMillis=200 | 預(yù)期的最大 GC 停頓時(shí)間;不宜過小,避免每次回收內(nèi)存少而導(dǎo)致頻繁 GC |
-XX:ParallelGCThreads=N | STW 并行線程數(shù),若可用核數(shù) M < 8 則 N=1,否則默認(rèn) N=M*5/8 |
-XX:ConcGCThreads=N | 并發(fā)階段并發(fā)線程數(shù),默認(rèn)是 ParallelGCThreads 的 1/4 |
-XX:InitiatingHeapOccupancyPercent=45 | 老年代 region 占比超過 45% 則觸發(fā)老年代 GC |
-XX:G1HeapRegionSize=N | 單個(gè) region 大小,1~32MB |
-XX:G1NewSizePercent=5, -XX:G1MaxNewSizePercent=60 | 新生代 region 最小占整堆的 5%,最大 60%,超出則觸發(fā)新生代 GC |
-XX:G1HeapWastePercent=5 | 允許浪費(fèi)的堆內(nèi)存占比,可回收內(nèi)存低于 5% 則不進(jìn)行混合回收 |
-XX:G1MixedGCLiveThresholdPercent=85 | 老年代存活對象占比超 85%,回收價(jià)值低,暫不回收 |
-XX:G1MixedGCCountTarget=8 | 單次收集中混合回收次數(shù) |
22、內(nèi)存分配策略
使用 Serial 收集器?-XX:+UseG1GC?演示
1. 對象優(yōu)先分配在 Eden 區(qū)
新對象在 Eden 區(qū)分配,空間不足則觸發(fā) Minor GC,存活對象拷貝到 To Survivor,若還是內(nèi)存不足則通過分配擔(dān)保機(jī)制轉(zhuǎn)移到老年區(qū),依舊不足才 OOM
byte[]?buf1?=?new?byte[6?*?MB];
byte[]?buf2?=?new?byte[6?*?MB];?//?10MB?的?eden?區(qū)剩余?4MB,空間不足,觸發(fā)?minor?GC
//?java?-verbose:gc?-Xms20m?-Xmx20m?-Xmn10m?-XX:+PrintGCDetails?-XX:+UseSerialGC?com.ch03.Allocation
//?minor?gc?后新生代內(nèi)存從?6M?降到?0.2M,存活對象移到了老年區(qū),總的堆內(nèi)存用量依舊是?6MB
[GC?(Allocation?Failure)?[DefNew:?6823K->286K(9216K),?0.002?secs]?6823K->6430K(19456K),?0.002?secs]?[Times:?user=0.00?sys=0.00,?real=0.00?secs]?
Heap
?def?new?generation???total?9216K,?used?6513K?
eden?space?8192K,??76%?used?//?buf2
from?space?1024K,??28%?used
to???space?1024K,???0%?used?
?tenured?generation???total?10240K,?used?6144K
?the?space?10240K,??60%?used?//?buf1
2. 大對象直接進(jìn)入老年區(qū)
對于 Serial, ParNew,可配置超過閾值?-XX:PretenureSizeThreshold?的大對象(連續(xù)內(nèi)存),直接在老年代中分配,避免觸發(fā) minor gc,導(dǎo)致 Eden 和 Survivor 產(chǎn)生大量的內(nèi)存復(fù)制操作
byte[]?buf1?=?new?byte[4?*?MB];
//?java?-verbose:gc?-Xms20m?-Xmx20m?-Xmn10m?-XX:+PrintGCDetails?-XX:+UseSerialGC
//?-XX:PretenureSizeThreshold=3145728?com.ch03.Allocation?//?3145728?即?3MB
Heap
?def?new?generation???total?9216K,?used?843K?
eden?space?8192K,??10%?used?
from?space?1024K,???0%?used?
to???space?1024K,???0%?used?
?tenured?generation???total?10240K,?used?4096K?
?the?space?10240K,??40%?used?//?buf1
3. 長期存活的對象進(jìn)入老年代
對象頭中 4bit 的 age 字段存儲了對象當(dāng)前 GC 分代年齡,當(dāng)超過閾值-XX:MaxTenuringThreshold(默認(rèn) 15,也即 age 字段最大值)后,將晉升到老年代,可搭配-XX:+PrintTenuringDistribution觀察分代分布
byte[]?buf1?=?new?byte[MB?/?16];
byte[]?buf2?=?new?byte[4?*?MB];
byte[]?buf3?=?new?byte[4?*?MB];?//?觸發(fā)?minor?gc
buf3?=?null;
buf3?=?new?byte[4?*?MB];
//?java?-verbose:gc?-Xms20m?-Xmx20m?-Xmn10m?-XX:+PrintGCDetails?-XX:+UseSerialGC?
//?-XX:MaxTenuringThreshold=1?-XX:+PrintTenuringDistribution?com.ch03.Allocation
[GC?(Allocation?Failure)?[DefNew
Desired?survivor?size?524288?bytes,?new?threshold?1?(max?1)
-?age???1:?????359280?bytes,?????359280?total
:?4839K->350K(9216K)]?4839K->4446K(19456K),?0.0017247?secs]?
//?至此,buf1?熬過了第一次收集,age=1
[GC?(Allocation?Failure)?[DefNew
Desired?survivor?size?524288?bytes,?new?threshold?1?(max?1):?4446K->0K(9216K)]?8542K->4438K(19456K)]?
Heap
?def?new?generation???total?9216K,?used?4178K?
eden?space?8192K,??51%?used?
from?space?1024K,???0%?used?//?buf1?在第二輪收集中被提前晉升
to???space?1024K,???0%?used?
?tenured?generation???total?10240K,?used?4438K?
?the?space?10240K,??43%?used?
4. 分代年齡動態(tài)判定
-XX:MaxTenuringThreshold并非晉升的最低硬性門檻,當(dāng) Survivor 中同齡對象超 50% 后,大于等于該年齡的對象會被自動晉升,哪怕還沒到閾值
5. 空間分配擔(dān)保
老年代作為 To Survivor 區(qū)的擔(dān)保區(qū)域,當(dāng) Eden + From Survivor 中存活對象的總大小超出 To Survivor 時(shí),將嘗試存入老年代。JDK6 之后,只要老年代的連續(xù)空間大于新生代對象的總大小,或之前晉升的平均大小,則只會進(jìn)行 Minor GC,否則進(jìn)行 Full GC
23、類文件結(jié)構(gòu)
Class 文件實(shí)現(xiàn)語言無關(guān)性,JVM 實(shí)現(xiàn)平臺無關(guān)性,參考《Java 虛擬機(jī)規(guī)范》

一個(gè) Class 文件描述了一個(gè)類或接口的明確定義,文件內(nèi)容是一組以 8 字節(jié)為單位的二進(jìn)制流,各數(shù)據(jù)項(xiàng)間沒有分隔符,超過 8 字節(jié)的數(shù)據(jù)項(xiàng)按 Big-Endian 切分后存儲。數(shù)據(jù)項(xiàng)分兩種:
無符號數(shù):描述基本類型;用? u1,u2,u4,u8?分別表示?1,2,4,8?字節(jié)長度的無符號數(shù);存儲數(shù)字值、索引序號、UTF-8 編碼值等表:由無符號數(shù)、其他表嵌套構(gòu)成的復(fù)合類型;約定? _info?后綴;存儲字段類型、方法簽名等
24、結(jié)構(gòu)定義
25、語法
參考文檔:The class File Format
ClassFile?{
u4?????????????magic;?????????//?魔數(shù)
u2?????????????minor_version;?//?版本號
u2?????????????major_version;
u2?????????????constant_pool_count;?//?常量池
cp_info????????constant_pool[constant_pool_count-1];
u2?????????????access_flags;?????//?類訪問標(biāo)記
u2?????????????this_class;???????//?本類全限定名
u2?????????????super_class;??????//?單一父類
u2?????????????interfaces_count;?//?多個(gè)接口
u2?????????????interfaces[interfaces_count];
u2?????????????fields_count;??//?字段表
field_info?????fields[fields_count];
u2?????????????methods_count;?//?方法表
method_info????methods[methods_count];
u2?????????????attributes_count;?//?類屬性
attribute_info?attributes[attributes_count];
}
magic:魔數(shù),簡單識別?
*.class?文件,值固定為?0xCAFEBABEminor_version, major_version:Class 文件的次、主版本號
constant_pool_count:常量池大小+1
constant_pool:常量池,索引從 1 開始,0 值被預(yù)留表示不引用任何常量池中的任何常量;常量分兩類
字面量:如 UTF8 字符串、int、float、long、double 等數(shù)字常量
符號引用:類、接口的全限定名、字段名與描述符、方法類型與描述符等 現(xiàn)有常量共計(jì) 17 種,常量間除了都使用
u1 tag前綴標(biāo)識常量類型外,結(jié)構(gòu)互不相同,常見的有:CONSTANT_Utf8_info:保存由 UTF8 編碼的字符串
CONSTANT_Utf8_info?{
??u1?tag;???????????//?值為?1
??u2?length;????????//?bytes?數(shù)組長度,u2?最大值?65535,即單個(gè)字符串字面量不超過?64KB
??u1?bytes[length];?//?長度不定的字節(jié)數(shù)組
}
CONSTANT_Class_info:表示類或接口的符號引用
CONSTANT_Class_info?{
??u1?tag;????????//?值為?7
??u2?name_index;?//?指向全限定類名的?Utf8_info?//?常量間存在層級組合關(guān)系
}
CONSTANT_Fieldref_info, CONSTANT_Methodref_info, CONSTANT_NameAndType_info:成員變量、成員方法及其類型描述符
CONSTANT_Fieldref_info?{
??u1?tag;?????????????????//?值為?9
??u2?class_index;?????????//?所屬類
??u2?name_and_type_index;?//?字段的名稱、類型描述符
}
CONSTANT_Methodref_info?{
??u1?tag;?????????????????//?值為?10
??u2?class_index;?????????//?所屬類
??u2?name_and_type_index;?//?方法的名稱、簽名描述符
}
CONSTANT_NameAndType_info?{
??u1?tag;??????????????//?值為?12
??u2?name_index;???????//?字段或方法的名稱
??u2?descriptor_index;?//?類型描述符
}
如上只列舉了其中 5 種常量的結(jié)構(gòu),可見常量間通過組合的方式,來描述層級關(guān)系
access_flags:類的訪問標(biāo)記,有 16bit,每個(gè)標(biāo)記對應(yīng)一個(gè)位,比如
ACC_PUBLIC對應(yīng)0x0001,表示類被 public 修飾;其他 8 個(gè)標(biāo)記參考?Opcodes.ACC_XXXthis_class, super_class:指向本類、唯一父類的 Class_info 符號常量
interface_count, interfaces:描述此類實(shí)現(xiàn)的多個(gè)接口信息
fields_count, fields:字段表;描述類字段、成員變量的個(gè)數(shù)及詳細(xì)信息
field_info?{
u2?????????????access_flags;?????//?作用域、static,final,volatile?等訪問標(biāo)記
u2?????????????name_index;???????//?字段名
u2?????????????descriptor_index;?//?類型描述符
u2?????????????attributes_count;?//?字段的屬性表
attribute_info?attributes[attributes_count];
}
類型描述符簡化描述了字段的數(shù)據(jù)類型、方法的參數(shù)列表及返回值,與 Java 中的類型對于關(guān)系如下:
基本類型:
Z:boolean, B:byte, C:char, S:short, I:int, F:float, D:double, J:longvoid 及引用類型:
V:void引用類型:
L:_,類名中的 . 替換為 /,添加 ; 分隔符,如 Object 類描述為Ljava/lang/Object;數(shù)組類型:每一維用一個(gè)前置?
[?表示 示例:boolean regionMatch(int, String, int, int)對應(yīng)描述符為?(ILjava/lang/String;II)Zmethods_count, methods:方法表;完整描述各成員方法的修飾符、參數(shù)列表、返回值等簽名信息
method_info?{
u2?????????????access_flags;?????//?訪問標(biāo)記
u2?????????????name_index;???????//?方法名
u2?????????????descriptor_index;?//?方法描述符
u2?????????????attributes_count;?//?方法屬性表
attribute_info?attributes[attributes_count];
}
字段表、方法表都可以帶多個(gè)屬性表,如常量字段表、方法字節(jié)碼指令表、方法異常表等。屬性模板:
attribute_info?{
u2?attribute_name_index;???//?屬性名
u4?attribute_length;???????//?屬性數(shù)據(jù)長度
u1?info[attribute_length];?//?其他字段,各屬性的結(jié)構(gòu)不同
}
屬性有 20+ 種,此處只記錄常見的三種
Code 屬性:存儲方法編譯后的字節(jié)碼指令
Code_attribute?{
??u2?attribute_name_index;?//?屬性名,指向的?Utf8_info?值固定為?"Code"
??u4?attribute_length;?????//?剩下字節(jié)長度
??u2?max_stack;??//?操作數(shù)棧最大深度,對于此方法的棧幀中操作數(shù)棧的深度
??u2?max_locals;?//?以?slot?變量槽為單位的局部變量表大小,存儲隱藏參數(shù)?this,實(shí)參列表,catch?參數(shù),局部變量等
??u4?code_length;???????//?字節(jié)碼指令總長度
??u1?code[code_length];?//?JVM?指令集大小?200+,單個(gè)指令的編號用?u1?描述
??u2?exception_table_length;?//?異常表,描述方法內(nèi)各指令區(qū)間產(chǎn)生的異常及其?handler?地址
??{???u2?start_pc;???//?catch_type?類型的異常,會在?[start_pc,?end_pc)?指令范圍內(nèi)拋出
??????u2?end_pc;???
??????u2?handler_pc;?//?若拋出此異常,則?goto?到?handler_pc?處執(zhí)行
??????u2?catch_type;
??}?exception_table[exception_table_length];
??u2?attributes_count;?//?Code?屬性自己的屬性
??attribute_info?attributes[attributes_count];
}
LineNumberTable 屬性:記錄 Java 源碼行號與字節(jié)碼行號的對應(yīng)關(guān)系,用于拋異常時(shí)顯示堆棧對應(yīng)的行號等信息。可作為 Code 屬性的子屬性
LineNumberTable_attribute?{
??u2?attribute_name_index;?u4?attribute_length;
??u2?line_number_table_length;
??{???u2?start_pc;?????//?字節(jié)碼指令區(qū)間開始位置
??????u2?line_number;??//?對應(yīng)的源碼行號
??}?line_number_table[line_number_table_length];
}
LocalVariableTable 屬性:記錄 Java 方法中局部變量的變量名,與棧幀局部變量表中的變量的對應(yīng)關(guān)系,用于保留各方法有意義的變量名稱
LocalVariableTable_attribute?{
??u2?attribute_name_index;?u4?attribute_length;
??u2?local_variable_table_length;
??{???u2?start_pc;?//?局部變量生命周期開始的字節(jié)碼偏移量
??????u2?length;???//?向后生命周期覆蓋的字節(jié)碼長度
??????u2?name_index;???????//?變量名
??????u2?descriptor_index;?//?類型描述符
??????u2?index;?//?對應(yīng)的局部變量表中的?slot?索引
??}?local_variable_table[local_variable_table_length];
}
其他屬性直接參考 JVM 文檔
26、示例
源碼:com/cls/Structure.java
package?com.cls;
public?class?Structure?{
public?static?void?main(String[]?args)?{
??System.out.println("hello?world");
}
}
javac -g:lines com/cls/Structure.java?編譯后,參考 javap 反編譯得到的正確結(jié)果,od -x --endian=big Structure.class?得出 class 文件內(nèi)容的十六進(jìn)制表示,解讀如下:
cafe?babe?#?1.??u4?魔數(shù),標(biāo)識?class?文件類型
0000?0034?#?2.??u2,u2?版本號,52?JDK8?
#?3.?常量池
---1---
001f?#?u2?constant_pool_count,31?項(xiàng)(從?1?開始計(jì)數(shù),0?預(yù)留)
0a??????#?u1?tag,10,Methoddef_info,成員方法結(jié)構(gòu)?
0006????#?u2?index,6,所屬類的?Class_info?在常量池中的編號???##?java/lang/Object
0011????#?u2?index,17,此方法?NameAndType?編號?????????????##?:()V
---2---
09??????#?9,F(xiàn)ileddef_info,成員變量結(jié)構(gòu)
0012????#?u2?index,18,所屬類?Class_info?編號?????##?java/lang/System
0013????#?u2?index,19,此字段?NameAndType?編號????##?out:Ljava/io/PrintStream
---3---
08??????#?8,String_info,字符串
0014????#?u2?index,20,字面量編號?????##?hello?world
---4---
0a?
0015????#?21????##?java/io/PrintStream
0016????#?22????##?println:(Ljava/lang/String;)V
---5---
07??????#?Class_info,全限定類名
0017????#?u2?index,23,字面量編號?????##?com/cls/Structure
---6---
07??????#?7,Class_info,類引用
0018????#?24????##?java/lang/Object
---7---
01??????#?Utf8_info,UTF8?編碼的字符串
0006????#?u2?length,6,字符串長度
3c?69?6e?69?74?3e?#?字面量值????##?""
---8-16---
01?0003?282956??????????????????????????????????????????##?"()V"
01?0004?436f6465????????????????????????????????????????##?"Code"
01?000f?4c696e654e756d6265725461626c65??????????????????##?"LineNumberTable"
01?0004?6d61696e????????????????????????????????????????##?"main"
01?0016?285b4c6a6176612f6c616e672f537472696e673b2956????##?"([Ljava/lang/String;)V"
01?0010?4d6574686f64506172616d6574657273????????????????##?"MethodParameters"
01?0004?61726773????????????????????????????????????????##?"args"
01?000a?536f7572636546696c65????????????????????????????##?"SourceFile"
01?000e?5374727563747572652e6a617661????????????????????##?"Structure.java"
---17---
0c??????#?12,NameAndType,名字及類型描述符
0007????#?u2?index,7,字段或方法名字面量編號????##?
0008????#?u2?index,8,字段或方法結(jié)構(gòu)編號???????##?()V
---18---
07?0019?#?25????##?java/lang/System
---19---
0c
001a?001b???#?26:27????##?out:Ljava/io/PrintStream;
---20---
01?000b?68656c6c6f20776f726c64????##?"hello?world"
---21--
07?001c?#?28????##?java/io/PrintStream
---22--
0c
001d?001e???#?29:30?????????????????????????????##?println:(Ljava/lang/String;)V
---23-31---
01?0011?636f6d2f636c732f537472756374757265??????????##?"com/cls/Structure"
01?0010?6a6176612f6c616e672f4f626a656374????????????##?"java/lang/Object?"
01?0010?6a6176?612f?6c61?6e67?2f53?7973?7465?6d?????##?"java/lang/System"
01?0003?6f7574??????????????????????????????????????##?"out"
01?0015?4c6a6176612f696f2f5072696e7453747265616d3b??##?"Ljava/io/PrintStream;"
01?0013?6a6176612f696f2f5072696e7453747265616d??????##?"java/io/PrintStream"
01?0007?7072696e746c6e??????????????????????????????##?"println"
01?0015?284c6a6176612f6c616e672f537472696e673b2956??##?"(Ljava/lang/String;)V"
0021?#?4.?u2,access_flags???????????????????????????##?ACC_PUBLIC?|?ACC_SUPER
0005?#?5.?u2,?this_class,5???????????????????????????##?--5.Class_info-->?com/cls/Structure
0006?#?6.?u2,?super_class,?6?????????????????????????##?--6.Class_info-->?java/lang/Object?
0000?#?7.?u2,?interface_count,?0
0000?#?8.?u2,?fields_count,?0
0002?#?9.?methods?count,?2
??#?方法一
0001????#?u2,?access_flags,?ACC_PUBLIC?????????????????
0007????#?u2,?name_index,?7?????????????????????????##??
0008????#?u2,?descriptor_index,?8???????????????????##?()V??????????????????????????
0001????#?u2,?attribute_count,?1?????????????????????????????
0009????????#?u2,?attribute_name_index,?9???????????##?Code?屬性
0000?001d???#?u4,?attribute_length,?30?
0001????????#?u2,?max_stack,?1????????????????????????????????
0001????????#?u2,?max_locals,?1????????????????????????????
0000?0005??#?u4,?code_array_length,?5????????????????????????
2a???????????????#?u1,?aload_0???????????????????????##?將第?0?個(gè)?slot?中的變量?this?入棧?
b7???0001????????#?u1,?invokespecial?????????????????##?執(zhí)行從?Object?繼承的?
b1???????????????#?u1,?return????????????????????????##?返回?void
0000????????#?u2,?exception_table_length,?0??????????##?exception?table?為空,無異常
0001????????#?u2,?attributes_count,?1????????????????##?Code?屬性本身的子屬性
000a????????????#?10????????????????????????????????????##?LineNumberTable?屬性
0000?0006???????#?6
0001????????????#?u2,?line_number_table_length,?1
0000????????????????#?u2,?start_pc,?0
0003????????????????#?u2,?line_number,?3
??#?方法二
0009????#?access_flags???????????????????????????????##?ACC_PIBLIC?|?ACC_STATIC
000b????#?name_index,?11?????????????????????????????##?main
000c????#?descriptor_index,?12???????????????????????##?([Ljava/lang/String;)V
0002????#?attribute_count,?2
0009????????#?attribute_name_index,?9????????????????##?Code
0000?0025??#?attribute_length,?37
0002????????#?max_stack,?2?
0001????????#?max_locals,?1
0000?0009??#?code_array_length,?9
b2???0002???????#?getstatic,?2???????????????????????##?Field:?java/lang/System.out:Ljava/io/PrintStream;?//?加載靜態(tài)對象變量
12???03?????????#?ldc,?3?????????????????????????????##?String:?"hello?world"??//?將常量參數(shù)入棧
b6???0004???????#?invokevirtual,?4???????????????????##?Method:?java/io/PrintStream.println:(Ljava/lang/String;)V?//?執(zhí)行方法
b1??????????????#?return
0000????????#?exception_table_length,?0
0001????????#?attributes_count,?1
000a????????#?10?????????????????????????????????????##?LineNumberTable
0000?000a???#?10
0002????????????#?line_number_table_length,?2
0000?0005???????????#?0?->?5
0008?0006???????????#?8?->?6
27、字節(jié)碼指令
JVM 面向操作數(shù)棧(operand stack)設(shè)計(jì)了指令集,每個(gè)指令由 1 字節(jié)的操作碼(opcode)表示,其后跟隨 0 個(gè)或多個(gè)操作數(shù)(operand),指令集列表參考 Java bytecode instruction listings
大部分與數(shù)據(jù)類型相關(guān)的指令,其操作碼符號都會帶類型前綴,如 i 前綴表示操作 int,剩余對應(yīng)關(guān)系為? b:byte, c:char, s:short, f:float, d:double, l:long, a:reference由于指令集大小有限(256個(gè)),故? boolean, byte, char, short?會被轉(zhuǎn)為int運(yùn)算
字節(jié)碼可大致分為六類:
加載和存儲指令:將變量從局部變量表 slot 加載到操作數(shù)棧的棧頂,反向則是存儲
//?將?slot?0,1,2,3,N?加載到棧頂,T?表示類型簡記前綴,可取?i,l,f,d,a
Tload_0,?Tload_1,?Tload_2,?Tload_3,?Tload?n
//?將棧頂數(shù)據(jù)寫回指定的?slot
Tstore_0,?Tstore_1,?Tstore_2,?Tstore_3,?Tstore?n
//?將不同范圍的常量值加載到棧頂,由于?0~5?常量過于常用,有單獨(dú)對應(yīng)的指令,ldc?則加載普通常量
bipush,?sipush,?Tconst_[0,1,2,3,4,5],?aconst_null,?ldc
運(yùn)算指令
Tadd, Tsub, Tmul, Tdiv, Trem ????//?算術(shù)運(yùn)算:加減乘除,取余
Tneg, Tor, Tand, Txor ???????????//?位運(yùn)算:取反、或、與、異或
dcmpg, dcmpl, fcmpg, fcmpl, lcmp //?比較運(yùn)算:后綴 g 即 greater, l 即 less than
iinc?????????????????????????????//?局部自增運(yùn)算,與?iload?搭配使用
強(qiáng)制類型轉(zhuǎn)換指令:窄化轉(zhuǎn)換為 T 類型(長度為 N)時(shí),會直接丟棄除了低 N 位外的其他位,可能會導(dǎo)致數(shù)據(jù)溢出、正負(fù)號不確定,浮點(diǎn)數(shù)轉(zhuǎn)整型則會丟失精度
i2b?//?int?->?byte
i2c,?i2s;?l2i,?f2i,?d2i;?d2l,?f2l;?d2f
對象創(chuàng)建與訪問指令:類實(shí)例、數(shù)組都是對象,存儲結(jié)構(gòu)不同,創(chuàng)建和訪問指令有所區(qū)別
new??????????????????????????????????????//?創(chuàng)建類實(shí)例
newarray,?annewarray,?multianewarry??????//?創(chuàng)建基本類型數(shù)組、引用類型數(shù)組、多維引用類型數(shù)組
getfield, putfield; getstatic, putstatic //?讀寫類實(shí)例字段;讀寫類靜態(tài)字段
Taload, Tastore; arraylength ????????????//?讀寫數(shù)組元素;計(jì)算數(shù)組長度
instanceof; checkcast ???????????????????//?校驗(yàn)對象是否為類實(shí)例;執(zhí)行強(qiáng)制轉(zhuǎn)換
操作數(shù)棧管理指令
pop,?pop2???????//?彈出棧頂?1,2?元素
dup, dup2; swap //?復(fù)制棧頂 1,2 個(gè)元素并重新入棧;交換棧頂兩個(gè)元素
控制轉(zhuǎn)移指令:判斷條件成立,則跳轉(zhuǎn)到指定的指令行(修改 PC 指向)
if_?//?整型比較,引用相等性判斷
if?????????????????????????????//?搭配其他類型的比較運(yùn)算指令使用
方法調(diào)用與返回指令
invokevirtual???//?根據(jù)對象的實(shí)際類型進(jìn)行分派,調(diào)用對應(yīng)的方法(比如繼承后方法重寫)?
invokespecial???//?調(diào)用特殊方法,如?()V,?()V?等初始化方法、私有方法、父類方法
invokestatic????//?調(diào)用類的靜態(tài)方法
invokeinterface?//?調(diào)用接口方法(實(shí)現(xiàn)接口的類對象,但被聲明為接口類型,調(diào)用方法)
invokedynamic???//?TODO
Treturn,?return?//?返回指定類型,返回?void
異常處理指令:
athrow?拋出異常,異常處理則由 exception_table 描述同步指令:synchronized 對象鎖由?
monitorenter, monitorexit?搭配對象的 monitor 鎖共同實(shí)現(xiàn)
28、類加載
29、類加載過程

1. 加載
原理:委托 ClassLoader 讀取 Class 二進(jìn)制字節(jié)流,載入到方法區(qū)內(nèi)存,并在堆內(nèi)存中生成對應(yīng)的java.lang.Class對象相互引用

2. 驗(yàn)證
校驗(yàn)字節(jié)流確保符合 Class 文件格式,執(zhí)行語義分析確保符合 Java 語法,校驗(yàn)字節(jié)碼指令合法性
3. 準(zhǔn)備
在堆中分配類變量(static)內(nèi)存并初始化為零值,主義還沒到執(zhí)行 putstatic 指令賦值的初始化階段,但靜態(tài)常量屬性除外:
public?class?ClassX?{
final?static?int?n?=?2;??????????//?常量的值在編譯期就已知,準(zhǔn)備階段完成賦值,值存儲在?ConstantValue
final?static?String?str?=?"str";?//?字符串靜態(tài)常量同理
}
static?final?java.lang.String?str;
descriptor:?Ljava/lang/String;
flags:?ACC_STATIC,?ACC_FINAL
ConstantValue:?String?str
4. 解析
將常量池中的符號引用(Class_info, Fieldref_info, Methodref_info)替換為直接引用(內(nèi)存地址)
5. 初始化
javac 會從上到下合并類中 static 變量賦值、static 語句塊,生成類構(gòu)造器()V,在初始化階段執(zhí)行,此方法的執(zhí)行由 JVM 保證線程安全;注意 JVM 規(guī)定有且僅有的,會立即觸發(fā)對類初始化的六種 case
public?class?ClassX?{
static?{
??println("main?class?ClassX?init");?//?1.?main()?所在的主類,總是先被初始化
}
public?static?void?main(String[]?args)?throws?Exception?{
??//?首次會觸發(fā)類的初始化
??//?SubX?b?=?new?SubX();??//?new?對象?//?2.?new,?getsatic,?putstatic,?invokestatic?指令
??//?println(SuperX.a);????//?讀寫類的?static?變量,或調(diào)用?static?方法?
??//?println(SubX.c);??????//?3.?子類初始化,會觸發(fā)父類初始化
??//?println(SubX.a);??????//????子類訪問父類的靜態(tài)變量,只會觸發(fā)父類初始化
??
??//?不會觸發(fā)類的初始化
??//?println(SubX.b);??????//?1.?訪問類的靜態(tài)常量(基本類型、字符串字面量)
??//?println(SubX.class);??//?2.?訪問類對象
??//?println(new?SubX[2]);?//?3.?創(chuàng)建類的數(shù)組
}
}
class?SuperX?{
static?int?a?=?0;
static?{
??println("class?SuperX?initiated");
}
}
class?SubX?extends?SuperX?{
final?static?double?b?=?0.1;
static?boolean?c?=?false;
static?{
??println("class?SubX?initiated");
}
}
30、類加載器
層級關(guān)系

雙親委派機(jī)制
原理:一個(gè)類加載器收到加載某個(gè)類的請求時(shí),會先委派上層的父類加載器去加載,逐層向上,當(dāng)父類加載器逐層向下反饋都無法加載此類后,該類加載器才會嘗試自己加載;此模型保證了,諸如 rt.jar 中的
java.lang.Object類,不論在底層哪種類加載器中都一定是被 Bootstrap 類加載器加載, JVM 中僅此一份,保證了一致性實(shí)現(xiàn)
//?java/lang/ClassLoader
protected?Class>?loadClass(String?name,?boolean?resolve)?throws?ClassNotFoundException?{
????synchronized?(getClassLoadingLock(name))?{
????????//?1.?先檢查自己的加載器是否已加載此類
????????Class>?c?=?findLoadedClass(name);
????????if?(c?==?null)?{
????????????long?t0?=?System.nanoTime();
????????????try?{
????????????????if?(parent?!=?null)?{
????????????????????//?2.?還有上層則委派給上層去加載
????????????????????c?=?parent.loadClass(name,?false);
????????????????}?else?{
????????????????????//?3.?如果沒有上級,則委派給?Bootstrap?加載
????????????????????c?=?findBootstrapClassOrNull(name);
????????????????}
????????????}?catch?(ClassNotFoundException?e)?{
????????????????//?類不存在
????????????}
????????????if?(c?==?null)?{
????????????????//?4.?到自己的?classpath?中查找類,用戶自定義?ClassLoader?自定義了查找規(guī)則
????????????????long?t1?=?System.nanoTime();
????????????????c?=?findClass(name);
????????????}
????????}
????????if?(resolve)?{
????????????resolveClass(c);
????????}
????????return?c;
????}
}
31、字節(jié)碼執(zhí)行引擎
32、運(yùn)行時(shí)棧幀結(jié)構(gòu)
public?static?void?main(String[]?args)?{
int?a?=?1008611;
int?b?=?++a;
}
對應(yīng)運(yùn)行時(shí)棧幀結(jié)構(gòu):

局部變量表:大小在編譯期確定,用于存放實(shí)參和局部變量,以大小為 32 bit 的變量槽為最小單位
long, double 類型被切分為 2 個(gè) slot 同時(shí)讀寫(單線程操作,無線程安全問題)
類對象調(diào)用方法時(shí),slot 0 固定為當(dāng)前對象的引用,即
this隱式實(shí)參變量槽有重用優(yōu)化,當(dāng) pc 指令超出某個(gè)槽中的變量的作用域時(shí),該槽會被其他變量重用
public?static?void?main(String[]?args)?{
??{
??????byte[]?buf?=?new?byte[10?*?1024?*?1024];
??}
??System.gc();??//?buf?還在局部變量表的?slot?0?中,作為?GC?Root?無法被回收
??//?int?v?=?0;?//?變量?v?重用?slot?0,gc?生效
??//?System.gc();?
操作數(shù)棧:最大深度在編譯期確定,與局部變量表配合入棧、執(zhí)行指令、出棧來執(zhí)行字節(jié)碼指令
返回地址:遇到
return?族指令則正常調(diào)用完成,發(fā)生異常但異常表中沒有對應(yīng)的 handler 則異常調(diào)用完成;正常退出到上層方法后,若有返回值則壓入棧,并將程序計(jì)數(shù)器恢復(fù)到方法調(diào)用指令的下一條指令
33、方法調(diào)用
34、虛方法、非虛方法
非虛方法:編譯期可知(程序運(yùn)行前就唯一確定)、且運(yùn)行期不可變的方法,在類加載階段就會將方法的符號引用解析為直接引用。有 5 種:
靜態(tài)方法(與類唯一關(guān)聯(lián)): invokestatic調(diào)用私有方法(外部不可訪問)、構(gòu)造器方法、父類方法: invokespecial調(diào)用final 方法(無法被繼承):由 invokevirtual調(diào)用(歷史原因)
public?class?StaticResolution?{
?public?static?void?doFunc()?{
??System.out.println("do?func...");
?}
?public?static?void?main(String[]?args)?{
??StaticResolution.doFunc();
?}
}
stack=0,?locals=1,?args_size=1????//?靜態(tài)方法的調(diào)用版本,在編譯時(shí)就以常量的形式,存入字節(jié)碼的參數(shù)
0:?invokestatic??#5???????????//?Method?doFunc:()V
??3:?return
虛方法:需在運(yùn)行時(shí)動態(tài)確定直接引用的方法,由invokevirtual, invokeinterface調(diào)用
35、靜態(tài)分派、動態(tài)分派
背景:方法可被重載(參數(shù)類型不同,或數(shù)量不同)、可被重寫(子類繼承后覆蓋)
分派:對象可聲明為類、父類、實(shí)現(xiàn)的接口等類型,當(dāng)對象作為實(shí)參或調(diào)用方法時(shí),需根據(jù)其靜態(tài)類型或?qū)嶋H類型,才能確定要調(diào)用的方法的版本,進(jìn)而確定其直接引用。此過程即方法的分派
reference 變量的 2 種類型
靜態(tài)類型:變量被聲明的類型,不會改變,編譯期可知 實(shí)際類型:變量指向的對象可被替換,運(yùn)行時(shí)隨時(shí)可能修改
靜態(tài)分派
原理:方法重載時(shí),依賴參數(shù)的靜態(tài)類型,來確定要使用哪個(gè)重載版本的方法 特點(diǎn):發(fā)生在編譯階段,由 javac 確定類型“匹配度最高的”重載版本,來作為 invokevirtual的參數(shù)
public?class?StaticDispatch?{
?static?abstract?class?Human?{}
?static?class?Man?extends?Human?{}
?static?class?Woman?extends?Human?{?}
?public?void?f(Human?human)?{System.out.println("f(Human)");}
?public?void?f(Man?man)?{System.out.println("f(Man)");}
?public?void?f(Woman?woman)?{System.out.println("f(Woman)");}
?public?static?void?main(String[]?args)?{
??Human?man?=?new?Man();?????//?靜態(tài)類型都是?Human
??Human?woman?=?new?Woman();?//?實(shí)際類型分別為?Man,?Woman
??StaticDispatch?sd?=?new?StaticDispatch();
??sd.f(man);???//?f(Human)?//?invokevirtual?#13?//?Method?f:(Lcom/ch08/StaticDispatch$Human;)V
??sd.f(woman);?//?f(Human)?//?編譯期就已確定重載版本,寫入字節(jié)碼中
?}
}
動態(tài)分派
原理:方法重寫時(shí),依賴 Receiver 對象的實(shí)際類型,來確定要使用哪個(gè)類版本的方法
特點(diǎn):發(fā)生在運(yùn)行時(shí),依賴
invokevirtual指令來確定調(diào)用方法的版本,進(jìn)而實(shí)現(xiàn)多態(tài),解析流程為

注:類的方法查找是高頻操作,JVM 會在方法區(qū)中為類建一張?zhí)摲椒ū?vtable,以實(shí)現(xiàn)方法的快速查找
public?class?DynamicDispatch?{
static?abstract?class?Human?{
????protected?abstract?void?f();
}
static?class?Man?extends?Human?{
????@Override
????protected?void?f()?{
????????System.out.println("Man?f()");
????}
}
static?class?Woman?extends?Human?{
????@Override
????protected?void?f()?{
????????System.out.println("Woman?f()");
????}
}
public?static?void?main(String[]?args)?{
????Human?man?=?new?Man();?//?雖然靜態(tài)類型都是?Human
????Human?woman?=?new?Woman();
????man.f();???//?Man?f()???//?invokevirtual?#6?//?Method?com/ch08/DynamicDispatch$Human.f:()V
????woman.f();?//?Woman?f()?//?雖然字節(jié)碼指令的參數(shù),都是靜態(tài)類型方法的符號引用
????man?=?new?Woman();
????man.f();?//?Woman?f()?//?但?invokevirtual?會根據(jù)?Receiver?實(shí)際類型,在運(yùn)行時(shí)解析到實(shí)際類的直接引用
}
}
注意,類的字段讀寫指令getfield, putfield沒有invokevirtual的動態(tài)分派機(jī)制,即子類的同名字段會直接覆蓋父類的字段。示例:
public?class?FieldHasNoPolymorphic?{
static?class?Father?{
????public?int?money?=?1;
????public?Father()?{
????????money?=?2;
????????showMoney();
????}
????public?void?showMoney()?{?System.out.println("Father,?money?=?"?+?money);?}
}
static?class?Son?extends?Father?{
????public?int?money?=?3;?//?子類字段在類加載的準(zhǔn)備階段被賦零值
????public?Son()?{?//?子類構(gòu)造器第一行默認(rèn)隱藏調(diào)用?super()
????????money?=?4;
????????showMoney();
????}
????public?void?showMoney()?{?System.out.println("Son,?money?=?"?+?money);?}
}
public?static?void?main(String[]?args)?{
????Father?guy?=?new?Son();?
????System.out.println("guy,?money?=?"?+?guy.money);
}
}
//?Son,?money?=?0?//?Father?類構(gòu)造器執(zhí)行,動態(tài)分派執(zhí)行了?Son::showMoney()
//?Son,?money?=?4?//?Son?類構(gòu)造器中訪問最新的、自己的?money?字段
//?guy,?money?=?2?//?字段的讀寫沒有動態(tài)分派,靜態(tài)類型是誰,就訪問誰的字段
36、單分派、多分派
方法的 Receiver,與方法的參數(shù),都是方法的宗量,根據(jù)一個(gè)宗量來選擇目標(biāo)方法稱為單分派,需要多個(gè)宗量才能確定方法的叫多分派
靜態(tài)分派機(jī)制會讓編譯器在編譯階段,對重載的多個(gè)方法,會選出參數(shù)匹配度最高的作為目標(biāo)方法 動態(tài)分派機(jī)制在運(yùn)行時(shí),依賴 Receiver 實(shí)際類型,配合 invokevirtual定位唯一的實(shí)例方法作為目標(biāo)方法
綜上兩點(diǎn),Java 是靜態(tài)多分派、動態(tài)單分派的語言
注明:第 10,11 章講 Java 的前后端編譯,學(xué)習(xí)了自動裝箱等常見語法糖的字節(jié)碼實(shí)現(xiàn),其余部分待有空搭配龍書一起學(xué);第 12,13 章內(nèi)容與《Java Concurrency In Practice》等書重合度較高,此處不再贅述
37、總結(jié)
學(xué)習(xí)《深入理解 JVM 3ed》,初步掌握了 JVM 內(nèi)存區(qū)域的劃分模型、GC 算法理論及常見回收器原理、Class 文件結(jié)構(gòu)中各數(shù)據(jù)項(xiàng)解釋、類加載流程、方法的執(zhí)行與分派等五大方面的知識,收獲頗豐。不過大部分都是理論,若有機(jī)會還是要研究下 openjdk 的源碼實(shí)現(xiàn) :(
后臺回復(fù)?學(xué)習(xí)資料?領(lǐng)取學(xué)習(xí)視頻
