日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

BATJ面试必会|Jvm 虚拟机篇

發(fā)布時(shí)間:2023/12/3 编程问答 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 BATJ面试必会|Jvm 虚拟机篇 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

轉(zhuǎn)載自??BATJ面試必會(huì)|Jvm 虛擬機(jī)篇

目錄

  • 一、運(yùn)行時(shí)數(shù)據(jù)區(qū)域

    • 程序計(jì)數(shù)器

    • Java 虛擬機(jī)棧

    • 本地方法棧

    • 方法區(qū)

    • 運(yùn)行時(shí)常量池

    • 直接內(nèi)存

  • 二、垃圾收集

    • 判斷一個(gè)對(duì)象是否可被回收

    • 引用類型

    • 垃圾收集算法

    • 垃圾收集器

  • 三、內(nèi)存分配與回收策略

    • Minor GC 和 Full GC

    • 內(nèi)存分配策略

    • Full GC 的觸發(fā)條件

  • 四、類加載機(jī)制

    • 類的生命周期

    • 類加載過程

    • 類初始化時(shí)機(jī)

    • 類與類加載器

    • 類加載器分類

    • 雙親委派模型

    • 自定義類加載器實(shí)現(xiàn)

  • 參考資料

一、運(yùn)行時(shí)數(shù)據(jù)區(qū)域

?

程序計(jì)數(shù)器

記錄正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址(如果正在執(zhí)行的是本地方法則為空)。

Java 虛擬機(jī)棧

每個(gè) Java 方法在執(zhí)行的同時(shí)會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量表、操作數(shù)棧、常量池引用等信息。從方法調(diào)用直至執(zhí)行完成的過程,就對(duì)應(yīng)著一個(gè)棧幀在 Java 虛擬機(jī)棧中入棧和出棧的過程。

?

可以通過 -Xss 這個(gè)虛擬機(jī)參數(shù)來指定每個(gè)線程的 Java 虛擬機(jī)棧內(nèi)存大小:

  • java -Xss512M HackTheJava

  • 該區(qū)域可能拋出以下異常:

    • 當(dāng)線程請(qǐng)求的棧深度超過最大值,會(huì)拋出 StackOverflowError 異常;

    • 棧進(jìn)行動(dòng)態(tài)擴(kuò)展時(shí)如果無法申請(qǐng)到足夠內(nèi)存,會(huì)拋出 OutOfMemoryError 異常。

    本地方法棧

    本地方法棧與 Java 虛擬機(jī)棧類似,它們之間的區(qū)別只不過是本地方法棧為本地方法服務(wù)。

    本地方法一般是用其它語言(C、C++ 或匯編語言等)編寫的,并且被編譯為基于本機(jī)硬件和操作系統(tǒng)的程序,對(duì)待這些方法需要特別處理。

    ?

    所有對(duì)象都在這里分配內(nèi)存,是垃圾收集的主要區(qū)域("GC 堆")。

    現(xiàn)代的垃圾收集器基本都是采用分代收集算法,其主要的思想是針對(duì)不同類型的對(duì)象采取不同的垃圾回收算法。可以將堆分成兩塊:

    • 新生代(Young Generation)

    • 老年代(Old Generation)

    堆不需要連續(xù)內(nèi)存,并且可以動(dòng)態(tài)增加其內(nèi)存,增加失敗會(huì)拋出 OutOfMemoryError 異常。

    可以通過 -Xms 和 -Xmx 這兩個(gè)虛擬機(jī)參數(shù)來指定一個(gè)程序的堆內(nèi)存大小,第一個(gè)參數(shù)設(shè)置初始值,第二個(gè)參數(shù)設(shè)置最大值。

  • java -Xms1M -Xmx2M HackTheJava

  • 方法區(qū)

    用于存放已被加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。

    和堆一樣不需要連續(xù)的內(nèi)存,并且可以動(dòng)態(tài)擴(kuò)展,動(dòng)態(tài)擴(kuò)展失敗一樣會(huì)拋出 OutOfMemoryError 異常。

    對(duì)這塊區(qū)域進(jìn)行垃圾回收的主要目標(biāo)是對(duì)常量池的回收和對(duì)類的卸載,但是一般比較難實(shí)現(xiàn)。

    HotSpot 虛擬機(jī)把它當(dāng)成永久代來進(jìn)行垃圾回收。但很難確定永久代的大小,因?yàn)樗艿胶芏嘁蛩赜绊?#xff0c;并且每次 Full GC 之后永久代的大小都會(huì)改變,所以經(jīng)常會(huì)拋出 OutOfMemoryError 異常。為了更容易管理方法區(qū),從 JDK 1.8 開始,移除永久代,并把方法區(qū)移至元空間,它位于本地內(nèi)存中,而不是虛擬機(jī)內(nèi)存中。

    運(yùn)行時(shí)常量池

    運(yùn)行時(shí)常量池是方法區(qū)的一部分。

    Class 文件中的常量池(編譯器生成的字面量和符號(hào)引用)會(huì)在類加載后被放入這個(gè)區(qū)域。

    除了在編譯期生成的常量,還允許動(dòng)態(tài)生成,例如 String 類的 intern()。

    直接內(nèi)存

    在 JDK 1.4 中新引入了 NIO 類,它可以使用 Native 函數(shù)庫(kù)直接分配堆外內(nèi)存,然后通過 Java 堆里的 DirectByteBuffer 對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場(chǎng)景中顯著提高性能,因?yàn)楸苊饬嗽诙褍?nèi)存和堆外內(nèi)存來回拷貝數(shù)據(jù)。

    二、垃圾收集

    垃圾收集主要是針對(duì)堆和方法區(qū)進(jìn)行。程序計(jì)數(shù)器、虛擬機(jī)棧和本地方法棧這三個(gè)區(qū)域?qū)儆诰€程私有的,只存在于線程的生命周期內(nèi),線程結(jié)束之后就會(huì)消失,因此不需要對(duì)這三個(gè)區(qū)域進(jìn)行垃圾回收。

    判斷一個(gè)對(duì)象是否可被回收

    1. 引用計(jì)數(shù)算法

    為對(duì)象添加一個(gè)引用計(jì)數(shù)器,當(dāng)對(duì)象增加一個(gè)引用時(shí)計(jì)數(shù)器加 1,引用失效時(shí)計(jì)數(shù)器減 1。引用計(jì)數(shù)為 0 的對(duì)象可被回收。

    在兩個(gè)對(duì)象出現(xiàn)循環(huán)引用的情況下,此時(shí)引用計(jì)數(shù)器永遠(yuǎn)不為 0,導(dǎo)致無法對(duì)它們進(jìn)行回收。正是因?yàn)檠h(huán)引用的存在,因此 Java 虛擬機(jī)不使用引用計(jì)數(shù)算法。

    public class Test {public Object instance = null;public static void main(String[] args) {Test a = new Test();Test b = new Test();a.instance = b;b.instance = a;}}

    2. 可達(dá)性分析算法

    以 GC Roots 為起始點(diǎn)進(jìn)行搜索,可達(dá)的對(duì)象都是存活的,不可達(dá)的對(duì)象可被回收。

    Java 虛擬機(jī)使用該算法來判斷對(duì)象是否可被回收,GC Roots 一般包含以下內(nèi)容:

    • 虛擬機(jī)棧中局部變量表中引用的對(duì)象

    • 本地方法棧中 JNI 中引用的對(duì)象

    • 方法區(qū)中類靜態(tài)屬性引用的對(duì)象

    • 方法區(qū)中的常量引用的對(duì)象

    ?

    3. 方法區(qū)的回收

    因?yàn)榉椒▍^(qū)主要存放永久代對(duì)象,而永久代對(duì)象的回收率比新生代低很多,所以在方法區(qū)上進(jìn)行回收性價(jià)比不高。

    主要是對(duì)常量池的回收和對(duì)類的卸載。

    為了避免內(nèi)存溢出,在大量使用反射和動(dòng)態(tài)代理的場(chǎng)景都需要虛擬機(jī)具備類卸載功能。

    類的卸載條件很多,需要滿足以下三個(gè)條件,并且滿足了條件也不一定會(huì)被卸載:

    • 該類所有的實(shí)例都已經(jīng)被回收,此時(shí)堆中不存在該類的任何實(shí)例。

    • 加載該類的 ClassLoader 已經(jīng)被回收。

    • 該類對(duì)應(yīng)的 Class 對(duì)象沒有在任何地方被引用,也就無法在任何地方通過反射訪問該類方法。

    4. finalize()

    類似 C++ 的析構(gòu)函數(shù),用于關(guān)閉外部資源。但是 try-finally 等方式可以做得更好,并且該方法運(yùn)行代價(jià)很高,不確定性大,無法保證各個(gè)對(duì)象的調(diào)用順序,因此最好不要使用。

    當(dāng)一個(gè)對(duì)象可被回收時(shí),如果需要執(zhí)行該對(duì)象的 finalize() 方法,那么就有可能在該方法中讓對(duì)象重新被引用,從而實(shí)現(xiàn)自救。自救只能進(jìn)行一次,如果回收的對(duì)象之前調(diào)用了 finalize() 方法自救,后面回收時(shí)不會(huì)再調(diào)用該方法。

    引用類型

    無論是通過引用計(jì)數(shù)算法判斷對(duì)象的引用數(shù)量,還是通過可達(dá)性分析算法判斷對(duì)象是否可達(dá),判定對(duì)象是否可被回收都與引用有關(guān)。

    Java 提供了四種強(qiáng)度不同的引用類型。

    1. 強(qiáng)引用

    被強(qiáng)引用關(guān)聯(lián)的對(duì)象不會(huì)被回收。

    使用 new 一個(gè)新對(duì)象的方式來創(chuàng)建強(qiáng)引用。

  • Object obj = new Object();

  • 2. 軟引用

    被軟引用關(guān)聯(lián)的對(duì)象只有在內(nèi)存不夠的情況下才會(huì)被回收。

    使用 SoftReference 類來創(chuàng)建軟引用。

  • Object obj = new Object();

  • SoftReference<Object> sf = new SoftReference<Object>(obj);

  • obj = null; // 使對(duì)象只被軟引用關(guān)聯(lián)

  • 3. 弱引用

    被弱引用關(guān)聯(lián)的對(duì)象一定會(huì)被回收,也就是說它只能存活到下一次垃圾回收發(fā)生之前。

    使用 WeakReference 類來創(chuàng)建弱引用。

  • Object obj = new Object();

  • WeakReference<Object> wf = new WeakReference<Object>(obj);

  • obj = null;

  • 4. 虛引用

    又稱為幽靈引用或者幻影引用,一個(gè)對(duì)象是否有虛引用的存在,不會(huì)對(duì)其生存時(shí)間造成影響,也無法通過虛引用得到一個(gè)對(duì)象。

    為一個(gè)對(duì)象設(shè)置虛引用的唯一目的是能在這個(gè)對(duì)象被回收時(shí)收到一個(gè)系統(tǒng)通知。

    使用 PhantomReference 來創(chuàng)建虛引用。

  • Object obj = new Object();

  • PhantomReference<Object> pf = new PhantomReference<Object>(obj);

  • obj = null;

  • 垃圾收集算法

    1. 標(biāo)記 - 清除

    ?

    標(biāo)記要回收的對(duì)象,然后清除。

    不足:

    • 標(biāo)記和清除過程效率都不高;

    • 會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,導(dǎo)致無法給大對(duì)象分配內(nèi)存。

    2. 標(biāo)記 - 整理

    ?

    讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存。

    3. 復(fù)制

    ?

    將內(nèi)存劃分為大小相等的兩塊,每次只使用其中一塊,當(dāng)這一塊內(nèi)存用完了就將還存活的對(duì)象復(fù)制到另一塊上面,然后再把使用過的內(nèi)存空間進(jìn)行一次清理。

    主要不足是只使用了內(nèi)存的一半。

    現(xiàn)在的商業(yè)虛擬機(jī)都采用這種收集算法回收新生代,但是并不是劃分為大小相等的兩塊,而是一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。在回收時(shí),將 Eden 和 Survivor 中還存活著的對(duì)象全部復(fù)制到另一塊 Survivor 上,最后清理 Eden 和使用過的那一塊 Survivor。

    HotSpot 虛擬機(jī)的 Eden 和 Survivor 大小比例默認(rèn)為 8:1,保證了內(nèi)存的利用率達(dá)到 90%。如果每次回收有多于 10% 的對(duì)象存活,那么一塊 Survivor 就不夠用了,此時(shí)需要依賴于老年代進(jìn)行空間分配擔(dān)保,也就是借用老年代的空間存儲(chǔ)放不下的對(duì)象。

    4. 分代收集

    現(xiàn)在的商業(yè)虛擬機(jī)采用分代收集算法,它根據(jù)對(duì)象存活周期將內(nèi)存劃分為幾塊,不同塊采用適當(dāng)?shù)氖占惴ā?/p>

    一般將堆分為新生代和老年代。

    • 新生代使用:復(fù)制算法

    • 老年代使用:標(biāo)記 - 清除 或者 標(biāo)記 - 整理 算法

    垃圾收集器

    ?

    以上是 HotSpot 虛擬機(jī)中的 7 個(gè)垃圾收集器,連線表示垃圾收集器可以配合使用。

    • 單線程與多線程:單線程指的是垃圾收集器只使用一個(gè)線程,而多線程使用多個(gè)線程;

    • 串行與并行:串行指的是垃圾收集器與用戶程序交替執(zhí)行,這意味著在執(zhí)行垃圾收集的時(shí)候需要停頓用戶程序;并行指的是垃圾收集器和用戶程序同時(shí)執(zhí)行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式執(zhí)行。

    1. Serial 收集器

    ?

    Serial 翻譯為串行,也就是說它以串行的方式執(zhí)行。

    它是單線程的收集器,只會(huì)使用一個(gè)線程進(jìn)行垃圾收集工作。

    它的優(yōu)點(diǎn)是簡(jiǎn)單高效,在單個(gè) CPU 環(huán)境下,由于沒有線程交互的開銷,因此擁有最高的單線程收集效率。

    它是 Client 場(chǎng)景下的默認(rèn)新生代收集器,因?yàn)樵谠搱?chǎng)景下內(nèi)存一般來說不會(huì)很大。它收集一兩百兆垃圾的停頓時(shí)間可以控制在一百多毫秒以內(nèi),只要不是太頻繁,這點(diǎn)停頓時(shí)間是可以接受的。

    2. ParNew 收集器

    ?

    它是 Serial 收集器的多線程版本。

    它是 Server 場(chǎng)景下默認(rèn)的新生代收集器,除了性能原因外,主要是因?yàn)槌?Serial 收集器,只有它能與 CMS 收集器配合使用。

    3. Parallel Scavenge 收集器

    與 ParNew 一樣是多線程收集器。

    其它收集器目標(biāo)是盡可能縮短垃圾收集時(shí)用戶線程的停頓時(shí)間,而它的目標(biāo)是達(dá)到一個(gè)可控制的吞吐量,因此它被稱為“吞吐量?jī)?yōu)先”收集器。這里的吞吐量指 CPU 用于運(yùn)行用戶程序的時(shí)間占總時(shí)間的比值。

    停頓時(shí)間越短就越適合需要與用戶交互的程序,良好的響應(yīng)速度能提升用戶體驗(yàn)。而高吞吐量則可以高效率地利用 CPU 時(shí)間,盡快完成程序的運(yùn)算任務(wù),適合在后臺(tái)運(yùn)算而不需要太多交互的任務(wù)。

    縮短停頓時(shí)間是以犧牲吞吐量和新生代空間來換取的:新生代空間變小,垃圾回收變得頻繁,導(dǎo)致吞吐量下降。

    可以通過一個(gè)開關(guān)參數(shù)打開 GC 自適應(yīng)的調(diào)節(jié)策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 區(qū)的比例、晉升老年代對(duì)象年齡等細(xì)節(jié)參數(shù)了。虛擬機(jī)會(huì)根據(jù)當(dāng)前系統(tǒng)的運(yùn)行情況收集性能監(jiān)控信息,動(dòng)態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時(shí)間或者最大的吞吐量。

    4. Serial Old 收集器

    ?

    是 Serial 收集器的老年代版本,也是給 Client 場(chǎng)景下的虛擬機(jī)使用。如果用在 Server 場(chǎng)景下,它有兩大用途:

    • 在 JDK 1.5 以及之前版本(Parallel Old 誕生以前)中與 Parallel Scavenge 收集器搭配使用。

    • 作為 CMS 收集器的后備預(yù)案,在并發(fā)收集發(fā)生 Concurrent Mode Failure 時(shí)使用。

    5. Parallel Old 收集器

    ?

    是 Parallel Scavenge 收集器的老年代版本。

    在注重吞吐量以及 CPU 資源敏感的場(chǎng)合,都可以優(yōu)先考慮 Parallel Scavenge 加 Parallel Old 收集器。

    6. CMS 收集器

    ?

    CMS(Concurrent Mark Sweep),Mark Sweep 指的是標(biāo)記 - 清除算法。

    分為以下四個(gè)流程:

    • 初始標(biāo)記:僅僅只是標(biāo)記一下 GC Roots 能直接關(guān)聯(lián)到的對(duì)象,速度很快,需要停頓。

    • 并發(fā)標(biāo)記:進(jìn)行 GC Roots Tracing 的過程,它在整個(gè)回收過程中耗時(shí)最長(zhǎng),不需要停頓。

    • 重新標(biāo)記:為了修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄,需要停頓。

    • 并發(fā)清除:不需要停頓。

    在整個(gè)過程中耗時(shí)最長(zhǎng)的并發(fā)標(biāo)記和并發(fā)清除過程中,收集器線程都可以與用戶線程一起工作,不需要進(jìn)行停頓。

    具有以下缺點(diǎn):

    • 吞吐量低:低停頓時(shí)間是以犧牲吞吐量為代價(jià)的,導(dǎo)致 CPU 利用率不夠高。

    • 無法處理浮動(dòng)垃圾,可能出現(xiàn) Concurrent Mode Failure。浮動(dòng)垃圾是指并發(fā)清除階段由于用戶線程繼續(xù)運(yùn)行而產(chǎn)生的垃圾,這部分垃圾只能到下一次 GC 時(shí)才能進(jìn)行回收。由于浮動(dòng)垃圾的存在,因此需要預(yù)留出一部分內(nèi)存,意味著 CMS 收集不能像其它收集器那樣等待老年代快滿的時(shí)候再回收。如果預(yù)留的內(nèi)存不夠存放浮動(dòng)垃圾,就會(huì)出現(xiàn) Concurrent Mode Failure,這時(shí)虛擬機(jī)將臨時(shí)啟用 Serial Old 來替代 CMS。

    • 標(biāo)記 - 清除算法導(dǎo)致的空間碎片,往往出現(xiàn)老年代空間剩余,但無法找到足夠大連續(xù)空間來分配當(dāng)前對(duì)象,不得不提前觸發(fā)一次 Full GC。

    7. G1 收集器

    G1(Garbage-First),它是一款面向服務(wù)端應(yīng)用的垃圾收集器,在多 CPU 和大內(nèi)存的場(chǎng)景下有很好的性能。HotSpot 開發(fā)團(tuán)隊(duì)賦予它的使命是未來可以替換掉 CMS 收集器。

    堆被分為新生代和老年代,其它收集器進(jìn)行收集的范圍都是整個(gè)新生代或者老年代,而 G1 可以直接對(duì)新生代和老年代一起回收。

    ?

    G1 把堆劃分成多個(gè)大小相等的獨(dú)立區(qū)域(Region),新生代和老年代不再物理隔離。

    ?

    通過引入 Region 的概念,從而將原來的一整塊內(nèi)存空間劃分成多個(gè)的小空間,使得每個(gè)小空間可以單獨(dú)進(jìn)行垃圾回收。這種劃分方法帶來了很大的靈活性,使得可預(yù)測(cè)的停頓時(shí)間模型成為可能。通過記錄每個(gè) Region 垃圾回收時(shí)間以及回收所獲得的空間(這兩個(gè)值是通過過去回收的經(jīng)驗(yàn)獲得),并維護(hù)一個(gè)優(yōu)先列表,每次根據(jù)允許的收集時(shí)間,優(yōu)先回收價(jià)值最大的 Region。

    每個(gè) Region 都有一個(gè) Remembered Set,用來記錄該 Region 對(duì)象的引用對(duì)象所在的 Region。通過使用 Remembered Set,在做可達(dá)性分析的時(shí)候就可以避免全堆掃描。

    ?

    如果不計(jì)算維護(hù) Remembered Set 的操作,G1 收集器的運(yùn)作大致可劃分為以下幾個(gè)步驟:

    • 初始標(biāo)記

    • 并發(fā)標(biāo)記

    • 最終標(biāo)記:為了修正在并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分標(biāo)記記錄,虛擬機(jī)將這段時(shí)間對(duì)象變化記錄在線程的 Remembered Set Logs 里面,最終標(biāo)記階段需要把 Remembered Set Logs 的數(shù)據(jù)合并到 Remembered Set 中。這階段需要停頓線程,但是可并行執(zhí)行。

    • 篩選回收:首先對(duì)各個(gè) Region 中的回收價(jià)值和成本進(jìn)行排序,根據(jù)用戶所期望的 GC 停頓時(shí)間來制定回收計(jì)劃。此階段其實(shí)也可以做到與用戶程序一起并發(fā)執(zhí)行,但是因?yàn)橹换厥找徊糠?Region,時(shí)間是用戶可控制的,而且停頓用戶線程將大幅度提高收集效率。

    具備如下特點(diǎn):

    • 空間整合:整體來看是基于“標(biāo)記 - 整理”算法實(shí)現(xiàn)的收集器,從局部(兩個(gè) Region 之間)上來看是基于“復(fù)制”算法實(shí)現(xiàn)的,這意味著運(yùn)行期間不會(huì)產(chǎn)生內(nèi)存空間碎片。

    • 可預(yù)測(cè)的停頓:能讓使用者明確指定在一個(gè)長(zhǎng)度為 M 毫秒的時(shí)間片段內(nèi),消耗在 GC 上的時(shí)間不得超過 N 毫秒。

    三、內(nèi)存分配與回收策略

    Minor GC 和 Full GC

    • Minor GC:回收新生代上,因?yàn)樾律鷮?duì)象存活時(shí)間很短,因此 Minor GC 會(huì)頻繁執(zhí)行,執(zhí)行的速度一般也會(huì)比較快。

    • Full GC:回收老年代和新生代,老年代對(duì)象其存活時(shí)間長(zhǎng),因此 Full GC 很少執(zhí)行,執(zhí)行速度會(huì)比 Minor GC 慢很多。

      ?

    內(nèi)存分配策略

    1. 對(duì)象優(yōu)先在 Eden 分配

    大多數(shù)情況下,對(duì)象在新生代 Eden 區(qū)分配,當(dāng) Eden 區(qū)空間不夠時(shí),發(fā)起 Minor GC。

    2. 大對(duì)象直接進(jìn)入老年代

    大對(duì)象是指需要連續(xù)內(nèi)存空間的對(duì)象,最典型的大對(duì)象是那種很長(zhǎng)的字符串以及數(shù)組。

    經(jīng)常出現(xiàn)大對(duì)象會(huì)提前觸發(fā)垃圾收集以獲取足夠的連續(xù)空間分配給大對(duì)象。

    -XX:PretenureSizeThreshold,大于此值的對(duì)象直接在老年代分配,避免在 Eden 區(qū)和 Survivor 區(qū)之間的大量?jī)?nèi)存復(fù)制。

    3. 長(zhǎng)期存活的對(duì)象進(jìn)入老年代

    為對(duì)象定義年齡計(jì)數(shù)器,對(duì)象在 Eden 出生并經(jīng)過 Minor GC 依然存活,將移動(dòng)到 Survivor 中,年齡就增加 1 歲,增加到一定年齡則移動(dòng)到老年代中。

    -XX:MaxTenuringThreshold 用來定義年齡的閾值。

    4. 動(dòng)態(tài)對(duì)象年齡判定

    虛擬機(jī)并不是永遠(yuǎn)地要求對(duì)象的年齡必須達(dá)到 MaxTenuringThreshold 才能晉升老年代,如果在 Survivor 中相同年齡所有對(duì)象大小的總和大于 Survivor 空間的一半,則年齡大于或等于該年齡的對(duì)象可以直接進(jìn)入老年代,無需等到 MaxTenuringThreshold 中要求的年齡。

    5. 空間分配擔(dān)保

    在發(fā)生 Minor GC 之前,虛擬機(jī)先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對(duì)象總空間,如果條件成立的話,那么 Minor GC 可以確認(rèn)是安全的。

    如果不成立的話虛擬機(jī)會(huì)查看 HandlePromotionFailure 設(shè)置值是否允許擔(dān)保失敗,如果允許那么就會(huì)繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對(duì)象的平均大小,如果大于,將嘗試著進(jìn)行一次 Minor GC;如果小于,或者 HandlePromotionFailure 設(shè)置不允許冒險(xiǎn),那么就要進(jìn)行一次 Full GC。

    Full GC 的觸發(fā)條件

    對(duì)于 Minor GC,其觸發(fā)條件非常簡(jiǎn)單,當(dāng) Eden 空間滿時(shí),就將觸發(fā)一次 Minor GC。而 Full GC 則相對(duì)復(fù)雜,有以下條件:

    1. 調(diào)用 System.gc()

    只是建議虛擬機(jī)執(zhí)行 Full GC,但是虛擬機(jī)不一定真正去執(zhí)行。不建議使用這種方式,而是讓虛擬機(jī)管理內(nèi)存。

    2. 老年代空間不足

    老年代空間不足的常見場(chǎng)景為前文所講的大對(duì)象直接進(jìn)入老年代、長(zhǎng)期存活的對(duì)象進(jìn)入老年代等。

    為了避免以上原因引起的 Full GC,應(yīng)當(dāng)盡量不要?jiǎng)?chuàng)建過大的對(duì)象以及數(shù)組。除此之外,可以通過 -Xmn 虛擬機(jī)參數(shù)調(diào)大新生代的大小,讓對(duì)象盡量在新生代被回收掉,不進(jìn)入老年代。還可以通過 -XX:MaxTenuringThreshold 調(diào)大對(duì)象進(jìn)入老年代的年齡,讓對(duì)象在新生代多存活一段時(shí)間。

    3. 空間分配擔(dān)保失敗

    使用復(fù)制算法的 Minor GC 需要老年代的內(nèi)存空間作擔(dān)保,如果擔(dān)保失敗會(huì)執(zhí)行一次 Full GC。具體內(nèi)容請(qǐng)參考上面的第五小節(jié)。

    4. JDK 1.7 及以前的永久代空間不足

    在 JDK 1.7 及以前,HotSpot 虛擬機(jī)中的方法區(qū)是用永久代實(shí)現(xiàn)的,永久代中存放的為一些 Class 的信息、常量、靜態(tài)變量等數(shù)據(jù)。

    當(dāng)系統(tǒng)中要加載的類、反射的類和調(diào)用的方法較多時(shí),永久代可能會(huì)被占滿,在未配置為采用 CMS GC 的情況下也會(huì)執(zhí)行 Full GC。如果經(jīng)過 Full GC 仍然回收不了,那么虛擬機(jī)會(huì)拋出 java.lang.OutOfMemoryError。

    為避免以上原因引起的 Full GC,可采用的方法為增大永久代空間或轉(zhuǎn)為使用 CMS GC。

    5. Concurrent Mode Failure

    執(zhí)行 CMS GC 的過程中同時(shí)有對(duì)象要放入老年代,而此時(shí)老年代空間不足(可能是 GC 過程中浮動(dòng)垃圾過多導(dǎo)致暫時(shí)性的空間不足),便會(huì)報(bào) Concurrent Mode Failure 錯(cuò)誤,并觸發(fā) Full GC。

    四、類加載機(jī)制

    類是在運(yùn)行期間第一次使用時(shí)動(dòng)態(tài)加載的,而不是一次性加載。因?yàn)槿绻淮涡约虞d,那么會(huì)占用很多的內(nèi)存。

    類的生命周期

    ?

    包括以下 7 個(gè)階段:

    • 加載(Loading)

    • 驗(yàn)證(Verification)

    • 準(zhǔn)備(Preparation)

    • 解析(Resolution)

    • 初始化(Initialization)

    • 使用(Using)

    • 卸載(Unloading)

    類加載過程

    包含了加載、驗(yàn)證、準(zhǔn)備、解析和初始化這 5 個(gè)階段。

    1. 加載

    加載是類加載的一個(gè)階段,注意不要混淆。

    加載過程完成以下三件事:

    • 通過類的完全限定名稱獲取定義該類的二進(jìn)制字節(jié)流。

    • 將該字節(jié)流表示的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)的運(yùn)行時(shí)存儲(chǔ)結(jié)構(gòu)。

    • 在內(nèi)存中生成一個(gè)代表該類的 Class 對(duì)象,作為方法區(qū)中該類各種數(shù)據(jù)的訪問入口。

    其中二進(jìn)制字節(jié)流可以從以下方式中獲取:

    • 從 ZIP 包讀取,成為 JAR、EAR、WAR 格式的基礎(chǔ)。

    • 從網(wǎng)絡(luò)中獲取,最典型的應(yīng)用是 Applet。

    • 運(yùn)行時(shí)計(jì)算生成,例如動(dòng)態(tài)代理技術(shù),在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理類的二進(jìn)制字節(jié)流。

    • 由其他文件生成,例如由 JSP 文件生成對(duì)應(yīng)的 Class 類。

    2. 驗(yàn)證

    確保 Class 文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。

    3. 準(zhǔn)備

    類變量是被 static 修飾的變量,準(zhǔn)備階段為類變量分配內(nèi)存并設(shè)置初始值,使用的是方法區(qū)的內(nèi)存。

    實(shí)例變量不會(huì)在這階段分配內(nèi)存,它會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起被分配在堆中。應(yīng)該注意到,實(shí)例化不是類加載的一個(gè)過程,類加載發(fā)生在所有實(shí)例化操作之前,并且類加載只進(jìn)行一次,實(shí)例化可以進(jìn)行多次。

    初始值一般為 0 值,例如下面的類變量 value 被初始化為 0 而不是 123。

  • public static int value = 123;

  • 如果類變量是常量,那么它將初始化為表達(dá)式所定義的值而不是 0。例如下面的常量 value 被初始化為 123 而不是 0。

  • public static final int value = 123;

  • 4. 解析

    將常量池的符號(hào)引用替換為直接引用的過程。

    其中解析過程在某些情況下可以在初始化階段之后再開始,這是為了支持 Java 的動(dòng)態(tài)綁定。

    5. 初始化

    初始化階段才真正開始執(zhí)行類中定義的 Java 程序代碼。初始化階段是虛擬機(jī)執(zhí)行類構(gòu)造器 <clinit>() 方法的過程。在準(zhǔn)備階段,類變量已經(jīng)賦過一次系統(tǒng)要求的初始值,而在初始化階段,根據(jù)程序員通過程序制定的主觀計(jì)劃去初始化類變量和其它資源。

    在準(zhǔn)備階段,已經(jīng)為類變量分配了系統(tǒng)所需的初始值,并且在初始化階段,根據(jù)程序員通過程序進(jìn)行的主觀計(jì)劃來初始化類變量和其他資源。

    <clinit>() 是由編譯器自動(dòng)收集類中所有類變量的賦值動(dòng)作和靜態(tài)語句塊中的語句合并產(chǎn)生的,編譯器收集的順序由語句在源文件中出現(xiàn)的順序決定。特別注意的是,靜態(tài)語句塊只能訪問到定義在它之前的類變量,定義在它之后的類變量只能賦值,不能訪問。例如以下代碼:

  • public class Test {

  • static {

  • i = 0; // 給變量賦值可以正常編譯通過

  • System.out.print(i); // 這句編譯器會(huì)提示“非法向前引用”

  • }

  • static int i = 1;

  • }

  • 由于父類的 <clinit>() 方法先執(zhí)行,也就意味著父類中定義的靜態(tài)語句塊的執(zhí)行要優(yōu)先于子類。例如以下代碼:

  • static class Parent {

  • public static int A = 1;

  • static {

  • A = 2;

  • }

  • }

  • ?

  • static class Sub extends Parent {

  • public static int B = A;

  • }

  • ?

  • public static void main(String[] args) {

  • System.out.println(Sub.B); // 2

  • }

  • 接口中不可以使用靜態(tài)語句塊,但仍然有類變量初始化的賦值操作,因此接口與類一樣都會(huì)生成 <clinit>() 方法。但接口與類不同的是,執(zhí)行接口的 <clinit>() 方法不需要先執(zhí)行父接口的 <clinit>() 方法。只有當(dāng)父接口中定義的變量使用時(shí),父接口才會(huì)初始化。另外,接口的實(shí)現(xiàn)類在初始化時(shí)也一樣不會(huì)執(zhí)行接口的 <clinit>() 方法。

    虛擬機(jī)會(huì)保證一個(gè)類的 <clinit>() 方法在多線程環(huán)境下被正確的加鎖和同步,如果多個(gè)線程同時(shí)初始化一個(gè)類,只會(huì)有一個(gè)線程執(zhí)行這個(gè)類的 <clinit>() 方法,其它線程都會(huì)阻塞等待,直到活動(dòng)線程執(zhí)行 <clinit>() 方法完畢。如果在一個(gè)類的 <clinit>() 方法中有耗時(shí)的操作,就可能造成多個(gè)線程阻塞,在實(shí)際過程中此種阻塞很隱蔽。

    類初始化時(shí)機(jī)

    1. 主動(dòng)引用

    虛擬機(jī)規(guī)范中并沒有強(qiáng)制約束何時(shí)進(jìn)行加載,但是規(guī)范嚴(yán)格規(guī)定了有且只有下列五種情況必須對(duì)類進(jìn)行初始化(加載、驗(yàn)證、準(zhǔn)備都會(huì)隨之發(fā)生):

    • 遇到 new、getstatic、putstatic、invokestatic 這四條字節(jié)碼指令時(shí),如果類沒有進(jìn)行過初始化,則必須先觸發(fā)其初始化。最常見的生成這 4 條指令的場(chǎng)景是:使用 new 關(guān)鍵字實(shí)例化對(duì)象的時(shí)候;讀取或設(shè)置一個(gè)類的靜態(tài)字段(被 final 修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候;以及調(diào)用一個(gè)類的靜態(tài)方法的時(shí)候。

    • 使用 java.lang.reflect 包的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候,如果類沒有進(jìn)行初始化,則需要先觸發(fā)其初始化。

    • 當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化,則需要先觸發(fā)其父類的初始化。

    • 當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要指定一個(gè)要執(zhí)行的主類(包含 main() 方法的那個(gè)類),虛擬機(jī)會(huì)先初始化這個(gè)主類;

    • 當(dāng)使用 JDK 1.7 的動(dòng)態(tài)語言支持時(shí),如果一個(gè) java.lang.invoke.MethodHandle 實(shí)例最后的解析結(jié)果為 REFgetStatic, REFputStatic, REF_invokeStatic 的方法句柄,并且這個(gè)方法句柄所對(duì)應(yīng)的類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化;

      ?

    2. 被動(dòng)引用

    以上 5 種場(chǎng)景中的行為稱為對(duì)一個(gè)類進(jìn)行主動(dòng)引用。除此之外,所有引用類的方式都不會(huì)觸發(fā)初始化,稱為被動(dòng)引用。被動(dòng)引用的常見例子包括:

    • 通過子類引用父類的靜態(tài)字段,不會(huì)導(dǎo)致子類初始化。

  • System.out.println(SubClass.value); // value 字段在 SuperClass 中定義

    • 通過數(shù)組定義來引用類,不會(huì)觸發(fā)此類的初始化。該過程會(huì)對(duì)數(shù)組類進(jìn)行初始化,數(shù)組類是一個(gè)由虛擬機(jī)自動(dòng)生成的、直接繼承自 Object 的子類,其中包含了數(shù)組的屬性和方法。

  • SuperClass[] sca = new SuperClass[10];

    • 常量在編譯階段會(huì)存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用到定義常量的類,因此不會(huì)觸發(fā)定義常量的類的初始化。

  • System.out.println(ConstClass.HELLOWORLD);

  • 類與類加載器

    兩個(gè)類相等,需要類本身相等,并且使用同一個(gè)類加載器進(jìn)行加載。這是因?yàn)槊恳粋€(gè)類加載器都擁有一個(gè)獨(dú)立的類名稱空間。

    這里的相等,包括類的 Class 對(duì)象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回結(jié)果為 true,也包括使用 instanceof 關(guān)鍵字做對(duì)象所屬關(guān)系判定結(jié)果為 true。

    類加載器分類

    從 Java 虛擬機(jī)的角度來講,只存在以下兩種不同的類加載器:

    • 啟動(dòng)類加載器(Bootstrap ClassLoader),使用 C++ 實(shí)現(xiàn),是虛擬機(jī)自身的一部分;

    • 所有其它類的加載器,使用 Java 實(shí)現(xiàn),獨(dú)立于虛擬機(jī),繼承自抽象類 java.lang.ClassLoader。

    從 Java 開發(fā)人員的角度看,類加載器可以劃分得更細(xì)致一些:

    • 啟動(dòng)類加載器(Bootstrap ClassLoader)此類加載器負(fù)責(zé)將存放在 <JRE_HOME>\lib 目錄中的,或者被 -Xbootclasspath 參數(shù)所指定的路徑中的,并且是虛擬機(jī)識(shí)別的(僅按照文件名識(shí)別,如 rt.jar,名字不符合的類庫(kù)即使放在 lib 目錄中也不會(huì)被加載)類庫(kù)加載到虛擬機(jī)內(nèi)存中。啟動(dòng)類加載器無法被 Java 程序直接引用,用戶在編寫自定義類加載器時(shí),如果需要把加載請(qǐng)求委派給啟動(dòng)類加載器,直接使用 null 代替即可。

    • 擴(kuò)展類加載器(Extension ClassLoader)這個(gè)類加載器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實(shí)現(xiàn)的。它負(fù)責(zé)將 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系統(tǒng)變量所指定路徑中的所有類庫(kù)加載到內(nèi)存中,開發(fā)者可以直接使用擴(kuò)展類加載器。

    • 應(yīng)用程序類加載器(Application ClassLoader)這個(gè)類加載器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)實(shí)現(xiàn)的。由于這個(gè)類加載器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般稱為系統(tǒng)類加載器。它負(fù)責(zé)加載用戶類路徑(ClassPath)上所指定的類庫(kù),開發(fā)者可以直接使用這個(gè)類加載器,如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個(gè)就是程序中默認(rèn)的類加載器。

      ?

    雙親委派模型

    應(yīng)用程序是由三種類加載器互相配合從而實(shí)現(xiàn)類加載,除此之外還可以加入自己定義的類加載器。

    下圖展示了類加載器之間的層次關(guān)系,稱為雙親委派模型(Parents Delegation Model)。該模型要求除了頂層的啟動(dòng)類加載器外,其它的類加載器都要有自己的父類加載器。類加載器之間的父子關(guān)系一般通過組合關(guān)系(Composition)來實(shí)現(xiàn),而不是繼承關(guān)系(Inheritance)。

    1. 工作過程

    一個(gè)類加載器首先將類加載請(qǐng)求轉(zhuǎn)發(fā)到父類加載器,只有當(dāng)父類加載器無法完成時(shí)才嘗試自己加載。

    2. 好處

    使得 Java 類隨著它的類加載器一起具有一種帶有優(yōu)先級(jí)的層次關(guān)系,從而使得基礎(chǔ)類得到統(tǒng)一。

    例如 java.lang.Object 存放在 rt.jar 中,如果編寫另外一個(gè) java.lang.Object 并放到 ClassPath 中,程序可以編譯通過。由于雙親委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 優(yōu)先級(jí)更高,這是因?yàn)?rt.jar 中的 Object 使用的是啟動(dòng)類加載器,而 ClassPath 中的 Object 使用的是應(yīng)用程序類加載器。rt.jar 中的 Object 優(yōu)先級(jí)更高,那么程序中所有的 Object 都是這個(gè) Object。

    3. 實(shí)現(xiàn)

    以下是抽象類 java.lang.ClassLoader 的代碼片段,其中的 loadClass() 方法運(yùn)行過程如下:先檢查類是否已經(jīng)加載過,如果沒有則讓父類加載器去加載。當(dāng)父類加載器加載失敗時(shí)拋出 ClassNotFoundException,此時(shí)嘗試自己去加載。

  • public abstract class ClassLoader {

  • // The parent class loader for delegation

  • private final ClassLoader parent;

  • ?

  • public Class<?> loadClass(String name) throws ClassNotFoundException {

  • return loadClass(name, false);

  • }

  • ?

  • protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

  • synchronized (getClassLoadingLock(name)) {

  • // First, check if the class has already been loaded

  • Class<?> c = findLoadedClass(name);

  • if (c == null) {

  • try {

  • if (parent != null) {

  • c = parent.loadClass(name, false);

  • } else {

  • c = findBootstrapClassOrNull(name);

  • }

  • } catch (ClassNotFoundException e) {

  • // ClassNotFoundException thrown if class not found

  • // from the non-null parent class loader

  • }

  • ?

  • if (c == null) {

  • // If still not found, then invoke findClass in order

  • // to find the class.

  • c = findClass(name);

  • }

  • }

  • if (resolve) {

  • resolveClass(c);

  • }

  • return c;

  • }

  • }

  • ?

  • protected Class<?> findClass(String name) throws ClassNotFoundException {

  • throw new ClassNotFoundException(name);

  • }

  • }

  • 自定義類加載器實(shí)現(xiàn)

    FileSystemClassLoader 是自定義類加載器,繼承自 java.lang.ClassLoader,用于加載文件系統(tǒng)上的類。它首先根據(jù)類的全名在文件系統(tǒng)上查找類的字節(jié)代碼文件(.class 文件),然后讀取該文件內(nèi)容,最后通過 defineClass() 方法來把這些字節(jié)代碼轉(zhuǎn)換成 java.lang.Class 類的實(shí)例。

    java.lang.ClassLoader 的 loadClass() 實(shí)現(xiàn)了雙親委派模型的邏輯,自定義類加載器一般不去重寫它,但是需要重寫 findClass() 方法。

  • public class FileSystemClassLoader extends ClassLoader {

  • ?

  • private String rootDir;

  • ?

  • public FileSystemClassLoader(String rootDir) {

  • this.rootDir = rootDir;

  • }

  • ?

  • protected Class<?> findClass(String name) throws ClassNotFoundException {

  • byte[] classData = getClassData(name);

  • if (classData == null) {

  • throw new ClassNotFoundException();

  • } else {

  • return defineClass(name, classData, 0, classData.length);

  • }

  • }

  • ?

  • private byte[] getClassData(String className) {

  • String path = classNameToPath(className);

  • try {

  • InputStream ins = new FileInputStream(path);

  • ByteArrayOutputStream baos = new ByteArrayOutputStream();

  • int bufferSize = 4096;

  • byte[] buffer = new byte[bufferSize];

  • int bytesNumRead;

  • while ((bytesNumRead = ins.read(buffer)) != -1) {

  • baos.write(buffer, 0, bytesNumRead);

  • }

  • return baos.toByteArray();

  • } catch (IOException e) {

  • e.printStackTrace();

  • }

  • return null;

  • }

  • ?

  • private String classNameToPath(String className) {

  • return rootDir + File.separatorChar

  • + className.replace('.', File.separatorChar) + ".class";

  • }

  • }

  • 參考資料

    • 周志明. 深入理解 Java 虛擬機(jī) [M]. 機(jī)械工業(yè)出版社, 2011.

    • Chapter 2. The Structure of the Java Virtual Machine

    • Jvm memory?Getting Started with the G1 Garbage Collector

    • JNI Part1: Java Native Interface Introduction and “Hello World” application

    • Memory Architecture Of JVM(Runtime Data Areas)

    • JVM Run-Time Data Areas

    • Android on x86: Java Native Interface and the Android Native Development Kit

    • 深入理解 JVM(2)——GC 算法與內(nèi)存分配策略

    • 深入理解 JVM(3)——7 種垃圾收集器

    • JVM Internals

    • 深入探討 Java 類加載器

    • Guide to WeakHashMap in Java

    • Tomcat example source code file (ConcurrentCache.java)

    總結(jié)

    以上是生活随笔為你收集整理的BATJ面试必会|Jvm 虚拟机篇的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。