【Java】Java 虛擬機(jī)
時間:2023-07-15 02:57:01 | 來源:網(wǎng)站運(yùn)營
時間:2023-07-15 02:57:01 來源:網(wǎng)站運(yùn)營
【Java】Java 虛擬機(jī): 最近做面試題發(fā)現(xiàn) Java 虛擬機(jī)還是考得挺多的。
1.運(yùn)行時數(shù)據(jù)區(qū)域
JDK 1.6 運(yùn)行時數(shù)據(jù)區(qū)域如下圖:
程序計(jì)數(shù)器:記錄正在執(zhí)行的虛擬機(jī)字節(jié)碼
指令的地址(如果正在執(zhí)行的是本地方法則為空)。
Java 虛擬機(jī)棧:每個
Java 方法在執(zhí)行的同時會創(chuàng)建一個棧幀,用于存儲
局部變量表、
操作數(shù)棧、
常量池引用等信息。
本地方法棧:與 Java 虛擬機(jī)棧類似,區(qū)別是本地方法棧為本地方法服務(wù)。本地方法一般是用其它語言(C、C++ 或匯編語言等)編寫的。
堆:所有
對象都在這里分配內(nèi)存,是垃圾收集的主要區(qū)域("GC 堆")。
方法區(qū):用于存放已被加載的
類信息、
常量、
靜態(tài)變量、即時編譯器編譯后的
代碼等數(shù)據(jù)。從 JDK 1.8 開始,移除永久代,并把方法區(qū)移至元空間,它位于本地內(nèi)存中,而不是虛擬機(jī)內(nèi)存中。
運(yùn)行時常量池:方法區(qū)的一部分。Class 文件中的常量池(編譯器生成的字面量和符號引用)會在類加載后被放入這個區(qū)域。
直接內(nèi)存:在 JDK 1.4 中新引入了 NIO 類,它可以使用 Native 函數(shù)庫直接分配堆外內(nèi)存,然后通過 Java 堆里的 DirectByteBuffer 對象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場景中顯著提高性能,因?yàn)楸苊饬嗽诙褍?nèi)存和堆外內(nèi)存來回拷貝數(shù)據(jù)。
2.垃圾收集
垃圾收集主要是針對堆和方法區(qū)進(jìn)行。
如何
判斷對象是否可以回收:
- 引用計(jì)數(shù)法:引用計(jì)數(shù)為 0 的對象可被回收。Java 虛擬機(jī)不使用引用計(jì)數(shù)算法。
- 可達(dá)性分析算法:以 GC Roots 為起始點(diǎn)進(jìn)行搜索,可達(dá)的對象都是存活的,不可達(dá)的對象可被回收。
- 方法區(qū)的回收:方法區(qū)主要存放永久代對象,主要是對常量池的回收和對類的卸載。
- finalize()
垃圾收集算法:
- 標(biāo)記-清除:標(biāo)記:活動對象會在對象頭部打上標(biāo)記。清除:進(jìn)行對象回收并取消標(biāo)志位。
- 標(biāo)記-整理:讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存。
- 復(fù)制:將內(nèi)存劃分為大小相等的兩塊,每次只使用其中一塊,當(dāng)這一塊內(nèi)存用完了就將還存活的對象復(fù)制到另一塊上面,然后再把使用過的內(nèi)存空間進(jìn)行一次清理?,F(xiàn)在的商業(yè)虛擬機(jī)都采用這種收集算法回收新生代,但是并不是劃分為大小相等的兩塊,而是一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。在回收時,將 Eden 和 Survivor 中還存活著的對象全部復(fù)制到另一塊 Survivor 上,最后清理 Eden 和使用過的那一塊 Survivor。
- 分代收集:根據(jù)對象存活周期將內(nèi)存劃分為幾塊,不同塊采用適當(dāng)?shù)氖占惴āR话銓⒍逊譃樾律屠夏甏?。新生代使用:?fù)制算法。老年代使用:標(biāo)記 - 清除 或者 標(biāo)記 - 整理 算法
CMS 收集器和 G1 收集器比較:
CMS(Concurrent Mark Sweep),Mark Sweep 指的是
標(biāo)記 - 清除算法。
分為以下四個流程:
- 初始標(biāo)記:僅僅只是標(biāo)記一下 GC Roots 能直接關(guān)聯(lián)到的對象,速度很快,需要停頓。
- 并發(fā)標(biāo)記:進(jìn)行 GC Roots Tracing 的過程,它在整個回收過程中耗時最長,不需要停頓。
- 重新標(biāo)記:為了修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分對象的標(biāo)記記錄,需要停頓。
- 并發(fā)清除:不需要停頓。
在整個過程中耗時最長的并發(fā)標(biāo)記和并發(fā)清除過程中,收集器線程都可以與用戶線程一起工作,不需要進(jìn)行停頓。
具有以下缺點(diǎn):
- 吞吐量低:低停頓時間是以犧牲吞吐量為代價的,導(dǎo)致 CPU 利用率不夠高。
- 無法處理浮動垃圾,可能出現(xiàn) Concurrent Mode Failure。浮動垃圾是指并發(fā)清除階段由于用戶線程繼續(xù)運(yùn)行而產(chǎn)生的垃圾,這部分垃圾只能到下一次 GC 時才能進(jìn)行回收。由于浮動垃圾的存在,因此需要預(yù)留出一部分內(nèi)存,意味著 CMS 收集不能像其它收集器那樣等待老年代快滿的時候再回收。如果預(yù)留的內(nèi)存不夠存放浮動垃圾,就會出現(xiàn) Concurrent Mode Failure,這時虛擬機(jī)將臨時啟用 Serial Old 來替代 CMS。
- 標(biāo)記 - 清除算法導(dǎo)致的空間碎片,往往出現(xiàn)老年代空間剩余,但無法找到足夠杭州續(xù)空間來分配當(dāng)前對象,不得不提前觸發(fā)一次 Full GC。
G1(Garbage-First),它是一款面向服務(wù)端應(yīng)用的垃圾收集器,在多 CPU 和大內(nèi)存的場景下有很好的性能。HotSpot 開發(fā)團(tuán)隊(duì)賦予它的使命是未來可以替換掉 CMS 收集器。
堆被分為新生代和老年代,其它收集器進(jìn)行收集的范圍都是整個新生代或者老年代,而 G1 可以直接對新生代和老年代一起回收。
G1 把堆劃分成多個大小相等的
獨(dú)立區(qū)域(Region),新生代和老年代不再物理隔離。
通過引入 Region 的概念,從而將原來的一整塊內(nèi)存空間劃分成多個的小空間,使得每個小空間可以單獨(dú)進(jìn)行垃圾回收。這種劃分方法帶來了很大的靈活性,使得可預(yù)測的停頓時間模型成為可能。通過記錄每個 Region 垃圾回收時間以及回收所獲得的空間(這兩個值是通過過去回收的經(jīng)驗(yàn)獲得),并維護(hù)一個優(yōu)先列表,每次根據(jù)允許的收集時間,優(yōu)先回收價值最大的 Region。
每個 Region 都有一個 Remembered Set,用來記錄該 Region 對象的引用對象所在的 Region。通過使用 Remembered Set,在做可達(dá)性分析的時候就可以避免全堆掃描。
如果不計(jì)算維護(hù) Remembered Set 的操作,G1 收集器的運(yùn)作大致可劃分為以下幾個步驟:
- 初始標(biāo)記
- 并發(fā)標(biāo)記
- 最終標(biāo)記:為了修正在并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分標(biāo)記記錄,虛擬機(jī)將這段時間對象變化記錄在線程的 Remembered Set Logs 里面,最終標(biāo)記階段需要把 Remembered Set Logs 的數(shù)據(jù)合并到 Remembered Set 中。這階段需要停頓線程,但是可并行執(zhí)行。
- 篩選回收:首先對各個 Region 中的回收價值和成本進(jìn)行排序,根據(jù)用戶所期望的 GC 停頓時間來制定回收計(jì)劃。此階段其實(shí)也可以做到與用戶程序一起并發(fā)執(zhí)行,但是因?yàn)橹换厥找徊糠?Region,時間是用戶可控制的,而且停頓用戶線程將大幅度提高收集效率。
具備如下特點(diǎn):
- 空間整合:整體來看是基于“標(biāo)記 - 整理”算法實(shí)現(xiàn)的收集器,從局部(兩個 Region 之間)上來看是基于“復(fù)制”算法實(shí)現(xiàn)的,這意味著運(yùn)行期間不會產(chǎn)生內(nèi)存空間碎片。
- 可預(yù)測的停頓:能讓使用者明確指定在一個長度為 M 毫秒的時間片段內(nèi),消耗在 GC 上的時間不得超過 N 毫秒。
3.內(nèi)存分配與回收
3.1 內(nèi)存分配策略
- 對象優(yōu)先在 Eden 分配,當(dāng) Eden 空間不夠時,發(fā)起 Minor GC。
- 大對象直接進(jìn)入老年代,最典型的大對象是那種很長的字符串以及數(shù)組。
- 長期存活的對象進(jìn)入老年代。
- 動態(tài)對象年齡判定:如果在 Survivor 中相同年齡所有對象大小的總和大于 Survivor 空間的一半,則年齡大于或等于該年齡的對象可以直接進(jìn)入老年代,無需等到 MaxTenuringThreshold 中要求的年齡。
- 空間分配擔(dān)保:在發(fā)生 Minor GC 之前,虛擬機(jī)先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間,如果條件成立的話,那么 Minor GC 可以確認(rèn)是安全的。如果不成立的話虛擬機(jī)會查看 HandlePromotionFailure 的值是否允許擔(dān)保失敗,如果允許那么就會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對象的平均大小,如果大于,將嘗試著進(jìn)行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允許冒險,那么就要進(jìn)行一次 Full GC。
3.2 內(nèi)存回收
Minor GC:回收新生代,因?yàn)樾律鷮ο蟠婊顣r間很短,因此 Minor GC 會頻繁執(zhí)行,執(zhí)行的速度一般也會比較快。
Full GC:回收老年代和新生代,老年代對象其存活時間長,因此 Full GC 很少執(zhí)行,執(zhí)行速度會比 Minor GC 慢很多。觸發(fā)條件:
- 調(diào)用 System.gc();
- 老年代空間不足;
- 空間分配擔(dān)保失??;
- JDK 1.7 及之前的永久代空間不足;
- Concurrent Mode Failure。
4.類加載
包括以下 7 個階段:
- 加載(Loading)
- 驗(yàn)證(Verification)
- 準(zhǔn)備(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸載(Unloading)
4.1 類加載器分類
從 Java 虛擬機(jī)的角度來講,只存在以下兩種不同的類加載器:
- 啟動類加載器(Bootstrap ClassLoader),使用 C++ 實(shí)現(xiàn),是虛擬機(jī)自身的一部分;
- 所有其它類的加載器,使用 Java 實(shí)現(xiàn),獨(dú)立于虛擬機(jī),繼承自抽象類 java.lang.ClassLoader。
從 Java 開發(fā)人員的角度看,類加載器可以劃分得更細(xì)致一些:
- 啟動類加載器(Bootstrap ClassLoader)此類加載器負(fù)責(zé)將存放在 <JRE_HOME>/lib 目錄中的,或者被 -Xbootclasspath 參數(shù)所指定的路徑中的,并且是虛擬機(jī)識別的(僅按照文件名識別,如 rt.jar,名字不符合的類庫即使放在 lib 目錄中也不會被加載)類庫加載到虛擬機(jī)內(nèi)存中。啟動類加載器無法被 Java 程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給啟動類加載器,直接使用 null 代替即可。
- 擴(kuò)展類加載器(Extension ClassLoader)這個類加載器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實(shí)現(xiàn)的。它負(fù)責(zé)將 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系統(tǒng)變量所指定路徑中的所有類庫加載到內(nèi)存中,開發(fā)者可以直接使用擴(kuò)展類加載器。
- 應(yīng)用程序類加載器(Application ClassLoader)這個類加載器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)實(shí)現(xiàn)的。由于這個類加載器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般稱為系統(tǒng)類加載器。它負(fù)責(zé)加載用戶類路徑(ClassPath)上所指定的類庫,開發(fā)者可以直接使用這個類加載器,如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認(rèn)的類加載器。
4.2 雙親委派模型
- 工作過程:一個類加載器首先將類加載請求轉(zhuǎn)發(fā)到父類加載器,只有當(dāng)父類加載器無法完成時才嘗試自己加載。
- 好處:使得 Java 類隨著它的類加載器一起具有一種帶有優(yōu)先級的層次關(guān)系,從而使得基礎(chǔ)類得到統(tǒng)一。例如 java.lang.Object 存放在 rt.jar 中,如果編寫另外一個 java.lang.Object 并放到 ClassPath 中,程序可以編譯通過。由于雙親委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 優(yōu)先級更高,這是因?yàn)?rt.jar 中的 Object 使用的是啟動類加載器,而 ClassPath 中的 Object 使用的是應(yīng)用程序類加載器。rt.jar 中的 Object 優(yōu)先級更高,那么程序中所有的 Object 都是這個 Object。