一文帶你深入理解JVM,看完之后你還敢說(shuō)你懂JVM嗎?顛覆you認(rèn)知
前言
今天帶大家深入理解JVM,從入門(mén)到精通,希望大家能夠喜歡~~~
概念
JVM是可運(yùn)行 Java 代碼的假想計(jì)算機(jī) ,包括一套字節(jié)碼指令集、一組寄存器、一個(gè)棧、一個(gè)垃圾回收,堆 和 一個(gè)存儲(chǔ)方法域。JVM 是運(yùn)行在操作系統(tǒng)之上的,它與硬件沒(méi)有直接的交互。
運(yùn)行過(guò)程:
我們都知道 Java 源文件,通過(guò)編譯器,能夠生產(chǎn)相應(yīng)的.Class 文件,也就是字節(jié)碼文件,而字節(jié)碼文件又通過(guò) Java 虛擬機(jī)中的解釋器,編譯成特定機(jī)器上的機(jī)器碼 。
也就是如下:
① Java 源文件—->編譯器—->字節(jié)碼文件
② 字節(jié)碼文件—->JVM—->機(jī)器碼
每一種平臺(tái)的解釋器是不同的,但是實(shí)現(xiàn)的虛擬機(jī)是相同的,這也就是 Java 為什么能夠跨平臺(tái)的原因了 ,當(dāng)一個(gè)程序從開(kāi)始運(yùn)行,這時(shí)虛擬機(jī)就開(kāi)始實(shí)例化了,多個(gè)程序啟動(dòng)就會(huì)存在多個(gè)虛擬機(jī)實(shí)例。程序退出或者關(guān)閉,則虛擬機(jī)實(shí)例消亡,多個(gè)虛擬機(jī)實(shí)例之間數(shù)據(jù)不能共享。
線程
這里所說(shuō)的線程指程序執(zhí)行過(guò)程中的一個(gè)線程實(shí)體。JVM 允許一個(gè)應(yīng)用并發(fā)執(zhí)行多個(gè)線程。
Hotspot JVM 中的 Java 線程與原生操作系統(tǒng)線程有直接的映射關(guān)系。當(dāng)線程本地存儲(chǔ)、緩沖區(qū)分配、同步對(duì)象、棧、程序計(jì)數(shù)器等準(zhǔn)備好以后,就會(huì)創(chuàng)建一個(gè)操作系統(tǒng)原生線程。
Java 線程結(jié)束,原生線程隨之被回收。操作系統(tǒng)負(fù)責(zé)調(diào)度所有線程,并把它們分配到任何可用的 CPU 上。當(dāng)原生線程初始化完畢,就會(huì)調(diào)用 Java 線程的 run() 方法。當(dāng)線程結(jié)束時(shí),會(huì)釋放原生線程和 Java 線程的所有資源。
Hotspot JVM 后臺(tái)運(yùn)行的系統(tǒng)線程主要有下面幾個(gè):
JVM 內(nèi)存區(qū)域
JVM 內(nèi)存區(qū)域主要分為線程私有區(qū)域【程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法區(qū)】、線程共享區(qū)域【JAVA 堆、方法區(qū)】、直接內(nèi)存。
線程私有數(shù)據(jù)區(qū)域生命周期與線程相同, 依賴(lài)用戶(hù)線程的啟動(dòng)/結(jié)束 而 創(chuàng)建/銷(xiāo)毀(在 HotspotVM 內(nèi), 每個(gè)線程都與操作系統(tǒng)的本地線程直接映射, 因此這部分內(nèi)存區(qū)域的存/否跟隨本地線程的生/死對(duì)應(yīng))
線程共享區(qū)域隨虛擬機(jī)的啟動(dòng)/關(guān)閉而創(chuàng)建/銷(xiāo)毀。
直接內(nèi)存并不是 JVM 運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分, 但也會(huì)被頻繁的使用: 在 JDK 1.4 引入的 NIO 提供了基于 Channel 與 Buffer 的 IO 方式, 它可以使用 Native 函數(shù)庫(kù)直接分配堆外內(nèi)存, 然后使用DirectByteBuffer 對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作(詳見(jiàn): Java I/O 擴(kuò)展), 這樣就避免了在 Java堆和 Native 堆中來(lái)回復(fù)制數(shù)據(jù), 因此在一些場(chǎng)景中可以顯著提高性能。
程序計(jì)數(shù)器(線程私有)
一塊較小的內(nèi)存空間, 是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器,每條線程都要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,這類(lèi)內(nèi)存也稱(chēng)為“線程私有”的內(nèi)存。
正在執(zhí)行 java 方法的話,計(jì)數(shù)器記錄的是虛擬機(jī)字節(jié)碼指令的地址(當(dāng)前指令的地址)。如果還是 Native 方法,則為空。
這個(gè)內(nèi)存區(qū)域是唯一一個(gè)在虛擬機(jī)中沒(méi)有規(guī)定任何 OutOfMemoryError 情況的區(qū)域。
虛擬機(jī)棧(線程私有)
是描述java方法執(zhí)行的內(nèi)存模型,每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。每一個(gè)方法從調(diào)用直至執(zhí)行完成的過(guò)程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中入棧到出棧的過(guò)程。
棧幀( Frame)是用來(lái)存儲(chǔ)數(shù)據(jù)和部分過(guò)程結(jié)果的數(shù)據(jù)結(jié)構(gòu),同時(shí)也被用來(lái)處理動(dòng)態(tài)鏈接(Dynamic Linking)、 方法返回值和異常分派( Dispatch Exception)。棧幀隨著方法調(diào)用而創(chuàng)建,隨著方法結(jié)束而銷(xiāo)毀——無(wú)論方法是正常完成還是異常完成(拋出了在方法內(nèi)未被捕獲的異常)都算作方法結(jié)束。
本地方法區(qū)(線程私有)
本地方法區(qū)和 Java Stack 作用類(lèi)似, 區(qū)別是虛擬機(jī)棧為執(zhí)行 Java 方法服務(wù), 而本地方法棧則為Native 方法服務(wù), 如果一個(gè) VM 實(shí)現(xiàn)使用 C-linkage 模型來(lái)支持 Native 調(diào)用, 那么該棧將會(huì)是一個(gè)C 棧,但 HotSpot VM 直接就把本地方法棧和虛擬機(jī)棧合二為一。
堆(Heap-線程共享)-運(yùn)行時(shí)數(shù)據(jù)區(qū)
是被線程共享的一塊內(nèi)存區(qū)域,創(chuàng)建的對(duì)象和數(shù)組都保存在 Java 堆內(nèi)存中,也是垃圾收集器進(jìn)行垃圾收集的最重要的內(nèi)存區(qū)域。由于現(xiàn)代 VM 采用分代收集算法, 因此 Java 堆從 GC 的角度還可以細(xì)分為: 新生代(Eden 區(qū)、From Survivor 區(qū)和 To Survivor 區(qū))和老年代。
方法區(qū)/永久代(線程共享)
即我們常說(shuō)的永久代(Permanent Generation), 用于存儲(chǔ)被 JVM 加載的類(lèi)信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù). HotSpot VM把GC分代收集擴(kuò)展至方法區(qū), 即使用Java堆的永久代來(lái)實(shí)現(xiàn)方法區(qū), 這樣 HotSpot 的垃圾收集器就可以像管理 Java 堆一樣管理這部分內(nèi)存,而不必為方法區(qū)開(kāi)發(fā)專(zhuān)門(mén)的內(nèi)存管理器(永久帶的內(nèi)存回收的主要目標(biāo)是針對(duì)常量池的回收和類(lèi)型的卸載, 因此收益一般很小)。
運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class 文件中除了有類(lèi)的版本、字段、方法、接口等描述等信息外,還有一項(xiàng)信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類(lèi)加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中。 Java 虛擬機(jī)對(duì) Class 文件的每一部分(自然也包括常量池)的格式都有嚴(yán)格的規(guī)定,每一個(gè)字節(jié)用于存儲(chǔ)哪種數(shù)據(jù)都必須符合規(guī)范上的要求,這樣才會(huì)被虛擬機(jī)認(rèn)可、裝載和執(zhí)行。
JVM 運(yùn)行時(shí)內(nèi)存
Java 堆從 GC 的角度還可以細(xì)分為: 新生代(Eden 區(qū)、From Survivor 區(qū)和 To Survivor 區(qū))和老年代。
新生代
是用來(lái)存放新生的對(duì)象。一般占據(jù)堆的 1/3 空間。由于頻繁創(chuàng)建對(duì)象,所以新生代會(huì)頻繁觸發(fā)MinorGC 進(jìn)行垃圾回收。新生代又分為 Eden 區(qū)、ServivorFrom、ServivorTo 三個(gè)區(qū)。
Eden 區(qū)
Java 新對(duì)象的出生地(如果新創(chuàng)建的對(duì)象占用內(nèi)存很大,則直接分配到老年代)。當(dāng) Eden 區(qū)內(nèi)存不夠的時(shí)候就會(huì)觸發(fā) MinorGC,對(duì)新生代區(qū)進(jìn)行一次垃圾回收。
ServivorFrom
上一次 GC 的幸存者,作為這一次 GC 的被掃描者。
ServivorTo
保留了一次 MinorGC 過(guò)程中的幸存者。
MinorGC 的過(guò)程(復(fù)制->清空->互換)
MinorGC 采用復(fù)制算法
1:eden、servicorFrom 復(fù)制到 ServicorTo,年齡+1
首先,把 Eden 和 ServivorFrom 區(qū)域中存活的對(duì)象復(fù)制到 ServicorTo 區(qū)域(如果有對(duì)象的年齡以及達(dá)到了老年的標(biāo)準(zhǔn),則賦值到老年代區(qū)),同時(shí)把這些對(duì)象的年齡+1(如果 ServicorTo 不夠位置了就放到老年區(qū));
2:清空 eden、servicorFrom
然后,清空 Eden 和 ServicorFrom 中的對(duì)象;
3:ServicorTo 和 ServicorFrom 互換
最后,ServicorTo 和 ServicorFrom 互換,原 ServicorTo 成為下一次 GC 時(shí)的 ServicorFrom區(qū)。
老年代
主要存放應(yīng)用程序中生命周期長(zhǎng)的內(nèi)存對(duì)象。
老年代的對(duì)象比較穩(wěn)定,所以 MajorGC 不會(huì)頻繁執(zhí)行。在進(jìn)行 MajorGC 前一般都先進(jìn)行了一次 MinorGC,使得有新生代的對(duì)象晉身入老年代,導(dǎo)致空間不夠用時(shí)才觸發(fā)。當(dāng)無(wú)法找到足夠大的連續(xù)空間分配給新創(chuàng)建的較大對(duì)象時(shí)也會(huì)提前觸發(fā)一次 MajorGC 進(jìn)行垃圾回收騰出空間。
MajorGC 采用標(biāo)記清除算法:首先掃描一次所有老年代,標(biāo)記出存活的對(duì)象,然后回收沒(méi)有標(biāo)記的對(duì)象。MajorGC 的耗時(shí)比較長(zhǎng),因?yàn)橐獟呙柙倩厥铡ajorGC 會(huì)產(chǎn)生內(nèi)存碎片,為了減少內(nèi)存損耗,我們一般需要進(jìn)行合并或者標(biāo)記出來(lái)方便下次直接分配。當(dāng)老年代也滿了裝不下的時(shí)候,就會(huì)拋出 OOM(Out of Memory)異常。
永久代
指內(nèi)存的永久保存區(qū)域,主要存放 Class 和 Meta(元數(shù)據(jù))的信息,Class 在被加載的時(shí)候被放入永久區(qū)域,它和和存放實(shí)例的區(qū)域不同,GC 不會(huì)在主程序運(yùn)行期對(duì)永久區(qū)域進(jìn)行清理。所以這也導(dǎo)致了永久代的區(qū)域會(huì)隨著加載的 Class 的增多而脹滿,最終拋出 OOM 異常。
JAVA8 與元數(shù)據(jù)
在 Java8 中,永久代已經(jīng)被移除,被一個(gè)稱(chēng)為“元數(shù)據(jù)區(qū)”(元空間)的區(qū)域所取代。元空間的本質(zhì)和永久代類(lèi)似,元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。因此,默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制。類(lèi)的元數(shù)據(jù)放入 nativememory, 字符串池和類(lèi)的靜態(tài)變量放入 java 堆中,這樣可以加載多少類(lèi)的元數(shù)據(jù)就不再由MaxPermSize 控制, 而由系統(tǒng)的實(shí)際可用空間來(lái)控制。
垃圾回收與算法
如何確定垃圾
引用計(jì)數(shù)法
在 Java 中,引用和對(duì)象是有關(guān)聯(lián)的。如果要操作對(duì)象則必須用引用進(jìn)行。因此,很顯然一個(gè)簡(jiǎn)單的辦法是通過(guò)引用計(jì)數(shù)來(lái)判斷一個(gè)對(duì)象是否可以回收。簡(jiǎn)單說(shuō),即一個(gè)對(duì)象如果沒(méi)有任何與之關(guān)聯(lián)的引用,即他們的引用計(jì)數(shù)都不為 0,則說(shuō)明對(duì)象不太可能再被用到,那么這個(gè)對(duì)象就是可回收對(duì)象。
可達(dá)性分析
為了解決引用計(jì)數(shù)法的循環(huán)引用問(wèn)題,Java 使用了可達(dá)性分析的方法。通過(guò)一系列的“GC roots”對(duì)象作為起點(diǎn)搜索。如果在“GC roots”和一個(gè)對(duì)象之間沒(méi)有可達(dá)路徑,則稱(chēng)該對(duì)象是不可達(dá)的。
要注意的是,不可達(dá)對(duì)象不等價(jià)于可回收對(duì)象,不可達(dá)對(duì)象變?yōu)榭苫厥諏?duì)象至少要經(jīng)過(guò)兩次標(biāo)記過(guò)程。兩次標(biāo)記后仍然是可回收對(duì)象,則將面臨回收。
標(biāo)記清除算法(Mark-Sweep)
最基礎(chǔ)的垃圾回收算法,分為兩個(gè)階段,標(biāo)注和清除。標(biāo)記階段標(biāo)記出所有需要回收的對(duì)象,清除階段回收被標(biāo)記的對(duì)象所占用的空間。如圖
從圖中我們就可以發(fā)現(xiàn),該算法最大的問(wèn)題是內(nèi)存碎片化嚴(yán)重,后續(xù)可能發(fā)生大對(duì)象不能找到可利用空間的問(wèn)題。
復(fù)制算法(copying)
為了解決 Mark-Sweep 算法內(nèi)存碎片化的缺陷而被提出的算法。按內(nèi)存容量將內(nèi)存劃分為等大小的兩塊。每次只使用其中一塊,當(dāng)這一塊內(nèi)存滿后將尚存活的對(duì)象復(fù)制到另一塊上去,把已使用的內(nèi)存清掉,如圖:
這種算法雖然實(shí)現(xiàn)簡(jiǎn)單,內(nèi)存效率高,不易產(chǎn)生碎片,但是最大的問(wèn)題是可用內(nèi)存被壓縮到了原本的一半。且存活對(duì)象增多的話,Copying 算法的效率會(huì)大大降低。
標(biāo)記整理算法(Mark-Compact)
結(jié)合了以上兩個(gè)算法,為了避免缺陷而提出。標(biāo)記階段和 Mark-Sweep 算法相同,標(biāo)記后不是清理對(duì)象,而是將存活對(duì)象移向內(nèi)存的一端。然后清除端邊界外的對(duì)象。如圖:
分代收集算法
分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根據(jù)對(duì)象存活的不同生命周期將內(nèi)存劃分為不同的域,一般情況下將 GC 堆劃分為老生代(Tenured/Old Generation)和新生代(YoungGeneration)。老生代的特點(diǎn)是每次垃圾回收時(shí)只有少量對(duì)象需要被回收,新生代的特點(diǎn)是每次垃圾回收時(shí)都有大量垃圾需要被回收,因此可以根據(jù)不同區(qū)域選擇不同的算法。
新生代與復(fù)制算法
目前大部分 JVM 的 GC 對(duì)于新生代都采取 Copying 算法,因?yàn)樾律忻看卫厥斩家厥沾蟛糠謱?duì)象,即要復(fù)制的操作比較少,但通常并不是按照 1:1 來(lái)劃分新生代。一般將新生代劃分為一塊較大的 Eden 空間和兩個(gè)較小的 Survivor 空間(From Space, To Space),每次使用Eden 空間和其中的一塊 Survivor 空間,當(dāng)進(jìn)行回收時(shí),將該兩塊空間中還存活的對(duì)象復(fù)制到另一塊 Survivor 空間中。
老年代與標(biāo)記復(fù)制算法
而老年代因?yàn)槊看沃换厥丈倭繉?duì)象,因而采用 Mark-Compact 算法。
1. JAVA 虛擬機(jī)提到過(guò)的處于方法區(qū)的永生代(Permanet Generation),它用來(lái)存儲(chǔ) class 類(lèi),常量,方法描述等。對(duì)永生代的回收主要包括廢棄常量和無(wú)用的類(lèi)。
2. 對(duì)象的內(nèi)存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目前存放對(duì)象的那一塊),少數(shù)情況會(huì)直接分配到老生代。
3. 當(dāng)新生代的 Eden Space 和 From Space 空間不足時(shí)就會(huì)發(fā)生一次 GC,進(jìn)行 GC 后,EdenSpace 和 From Space 區(qū)的存活對(duì)象會(huì)被挪到 To Space,然后將 Eden Space 和 FromSpace 進(jìn)行清理。
4. 如果 To Space 無(wú)法足夠存儲(chǔ)某個(gè)對(duì)象,則將這個(gè)對(duì)象存儲(chǔ)到老生代。
5. 在進(jìn)行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反復(fù)循環(huán)。
6. 當(dāng)對(duì)象在 Survivor 區(qū)躲過(guò)一次 GC 后,其年齡就會(huì)+1。默認(rèn)情況下年齡到達(dá) 15 的對(duì)象會(huì)被移到老生代中。
JAVA 四種引用類(lèi)型
強(qiáng)引用
在 Java 中最常見(jiàn)的就是強(qiáng)引用,把一個(gè)對(duì)象賦給一個(gè)引用變量,這個(gè)引用變量就是一個(gè)強(qiáng)引用。當(dāng)一個(gè)對(duì)象被強(qiáng)引用變量引用時(shí),它處于可達(dá)狀態(tài),它是不可能被垃圾回收機(jī)制回收的,即使該對(duì)象以后永遠(yuǎn)都不會(huì)被用到 JVM 也不會(huì)回收。因此強(qiáng)引用是造成 Java 內(nèi)存泄漏的主要原因之一。
軟引用
軟引用需要用 SoftReference 類(lèi)來(lái)實(shí)現(xiàn),對(duì)于只有軟引用的對(duì)象來(lái)說(shuō),當(dāng)系統(tǒng)內(nèi)存足夠時(shí)它不會(huì)被回收,當(dāng)系統(tǒng)內(nèi)存空間不足時(shí)它會(huì)被回收。軟引用通常用在對(duì)內(nèi)存敏感的程序中。
弱引用
弱引用需要用 WeakReference 類(lèi)來(lái)實(shí)現(xiàn),它比軟引用的生存期更短,對(duì)于只有弱引用的對(duì)象來(lái)說(shuō),只要垃圾回收機(jī)制一運(yùn)行,不管 JVM 的內(nèi)存空間是否足夠,總會(huì)回收該對(duì)象占用的內(nèi)存。
虛引用
虛引用需要 PhantomReference 類(lèi)來(lái)實(shí)現(xiàn),它不能單獨(dú)使用,必須和引用隊(duì)列聯(lián)合使用。虛引用的主要作用是跟蹤對(duì)象被垃圾回收的狀態(tài)。
GC 分代收集算法 VS 分區(qū)收集算法
分代收集算法
當(dāng)前主流 VM 垃圾收集都采用”分代收集”(Generational Collection)算法, 這種算法會(huì)根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊, 如 JVM 中的 新生代、老年代、永久代,這樣就可以根據(jù)各年代特點(diǎn)分別采用最適當(dāng)?shù)?GC 算法
在新生代-復(fù)制算法
每次垃圾收集都能發(fā)現(xiàn)大批對(duì)象已死, 只有少量存活. 因此選用復(fù)制算法, 只需要付出少量存活對(duì)象的復(fù)制成本就可以完成收集.
在老年代-標(biāo)記整理算法
因?yàn)閷?duì)象存活率高、沒(méi)有額外空間對(duì)它進(jìn)行分配擔(dān)保, 就必須采用“標(biāo)記—清理”或“標(biāo)記—整理”算法來(lái)進(jìn)行回收, 不必進(jìn)行內(nèi)存復(fù)制, 且直接騰出空閑內(nèi)存.
分區(qū)收集算法
分區(qū)算法則將整個(gè)堆空間劃分為連續(xù)的不同小區(qū)間, 每個(gè)小區(qū)間獨(dú)立使用, 獨(dú)立回收. 這樣做的好處是可以控制一次回收多少個(gè)小區(qū)間 , 根據(jù)目標(biāo)停頓時(shí)間, 每次合理地回收若干個(gè)小區(qū)間(而不是整個(gè)堆), 從而減少一次 GC 所產(chǎn)生的停頓。
GC 垃圾收集器
Java 堆內(nèi)存被劃分為新生代和年老代兩部分,新生代主要使用復(fù)制和標(biāo)記-清除垃圾回收算法;
年老代主要使用標(biāo)記-整理垃圾回收算法,因此 java 虛擬中針對(duì)新生代和年老代分別提供了多種不同的垃圾收集器,JDK1.6 中 Sun HotSpot 虛擬機(jī)的垃圾收集器如下:
Serial 垃圾收集器(單線程、復(fù)制算法)
Serial(英文連續(xù))是最基本垃圾收集器,使用復(fù)制算法,曾經(jīng)是JDK1.3.1 之前新生代唯一的垃圾收集器。Serial 是一個(gè)單線程的收集器,它不但只會(huì)使用一個(gè) CPU 或一條線程去完成垃圾收集工作,并且在進(jìn)行垃圾收集的同時(shí),必須暫停其他所有的工作線程,直到垃圾收集結(jié)束。
Serial 垃圾收集器雖然在收集垃圾過(guò)程中需要暫停所有其他的工作線程,但是它簡(jiǎn)單高效,對(duì)于限定單個(gè) CPU 環(huán)境來(lái)說(shuō),沒(méi)有線程交互的開(kāi)銷(xiāo),可以獲得最高的單線程垃圾收集效率,因此 Serial垃圾收集器依然是 java 虛擬機(jī)運(yùn)行在 Client 模式下默認(rèn)的新生代垃圾收集器。
ParNew 垃圾收集器(Serial+多線程)
ParNew 垃圾收集器其實(shí)是 Serial 收集器的多線程版本,也使用復(fù)制算法,除了使用多線程進(jìn)行垃圾收集之外,其余的行為和 Serial 收集器完全一樣,ParNew 垃圾收集器在垃圾收集過(guò)程中同樣也要暫停所有其他的工作線程。
ParNew 收集器默認(rèn)開(kāi)啟和 CPU 數(shù)目相同的線程數(shù),可以通過(guò)-XX:ParallelGCThreads 參數(shù)來(lái)限制垃圾收集器的線程數(shù)。【Parallel:平行的】
ParNew雖然是除了多線程外和Serial 收集器幾乎完全一樣,但是ParNew垃圾收集器是很多 java虛擬機(jī)運(yùn)行在 Server 模式下新生代的默認(rèn)垃圾收集器。
Parallel Scavenge 收集器(多線程復(fù)制算法、高效)
Parallel Scavenge 收集器也是一個(gè)新生代垃圾收集器,同樣使用復(fù)制算法,也是一個(gè)多線程的垃圾收集器,它重點(diǎn)關(guān)注的是程序達(dá)到一個(gè)可控制的吞吐量(Thoughput,CPU 用于運(yùn)行用戶(hù)代碼的時(shí)間/CPU 總消耗時(shí)間,即吞吐量=運(yùn)行用戶(hù)代碼時(shí)間/(運(yùn)行用戶(hù)代碼時(shí)間+垃圾收集時(shí)間)),高吞吐量可以最高效率地利用 CPU 時(shí)間,盡快地完成程序的運(yùn)算任務(wù),主要適用于在后臺(tái)運(yùn)算而不需要太多交互的任務(wù)。自適應(yīng)調(diào)節(jié)策略也是 ParallelScavenge 收集器與 ParNew 收集器的一個(gè)重要區(qū)別。
Serial Old 收集器(單線程標(biāo)記整理算法 )
Serial Old 是 Serial 垃圾收集器年老代版本,它同樣是個(gè)單線程的收集器,使用標(biāo)記-整理算法,這個(gè)收集器也主要是運(yùn)行在 Client 默認(rèn)的 java 虛擬機(jī)默認(rèn)的年老代垃圾收集器。
在 Server 模式下,主要有兩個(gè)用途:
1. 在 JDK1.5 之前版本中與新生代的 Parallel Scavenge 收集器搭配使用。
2. 作為年老代中使用 CMS 收集器的后備垃圾收集方案。
新生代 Serial 與年老代 Serial Old 搭配垃圾收集過(guò)程圖:
新生代 Parallel Scavenge 收集器與 ParNew 收集器工作原理類(lèi)似,都是多線程的收集器,都使用的是復(fù)制算法,在垃圾收集過(guò)程中都需要暫停所有的工作線程。新生代 ParallelScavenge/ParNew 與年老代 Serial Old 搭配垃圾收集過(guò)程圖:
Parallel Old 收集器(多線程標(biāo)記整理算法)
Parallel Old 收集器是Parallel Scavenge的年老代版本,使用多線程的標(biāo)記-整理算法,在 JDK1.6才開(kāi)始提供。
在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保證新生代的吞吐量?jī)?yōu)先,無(wú)法保證整體的吞吐量,Parallel Old 正是為了在年老代同樣提供吞吐量?jī)?yōu)先的垃圾收集器,如果系統(tǒng)對(duì)吞吐量要求比較高,可以?xún)?yōu)先考慮新生代 Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略。
新生代 Parallel Scavenge 和年老代 Parallel Old 收集器搭配運(yùn)行過(guò)程圖:
CMS 收集器(多線程標(biāo)記清除算法)
Concurrent mark sweep(CMS)收集器是一種年老代垃圾收集器,其最主要目標(biāo)是獲取最短垃圾回收停頓時(shí)間,和其他年老代使用標(biāo)記-整理算法不同,它使用多線程的標(biāo)記-清除算法。
最短的垃圾收集停頓時(shí)間可以為交互比較高的程序提高用戶(hù)體驗(yàn)。
CMS 工作機(jī)制相比其他的垃圾收集器來(lái)說(shuō)更復(fù)雜,整個(gè)過(guò)程分為以下 4 個(gè)階段:
初始標(biāo)記
只是標(biāo)記一下 GC Roots 能直接關(guān)聯(lián)的對(duì)象,速度很快,仍然需要暫停所有的工作線程。
并發(fā)標(biāo)記
進(jìn)行 GC Roots 跟蹤的過(guò)程,和用戶(hù)線程一起工作,不需要暫停工作線程。
重新標(biāo)記
為了修正在并發(fā)標(biāo)記期間,因用戶(hù)程序繼續(xù)運(yùn)行而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記
記錄,仍然需要暫停所有的工作線程。
并發(fā)清除
清除 GC Roots 不可達(dá)對(duì)象,和用戶(hù)線程一起工作,不需要暫停工作線程。由于耗時(shí)最長(zhǎng)的并發(fā)標(biāo)記和并發(fā)清除過(guò)程中,垃圾收集線程可以和用戶(hù)現(xiàn)在一起并發(fā)工作,所以總體上來(lái)看CMS 收集器的內(nèi)存回收和用戶(hù)線程是一起并發(fā)地執(zhí)行。
CMS 收集器工作過(guò)程:
G1 收集器
Garbage first 垃圾收集器是目前垃圾收集器理論發(fā)展的最前沿成果,相比與 CMS 收集器,G1 收集器兩個(gè)最突出的改進(jìn)是:
1. 基于標(biāo)記-整理算法,不產(chǎn)生內(nèi)存碎片。
2. 可以非常精確控制停頓時(shí)間,在不犧牲吞吐量前提下,實(shí)現(xiàn)低停頓垃圾回收。
G1 收集器避免全區(qū)域垃圾收集,它把堆內(nèi)存劃分為大小固定的幾個(gè)獨(dú)立區(qū)域,并且跟蹤這些區(qū)域的垃圾收集進(jìn)度,同時(shí)在后臺(tái)維護(hù)一個(gè)優(yōu)先級(jí)列表,每次根據(jù)所允許的收集時(shí)間,優(yōu)先回收垃圾最多的區(qū)域。區(qū)域劃分和優(yōu)先級(jí)區(qū)域回收機(jī)制,確保 G1 收集器可以在有限時(shí)間獲得最高的垃圾收集效率。
JAVA IO/NIO
阻塞 IO 模型
最傳統(tǒng)的一種 IO 模型,即在讀寫(xiě)數(shù)據(jù)過(guò)程中會(huì)發(fā)生阻塞現(xiàn)象。當(dāng)用戶(hù)線程發(fā)出 IO 請(qǐng)求之后,內(nèi)核會(huì)去查看數(shù)據(jù)是否就緒,如果沒(méi)有就緒就會(huì)等待數(shù)據(jù)就緒,而用戶(hù)線程就會(huì)處于阻塞狀態(tài),用戶(hù)線程交出 CPU。當(dāng)數(shù)據(jù)就緒之后,內(nèi)核會(huì)將數(shù)據(jù)拷貝到用戶(hù)線程,并返回結(jié)果給用戶(hù)線程,用戶(hù)線程才解除 block 狀態(tài)。典型的阻塞 IO 模型的例子為:data = socket.read();如果數(shù)據(jù)沒(méi)有就緒,就會(huì)一直阻塞在 read 方法。
非阻塞 IO 模型
當(dāng)用戶(hù)線程發(fā)起一個(gè) read 操作后,并不需要等待,而是馬上就得到了一個(gè)結(jié)果。如果結(jié)果是一個(gè)error 時(shí),它就知道數(shù)據(jù)還沒(méi)有準(zhǔn)備好,于是它可以再次發(fā)送 read 操作。一旦內(nèi)核中的數(shù)據(jù)準(zhǔn)備好了,并且又再次收到了用戶(hù)線程的請(qǐng)求,那么它馬上就將數(shù)據(jù)拷貝到了用戶(hù)線程,然后返回。
所以事實(shí)上,在非阻塞 IO 模型中,用戶(hù)線程需要不斷地詢(xún)問(wèn)內(nèi)核數(shù)據(jù)是否就緒,也就說(shuō)非阻塞 IO不會(huì)交出 CPU,而會(huì)一直占用 CPU。典型的非阻塞 IO 模型一般如下:
while(true){
data = socket.read();
if(data!= error){
處理數(shù)據(jù)
break;
}
}
但是對(duì)于非阻塞 IO 就有一個(gè)非常嚴(yán)重的問(wèn)題,在 while 循環(huán)中需要不斷地去詢(xún)問(wèn)內(nèi)核數(shù)據(jù)是否就緒,這樣會(huì)導(dǎo)致 CPU 占用率非常高,因此一般情況下很少使用 while 循環(huán)這種方式來(lái)讀取數(shù)據(jù)。
多路復(fù)用 IO 模型
多路復(fù)用 IO 模型是目前使用得比較多的模型。Java NIO 實(shí)際上就是多路復(fù)用 IO。在多路復(fù)用 IO模型中,會(huì)有一個(gè)線程不斷去輪詢(xún)多個(gè) socket 的狀態(tài),只有當(dāng) socket 真正有讀寫(xiě)事件時(shí),才真正調(diào)用實(shí)際的 IO 讀寫(xiě)操作。因?yàn)樵诙嗦窂?fù)用 IO 模型中,只需要使用一個(gè)線程就可以管理多個(gè)socket,系統(tǒng)不需要建立新的進(jìn)程或者線程,也不必維護(hù)這些線程和進(jìn)程,并且只有在真正有socket 讀寫(xiě)事件進(jìn)行時(shí),才會(huì)使用 IO 資源,所以它大大減少了資源占用。在 Java NIO 中,是通過(guò) selector.select()去查詢(xún)每個(gè)通道是否有到達(dá)事件,如果沒(méi)有事件,則一直阻塞在那里,因此這種方式會(huì)導(dǎo)致用戶(hù)線程的阻塞。多路復(fù)用 IO 模式,通過(guò)一個(gè)線程就可以管理多個(gè) socket,只有當(dāng)socket 真正有讀寫(xiě)事件發(fā)生才會(huì)占用資源來(lái)進(jìn)行實(shí)際的讀寫(xiě)操作。因此,多路復(fù)用 IO 比較適合連接數(shù)比較多的情況。
另外多路復(fù)用 IO 為何比非阻塞 IO 模型的效率高是因?yàn)樵诜亲枞?IO 中,不斷地詢(xún)問(wèn) socket 狀態(tài)時(shí)通過(guò)用戶(hù)線程去進(jìn)行的,而在多路復(fù)用 IO 中,輪詢(xún)每個(gè) socket 狀態(tài)是內(nèi)核在進(jìn)行的,這個(gè)效率要比用戶(hù)線程要高的多。
不過(guò)要注意的是,多路復(fù)用 IO 模型是通過(guò)輪詢(xún)的方式來(lái)檢測(cè)是否有事件到達(dá),并且對(duì)到達(dá)的事件逐一進(jìn)行響應(yīng)。因此對(duì)于多路復(fù)用 IO 模型來(lái)說(shuō),一旦事件響應(yīng)體很大,那么就會(huì)導(dǎo)致后續(xù)的事件遲遲得不到處理,并且會(huì)影響新的事件輪詢(xún)。
信號(hào)驅(qū)動(dòng) IO 模型
在信號(hào)驅(qū)動(dòng) IO 模型中,當(dāng)用戶(hù)線程發(fā)起一個(gè) IO 請(qǐng)求操作,會(huì)給對(duì)應(yīng)的 socket 注冊(cè)一個(gè)信號(hào)函數(shù),然后用戶(hù)線程會(huì)繼續(xù)執(zhí)行,當(dāng)內(nèi)核數(shù)據(jù)就緒時(shí)會(huì)發(fā)送一個(gè)信號(hào)給用戶(hù)線程,用戶(hù)線程接收到信號(hào)之后,便在信號(hào)函數(shù)中調(diào)用 IO 讀寫(xiě)操作來(lái)進(jìn)行實(shí)際的 IO 請(qǐng)求操作。
異步 IO 模型
異步 IO 模型才是最理想的 IO 模型,在異步 IO 模型中,當(dāng)用戶(hù)線程發(fā)起 read 操作之后,立刻就可以開(kāi)始去做其它的事。而另一方面,從內(nèi)核的角度,當(dāng)它受到一個(gè) asynchronous read 之后,它會(huì)立刻返回,說(shuō)明 read 請(qǐng)求已經(jīng)成功發(fā)起了,因此不會(huì)對(duì)用戶(hù)線程產(chǎn)生任何 block。然后,內(nèi)核會(huì)等待數(shù)據(jù)準(zhǔn)備完成,然后將數(shù)據(jù)拷貝到用戶(hù)線程,當(dāng)這一切都完成之后,內(nèi)核會(huì)給用戶(hù)線程發(fā)送一個(gè)信號(hào),告訴它 read 操作完成了。也就說(shuō)用戶(hù)線程完全不需要實(shí)際的整個(gè) IO 操作是如何進(jìn)行的,只需要先發(fā)起一個(gè)請(qǐng)求,當(dāng)接收內(nèi)核返回的成功信號(hào)時(shí)表示 IO 操作已經(jīng)完成,可以直接去使用數(shù)據(jù)了。
也就說(shuō)在異步 IO 模型中,IO 操作的兩個(gè)階段都不會(huì)阻塞用戶(hù)線程,這兩個(gè)階段都是由內(nèi)核自動(dòng)完成,然后發(fā)送一個(gè)信號(hào)告知用戶(hù)線程操作已完成。用戶(hù)線程中不需要再次調(diào)用 IO 函數(shù)進(jìn)行具體的讀寫(xiě)。這點(diǎn)是和信號(hào)驅(qū)動(dòng)模型有所不同的,在信號(hào)驅(qū)動(dòng)模型中,當(dāng)用戶(hù)線程接收到信號(hào)表示數(shù)據(jù)已經(jīng)就緒,然后需要用戶(hù)線程調(diào)用 IO 函數(shù)進(jìn)行實(shí)際的讀寫(xiě)操作;而在異步 IO 模型中,收到信號(hào)表示 IO 操作已經(jīng)完成,不需要再在用戶(hù)線程中調(diào)用 IO 函數(shù)進(jìn)行實(shí)際的讀寫(xiě)操作。
JAVA IO 包
JAVA NIO
NIO 主要有三大核心部分:Channel(通道),Buffer(緩沖區(qū)), Selector。傳統(tǒng) IO 基于字節(jié)流和字符流進(jìn)行操作,而 NIO 基于 Channel 和 Buffer(緩沖區(qū))進(jìn)行操作,數(shù)據(jù)總是從通道讀取到緩沖區(qū)中,或者從緩沖區(qū)寫(xiě)入到通道中。Selector(選擇區(qū))用于監(jiān)聽(tīng)多個(gè)通道的事件(比如:連接打開(kāi),數(shù)據(jù)到達(dá))。因此,單個(gè)線程可以監(jiān)聽(tīng)多個(gè)數(shù)據(jù)通道。
NIO 和傳統(tǒng) IO 之間第一個(gè)最大的區(qū)別是,IO 是面向流的,NIO 是面向緩沖區(qū)的。
NIO 的緩沖區(qū)
Java IO 面向流意味著每次從流中讀一個(gè)或多個(gè)字節(jié),直至讀取所有字節(jié),它們沒(méi)有被緩存在任何地方。此外,它不能前后移動(dòng)流中的數(shù)據(jù)。如果需要前后移動(dòng)從流中讀取的數(shù)據(jù),需要先將它緩存到一個(gè)緩沖區(qū)。NIO 的緩沖導(dǎo)向方法不同。數(shù)據(jù)讀取到一個(gè)它稍后處理的緩沖區(qū),需要時(shí)可在緩沖區(qū)中前后移動(dòng)。這就增加了處理過(guò)程中的靈活性。但是,還需要檢查是否該緩沖區(qū)中包含所有您需要處理的數(shù)據(jù)。而且,需確保當(dāng)更多的數(shù)據(jù)讀入緩沖區(qū)時(shí),不要覆蓋緩沖區(qū)里尚未處理的數(shù)據(jù)。
NIO 的非阻塞
IO 的各種流是阻塞的。這意味著,當(dāng)一個(gè)線程調(diào)用 read() 或 write()時(shí),該線程被阻塞,直到有一些數(shù)據(jù)被讀取,或數(shù)據(jù)完全寫(xiě)入。該線程在此期間不能再干任何事情了。 NIO 的非阻塞模式,使一個(gè)線程從某通道發(fā)送請(qǐng)求讀取數(shù)據(jù),但是它僅能得到目前可用的數(shù)據(jù),如果目前沒(méi)有數(shù)據(jù)可用時(shí),就什么都不會(huì)獲取。而不是保持線程阻塞,所以直至數(shù)據(jù)變的可以讀取之前,該線程可以繼續(xù)做其他的事情。 非阻塞寫(xiě)也是如此。一個(gè)線程請(qǐng)求寫(xiě)入一些數(shù)據(jù)到某通道,但不需要等待它完全寫(xiě)入,這個(gè)線程同時(shí)可以去做別的事情。 線程通常將非阻塞 IO 的空閑時(shí)間用于在其它通道上執(zhí)行 IO 操作,所以一個(gè)單獨(dú)的線程現(xiàn)在可以管理多個(gè)輸入和輸出通道(channel)。
Channel
首先說(shuō)一下 Channel,國(guó)內(nèi)大多翻譯成“通道”。Channel 和 IO 中的 Stream(流)是差不多一個(gè)等級(jí)的。只不過(guò) Stream 是單向的,譬如:InputStream, OutputStream,而 Channel 是雙向的,既可以用來(lái)進(jìn)行讀操作,又可以用來(lái)進(jìn)行寫(xiě)操作。
NIO 中的 Channel 的主要實(shí)現(xiàn)有:
1. FileChannel
2. DatagramChannel
3. SocketChannel
4. ServerSocketChannel
這里看名字就可以猜出個(gè)所以然來(lái):分別可以對(duì)應(yīng)文件 IO、UDP 和 TCP(Server 和 Client)。
下面演示的案例基本上就是圍繞這 4 個(gè)類(lèi)型的 Channel 進(jìn)行陳述的。
Buffer
Buffer,故名思意,緩沖區(qū),實(shí)際上是一個(gè)容器,是一個(gè)連續(xù)數(shù)組。Channel 提供從文件、網(wǎng)絡(luò)讀取數(shù)據(jù)的渠道,但是讀取或?qū)懭氲臄?shù)據(jù)都必須經(jīng)由 Buffer。
上面的圖描述了從一個(gè)客戶(hù)端向服務(wù)端發(fā)送數(shù)據(jù),然后服務(wù)端接收數(shù)據(jù)的過(guò)程。客戶(hù)端發(fā)送數(shù)據(jù)時(shí),必須先將數(shù)據(jù)存入 Buffer 中,然后將 Buffer 中的內(nèi)容寫(xiě)入通道。服務(wù)端這邊接收數(shù)據(jù)必須通過(guò) Channel 將數(shù)據(jù)讀入到 Buffer 中,然后再?gòu)?Buffer 中取出數(shù)據(jù)來(lái)處理。
在 NIO 中,Buffer 是一個(gè)頂層父類(lèi),它是一個(gè)抽象類(lèi),常用的 Buffer 的子類(lèi)有:ByteBuffer、IntBuffer、 CharBuffer、 LongBuffer、 DoubleBuffer、FloatBuffer、ShortBuffer
Selector
Selector 類(lèi)是 NIO 的核心類(lèi),Selector 能夠檢測(cè)多個(gè)注冊(cè)的通道上是否有事件發(fā)生,如果有事件發(fā)生,便獲取事件然后針對(duì)每個(gè)事件進(jìn)行相應(yīng)的響應(yīng)處理。這樣一來(lái),只是用一個(gè)單線程就可以管理多個(gè)通道,也就是管理多個(gè)連接。這樣使得只有在連接真正有讀寫(xiě)事件發(fā)生時(shí),才會(huì)調(diào)用函數(shù)來(lái)進(jìn)行讀寫(xiě),就大大地減少了系統(tǒng)開(kāi)銷(xiāo),并且不必為每個(gè)連接都創(chuàng)建一個(gè)線程,不用去維護(hù)多個(gè)線程,并且避免了多線程之間的上下文切換導(dǎo)致的開(kāi)銷(xiāo)。
JVM 類(lèi)加載機(jī)制
JVM 類(lèi)加載機(jī)制分為五個(gè)部分:加載,驗(yàn)證,準(zhǔn)備,解析,初始化,下面我們就分別來(lái)看一下這五個(gè)過(guò)程。
加載
加載是類(lèi)加載過(guò)程中的一個(gè)階段,這個(gè)階段會(huì)在內(nèi)存中生成一個(gè)代表這個(gè)類(lèi)的 java.lang.Class 對(duì)象,作為方法區(qū)這個(gè)類(lèi)的各種數(shù)據(jù)的入口。注意這里不一定非得要從一個(gè) Class 文件獲取,這里既可以從 ZIP 包中讀取(比如從 jar 包和 war 包中讀取),也可以在運(yùn)行時(shí)計(jì)算生成(動(dòng)態(tài)代理),也可以由其它文件生成(比如將 JSP 文件轉(zhuǎn)換成對(duì)應(yīng)的 Class 類(lèi))。
驗(yàn)證
這一階段的主要目的是為了確保 Class 文件的字節(jié)流中包含的信息是否符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。
準(zhǔn)備
準(zhǔn)備階段是正式為類(lèi)變量分配內(nèi)存并設(shè)置類(lèi)變量的初始值階段,即在方法區(qū)中分配這些變量所使用的內(nèi)存空間。注意這里所說(shuō)的初始值概念,比如一個(gè)類(lèi)變量定義為:
public static int v = 8080;
實(shí)際上變量 v 在準(zhǔn)備階段過(guò)后的初始值為 0 而不是 8080,將 v 賦值為 8080 的 put static 指令是程序被編譯后,存放于類(lèi)構(gòu)造器<client>方法之中。
但是注意如果聲明為:
public static final int v = 8080;
在編譯階段會(huì)為 v 生成 ConstantValue 屬性,在準(zhǔn)備階段虛擬機(jī)會(huì)根據(jù) ConstantValue 屬性將 v賦值為 8080。
解析
解析階段是指虛擬機(jī)將常量池中的符號(hào)引用替換為直接引用的過(guò)程。符號(hào)引用就是 class 文件中的:
1. CONSTANT_Class_info
2. CONSTANT_Field_info
3. CONSTANT_Method_info
等類(lèi)型的常量。
符號(hào)引用
? 符號(hào)引用與虛擬機(jī)實(shí)現(xiàn)的布局無(wú)關(guān),引用的目標(biāo)并不一定要已經(jīng)加載到內(nèi)存中。各種虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局可以各不相同,但是它們能接受的符號(hào)引用必須是一致的,因?yàn)榉?hào)引用的字面量形式明確定義在 Java 虛擬機(jī)規(guī)范的 Class 文件格式中。
直接引用
? 直接引用可以是指向目標(biāo)的指針,相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。如果有了直接引用,那引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在。
初始化
初始化階段是類(lèi)加載最后一個(gè)階段,前面的類(lèi)加載階段之后,除了在加載階段可以自定義類(lèi)加載器以外,其它操作都由 JVM 主導(dǎo)。到了初始階段,才開(kāi)始真正執(zhí)行類(lèi)中定義的 Java 程序代碼。
類(lèi)構(gòu)造器<client>
初始化階段是執(zhí)行類(lèi)構(gòu)造器<client>方法的過(guò)程。<client>方法是由編譯器自動(dòng)收集類(lèi)中的類(lèi)變量的賦值操作和靜態(tài)語(yǔ)句塊中的語(yǔ)句合并而成的。虛擬機(jī)會(huì)保證子<client>方法執(zhí)行之前,父類(lèi)的<client>方法已經(jīng)執(zhí)行完畢,如果一個(gè)類(lèi)中沒(méi)有對(duì)靜態(tài)變量賦值也沒(méi)有靜態(tài)語(yǔ)句塊,那么編譯器可以不為這個(gè)類(lèi)生成<client>()方法。
注意以下幾種情況不會(huì)執(zhí)行類(lèi)初始化:
1. 通過(guò)子類(lèi)引用父類(lèi)的靜態(tài)字段,只會(huì)觸發(fā)父類(lèi)的初始化,而不會(huì)觸發(fā)子類(lèi)的初始化。
2. 定義對(duì)象數(shù)組,不會(huì)觸發(fā)該類(lèi)的初始化。
3. 常量在編譯期間會(huì)存入調(diào)用類(lèi)的常量池中,本質(zhì)上并沒(méi)有直接引用定義常量的類(lèi),不會(huì)觸發(fā)定義常量所在的類(lèi)。
4. 通過(guò)類(lèi)名獲取 Class 對(duì)象,不會(huì)觸發(fā)類(lèi)的初始化。
5. 通過(guò) Class.forName 加載指定類(lèi)時(shí),如果指定參數(shù) initialize 為 false 時(shí),也不會(huì)觸發(fā)類(lèi)初始化,其實(shí)這個(gè)參數(shù)是告訴虛擬機(jī),是否要對(duì)類(lèi)進(jìn)行初始化。
6. 通過(guò) ClassLoader 默認(rèn)的 loadClass 方法,也不會(huì)觸發(fā)初始化動(dòng)作。
類(lèi)加載器
虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)把加載動(dòng)作放到 JVM 外部實(shí)現(xiàn),以便讓?xiě)?yīng)用程序決定如何獲取所需的類(lèi),JVM 提供了 3 種類(lèi)加載器:
啟動(dòng)類(lèi)加載器(Bootstrap ClassLoader)
1. 負(fù)責(zé)加載 JAVA_HOME\lib 目錄中的,或通過(guò)-Xbootclasspath 參數(shù)指定路徑中的,且被虛擬機(jī)認(rèn)可(按文件名識(shí)別,如 rt.jar)的類(lèi)。
擴(kuò)展類(lèi)加載器(Extension ClassLoader)
2. 負(fù)責(zé)加載 JAVA_HOME\lib\ext 目錄中的,或通過(guò) java.ext.dirs 系統(tǒng)變量指定路徑中的類(lèi)庫(kù)。
應(yīng)用程序類(lèi)加載器(Application ClassLoader):
3. 負(fù)責(zé)加載用戶(hù)路徑(classpath)上的類(lèi)庫(kù)。
JVM 通過(guò)雙親委派模型進(jìn)行類(lèi)的加載,當(dāng)然我們也可以通過(guò)繼承 java.lang.ClassLoader實(shí)現(xiàn)自定義的類(lèi)加載器。
雙親委派
當(dāng)一個(gè)類(lèi)收到了類(lèi)加載請(qǐng)求,他首先不會(huì)嘗試自己去加載這個(gè)類(lèi),而是把這個(gè)請(qǐng)求委派給父類(lèi)去完成,每一個(gè)層次類(lèi)加載器都是如此,因此所有的加載請(qǐng)求都應(yīng)該傳送到啟動(dòng)類(lèi)加載其中,只有當(dāng)父類(lèi)加載器反饋?zhàn)约簾o(wú)法完成這個(gè)請(qǐng)求的時(shí)候(在它的加載路徑下沒(méi)有找到所需加載的Class),子類(lèi)加載器才會(huì)嘗試自己去加載。
采用雙親委派的一個(gè)好處是比如加載位于 rt.jar 包中的類(lèi) java.lang.Object,不管是哪個(gè)加載器加載這個(gè)類(lèi),最終都是委托給頂層的啟動(dòng)類(lèi)加載器進(jìn)行加載,這樣就保證了使用不同的類(lèi)加載器最終得到的都是同樣一個(gè) Object 對(duì)象。
OSGI(動(dòng)態(tài)模型系統(tǒng))
OSGi(Open Service Gateway Initiative),是面向 Java 的動(dòng)態(tài)模型系統(tǒng),是 Java 動(dòng)態(tài)化模塊化系統(tǒng)的一系列規(guī)范。
動(dòng)態(tài)改變構(gòu)造
OSGi 服務(wù)平臺(tái)提供在多種網(wǎng)絡(luò)設(shè)備上無(wú)需重啟的動(dòng)態(tài)改變構(gòu)造的功能。為了最小化耦合度和促使這些耦合度可管理,OSGi 技術(shù)提供一種面向服務(wù)的架構(gòu),它能使這些組件動(dòng)態(tài)地發(fā)現(xiàn)對(duì)方。
模塊化編程與熱插拔
OSGi 旨在為實(shí)現(xiàn) Java 程序的模塊化編程提供基礎(chǔ)條件,基于 OSGi 的程序很可能可以實(shí)現(xiàn)模塊級(jí)的熱插拔功能,當(dāng)程序升級(jí)更新時(shí),可以只停用、重新安裝然后啟動(dòng)程序的其中一部分,這對(duì)企業(yè)級(jí)程序開(kāi)發(fā)來(lái)說(shuō)是非常具有誘惑力的特性。
OSGi 描繪了一個(gè)很美好的模塊化開(kāi)發(fā)目標(biāo),而且定義了實(shí)現(xiàn)這個(gè)目標(biāo)的所需要服務(wù)與架構(gòu),同時(shí)也有成熟的框架進(jìn)行實(shí)現(xiàn)支持。但并非所有的應(yīng)用都適合采用 OSGi 作為基礎(chǔ)架構(gòu),它在提供強(qiáng)大功能同時(shí),也引入了額外的復(fù)雜度,因?yàn)樗蛔袷亓祟?lèi)加載的雙親委托模型。
JVM福利贈(zèng)送
最后,再給大家分享一些JVM的技術(shù)文檔吧,,希望大家能夠喜歡~~~
需要的小伙伴,就可以轉(zhuǎn)發(fā)關(guān)注公眾號(hào):程序員高級(jí)碼農(nóng) 獲取即可!!!
感謝大家支持~~~~多多轉(zhuǎn)發(fā)關(guān)注不迷路~~~~~~~~~~
posted on 2020-06-02 16:49 程序員高級(jí)工程師 閱讀(1376) 評(píng)論(0) 收藏 舉報(bào)
浙公網(wǎng)安備 33010602011771號(hào)