最近初讀《深入理解java虛擬機(jī)》對很多點(diǎn)豁然開朗,建議大家如果時(shí)間充裕可以找來看一看,比博客什么的更加深入。

JVM總體架構(gòu):

首先,我們需要理解什么是java虛擬機(jī)(JVM),JVM是英文Java Virtual Machine的縮寫,JVM用于執(zhí)行經(jīng)過編譯的字節(jié)碼文件,java的跨平臺執(zhí)行就是依靠JVM的一致性。
代碼如何在虛擬機(jī)上運(yùn)行:
xx·Java 源文件—->編譯器—->字節(jié)碼文件(xx.class)—->JVM—->機(jī)器碼
? ? ? 每一種平臺的解釋器是不同的,但是實(shí)現(xiàn)的虛擬機(jī)是相同的,這也就是 Java 為什么能夠跨平臺的原因。
? ? 當(dāng)一個(gè)程序從開始運(yùn)行,這時(shí)虛擬機(jī)就開始實(shí)例化了,所以多個(gè)程序啟動(dòng)就會(huì)存在多個(gè)虛擬機(jī)實(shí)例。程序退出或者關(guān)閉,則虛擬機(jī)實(shí)例消亡,虛擬機(jī)實(shí)例之間數(shù)據(jù)不能共享。
Hotspot JVM(java虛擬機(jī)的一種,Sun JDK和OpenJDK中所帶的虛擬機(jī),也是目前使用范圍最廣的Java虛擬機(jī)) 后臺運(yùn)行的系統(tǒng)線程主要有下面幾個(gè):

一.JVM內(nèi)存區(qū)域:
內(nèi)存區(qū)域結(jié)構(gòu)思維導(dǎo)圖如下:

線程私有:依賴用戶線程的啟動(dòng)/結(jié)束而創(chuàng)建/銷毀(在 HotspotVM 內(nèi), 每個(gè)線程都與操作系統(tǒng)的本地線程直接映射, 因此這部分內(nèi)存區(qū)域的存/否跟隨本地線程的生/死對應(yīng))。
線程共享區(qū)域:隨虛擬機(jī)的啟動(dòng)/關(guān)閉而創(chuàng)建/銷毀。
內(nèi)存結(jié)構(gòu)如下:

1.程序計(jì)數(shù)器(線程私有)
? ? ? 一塊較小的內(nèi)存空間, 是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器,每條線程都要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,這類內(nèi)存也稱為“線程私有”的內(nèi)存。
內(nèi)存區(qū)域是唯一一個(gè)在虛擬機(jī)中沒有規(guī)定任何OutOfMemoryError 情況的區(qū)域。
2.虛擬機(jī)棧(線程私有)
描述java方法執(zhí)行的內(nèi)存模型,每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。每一個(gè)方法從調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中入棧到出棧的過程。
3.本地方法區(qū)(線程私有)
? ? ? 本地方法區(qū)和 Java Stack 作用類似, 區(qū)別是虛擬機(jī)棧為執(zhí)行 Java 方法服務(wù), 而本地方法棧則為Native方法服務(wù)。
4.堆(Heap-線程共享)-運(yùn)行時(shí)數(shù)據(jù)區(qū)
? ? ? 被線程共享的一塊內(nèi)存區(qū)域,創(chuàng)建的對象和數(shù)組都保存在 Java 堆內(nèi)存中,也是垃圾收集器進(jìn)行垃圾收集的最重要的內(nèi)存區(qū)域。
被線程共享的一塊內(nèi)存區(qū)域,創(chuàng)建的對象和數(shù)組都保存在 Java 堆內(nèi)存中,也是垃圾收集器進(jìn)行垃圾收集的最重要的內(nèi)存區(qū)域。

java堆
? ? Java 堆從GC(Garba Collection)的角度還可以細(xì)分為: 新生代(Eden區(qū)、From Survivor區(qū)和To Survivor區(qū))和老年代。
Ⅰ.新生代
a.Eden區(qū) Java新對象的出生地(如果新創(chuàng)建的對象占用內(nèi)存很大,則直接分配到老年代)。當(dāng)Eden區(qū)內(nèi)存不夠的時(shí)候就會(huì)觸發(fā)MinorGC,對新生代區(qū)進(jìn)行一次垃圾回收。
b.ServivorFrom 上一次GC的幸存者,作為這一次GC的被掃描者。
c.ervivorTo 保留了一次 MinorGC過程中的幸存者。
d.MinorGC(復(fù)制算法---一種垃圾收集算法)? ?
? ? ? ? 1: 首先,把Eden和ServivorFrom區(qū)域中存活的對象復(fù)制到ServicorTo區(qū)域(如果有對象的年齡以及達(dá)到了老年的標(biāo)準(zhǔn),則賦值到老年代區(qū)),同時(shí)把這些對象的年齡+1,年齡達(dá)到閾值(默認(rèn)為15)就移動(dòng)到老年代中。(如果 ServicorTo空間不足就放到老年區(qū));
? ? ? ? 2:清空eden、servicorFrom;
? ? ? ? 3:最后,ServicorTo和ServicorFrom互換,原ServicorTo成為下一次GC時(shí)的ServicorFrom。(ServicorTo與ServicorFrom循環(huán)重復(fù)使用)。
Ⅱ.老年代
? ? 主要存放應(yīng)用程序中生命周期長的內(nèi)存對象。
? ? 老年代的對象比較穩(wěn)定,所以 MajorGC(一種垃圾收集算法) 不會(huì)頻繁執(zhí)行。在進(jìn)行 MajorGC 前一般都先進(jìn)行了一次 MinorGC,使得有新生代的對象晉身入老年代,導(dǎo)致空間不夠用時(shí)才觸發(fā)。當(dāng)無法找到足夠大的連續(xù)空間分配給新創(chuàng)建的較大對象時(shí)也會(huì)提前觸發(fā)一次MajorGC 進(jìn)行垃圾回收騰出空間。
? ? MajorGC 采用標(biāo)記清除算法:首先掃描一次所有老年代,標(biāo)記出存活的對象,然后回收沒有標(biāo)記的對象。MajorGC 的耗時(shí)比較長,因?yàn)橐獟呙柙倩厥?。MajorGC 會(huì)產(chǎn)生內(nèi)存碎片,為了減少內(nèi)存損耗,我們一般需要進(jìn)行合并或者標(biāo)記出來方便下次直接分配。當(dāng)老年代也滿了裝不下的時(shí)候,就會(huì)拋出OOM(Out of Memory)異常。
5.方法區(qū)/永久代(線程共享)
常說的永久代(Permanent Generation), 用于存儲(chǔ)被 JVM 加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
? ? ? HotSpot VM把GC分代收集擴(kuò)展至方法區(qū), 即使用Java 堆的永久代來實(shí)現(xiàn)方法區(qū), 這樣HotSpot 的垃圾收集器就可以像管理 Java 堆一樣管理這部分內(nèi)存, 而不必為方法區(qū)開發(fā)專門的內(nèi)存管理器(永久帶的內(nèi)存回收的主要目標(biāo)是針對常量池的回收和類型的卸載, 因此收益一般很小)。
? ? 在Java8中,永久代已經(jīng)被移除,被一個(gè)稱為“元數(shù)據(jù)區(qū)”(元空間)的區(qū)域所取代。元空間的本質(zhì)和永久代類似,元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。因此,默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制。類的元數(shù)據(jù)放入native memory, 字符串池和類的靜態(tài)變量放入 java 堆中,這樣可以加載多少類的元數(shù)據(jù)就不再由 MaxPermSize 控制, 而由系統(tǒng)的實(shí)際可用空間來控制。
二.垃圾回收與算法詳解
思維導(dǎo)圖如下

? ? ? 如果你要去回收你會(huì)怎么做?我猜你肯定會(huì)說,直接丟掉呀!那么你知道哪些是垃圾,哪些是正常數(shù)據(jù)么?
1.垃圾確定
a.引用計(jì)數(shù)法
? ? 引用和對象是有關(guān)聯(lián)的。如果要操作對象則必須用引用進(jìn)行。很顯然一個(gè)簡單的辦法是通過引用計(jì)數(shù)來判斷一個(gè)對象是否可以回收。簡單說,即一個(gè)對象如果沒有任何與之關(guān) 聯(lián)的引用,即他們的引用計(jì)數(shù)都不為 0,則說明對象不太可能再被用到,那么這個(gè)對象就是可回收對象。
? ? 兩個(gè)對象出現(xiàn)循環(huán)引用的情況下,此時(shí)引用計(jì)數(shù)器永遠(yuǎn)不為 0,導(dǎo)致無法對它們進(jìn)行回收。正是因?yàn)檠h(huán)引用的存在,因此 Java 虛擬機(jī)不使用引用計(jì)數(shù)算法,如下代碼所示a、b。
? public static void main(String[] args) {
? ? ? ? Test a = new Test();//對象a
? ? ? ? Test b = new Test();//對象b
? ? ? ? a.instance = b;? ? //a引用b
? ? ? ? b.instance = a;? ? //b引用a
? ? ? ? a = null;
? ? ? ? b = null;
? ? ? ? doSomething();
? ? }
b.可達(dá)性分析
以 GC Roots 為起始點(diǎn)進(jìn)行搜索,可達(dá)的對象都是存活的,不可達(dá)的對象可被回收??蛇_(dá)的含義是程序運(yùn)行過程中肯定能夠走到該對象,稱為可達(dá)的,而那種死活都不會(huì)執(zhí)行的對象就是不可達(dá)的。
Java 虛擬機(jī)使用該算法來判斷對象是否可被回收,GC Roots 一般包含以下內(nèi)容:
? 1.虛擬機(jī)棧中局部變量表中引用的對象
? ? 2.本地方法棧中 JNI 中引用的對象
? ? 3.方法區(qū)中類靜態(tài)屬性引用的對象
? ? 4.方法區(qū)中的常量引用的對象
備注:感興趣可以看看四種強(qiáng)度的引用類型,萬一呢?(狗頭)
2.垃圾回收算法
這一塊是確定哪些可以扔之后,確定怎么扔,畢竟電腦不是現(xiàn)實(shí)生活,路過垃圾桶就扔了。
(1)標(biāo)記清除算法(Mark-Sweep)
? ? ? ? 最基礎(chǔ)的垃圾回收算法,分為兩個(gè)階段,標(biāo)注和清除。標(biāo)記階段標(biāo)記出所有需要回收的對象,清除階段回收被標(biāo)記的對象所占用的空間。(哪兒是垃圾就刪哪兒)如圖:

缺點(diǎn):
標(biāo)記和清除過程效率都不高;
會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,導(dǎo)致無法給大對象分配內(nèi)存。
(2)標(biāo)記 - 整理
讓所有存活的對象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存。

優(yōu)點(diǎn): 不會(huì)產(chǎn)生內(nèi)存碎片
缺點(diǎn): 需要移動(dòng)大量對象,處理效率比較低。
(3)復(fù)制算法(copying)
? ? ? ? 為了解決Mark-Sweep 算法內(nèi)存碎片化的缺陷而被提出的算法。按內(nèi)存容量將內(nèi)存劃分為等大小 的兩塊。每次只使用其中一塊,當(dāng)這一塊內(nèi)存滿后將尚存活的對象復(fù)制到另一塊上去,把已使用的內(nèi)存清掉,如圖

主要不足是只使用了內(nèi)存的一半。商業(yè)虛擬機(jī)都采用這種收集算法回收新生代,但是并不是劃分為大小相等的兩塊,而是一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。在回收時(shí),將 Eden 和 Survivor 中還存活著的對象全部復(fù)制到另一塊 Survivor 上,最后清理 Eden 和使用過的那一塊 Survivor。
(4)分代收集算法
現(xiàn)在的商業(yè)虛擬機(jī)采用分代收集算法,它根據(jù)對象存活周期將內(nèi)存劃分為幾塊,不同塊采用適當(dāng)?shù)氖占惴ā?/p>
一般將堆分為新生代和老年代。
新生代使用:復(fù)制算法
老年代使用:標(biāo)記-清除或標(biāo)記-整理算法
3.垃圾收集器
以下是 HotSpot 虛擬機(jī)中的 7 個(gè)垃圾收集器,連線表示垃圾收集器可以配合使用。

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

Serial 翻譯為串行,也就是說它以串行的方式執(zhí)行。
它是單線程的收集器,只會(huì)使用一個(gè)線程進(jìn)行垃圾收集工作。
有點(diǎn):簡單高效,在單個(gè) CPU 環(huán)境下,由于沒有線程交互的開銷,因此擁有最高的單線程收集效率。
(2)ParNew收集器

Serial 收集器的多線程版本。
它是 Server 場景下默認(rèn)的新生代收集器,除了性能原因外,主要是因?yàn)槌?Serial 收集器,只有它能與 CMS 收集器配合使用。
(3)Parallel Scavenge 收集器
? ? 與 ParNew 一樣是多線程收集器。
? ? 其它收集器目標(biāo)是盡可能縮短垃圾收集時(shí)用戶線程的停頓時(shí)間,而它的目標(biāo)是達(dá)到一個(gè)可控制的吞吐量,因此它被稱為“吞吐量優(yōu)先”收集器。這里的吞吐量指 CPU 用于運(yùn)行用戶程序的時(shí)間占總時(shí)間的比值。
? ? 縮短停頓時(shí)間是以犧牲吞吐量和新生代空間來換取的:新生代空間變小,垃圾回收變得頻繁,導(dǎo)致吞吐量下降。
(4)Serial Old 收集器
是 Serial 收集器的老年代版本,也是給 Client 場景下的虛擬機(jī)使用。如果用在 Server 場景下,它有兩大用途:
在 JDK 1.5 以及之前版本(Parallel Old 誕生以前)中與 Parallel Scavenge 收集器搭配使用。
作為 CMS 收集器的后備預(yù)案,在并發(fā)收集發(fā)生 Concurrent Mode Failure 時(shí)使用。
(5)Parallel Old 收集器

是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 資源敏感的場合,都可以優(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)到的對象,速度很快,需要停頓。
并發(fā)標(biāo)記:進(jìn)行 GC Roots Tracing 的過程,它在整個(gè)回收過程中耗時(shí)最長,不需要停頓。
重新標(biāo)記:為了修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對象的標(biāo)記記錄,需要停頓。
并發(fā)清除:不需要停頓。
? ? ? 在整個(gè)過程中耗時(shí)最長的并發(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)前對象,不得不提前觸發(fā)一次 Full GC。
(7)G1 收集器
? ? ? G1(Garbage-First),它是一款面向服務(wù)端應(yīng)用的垃圾收集器,在多 CPU 和大內(nèi)存的場景下有很好的性能。HotSpot 開發(fā)團(tuán)隊(duì)賦予它的使命是未來可以替換掉 CMS 收集器。
? ? ? 堆被分為新生代和老年代,其它收集器進(jìn)行收集的范圍都是整個(gè)新生代或者老年代,而 G1 可以直接對新生代和老年代一起回收。
G1 把堆劃分成多個(gè)大小相等的獨(dú)立區(qū)域(Region),新生代和老年代不再物理隔離。

? ? ? 通過引入 Region 的概念,從而將原來的一整塊內(nèi)存空間劃分成多個(gè)的小空間,使得每個(gè)小空間可以單獨(dú)進(jìn)行垃圾回收。
? ? ? 通過記錄每個(gè) Region 垃圾回收時(shí)間以及回收所獲得的空間(這兩個(gè)值是通過過去回收的經(jīng)驗(yàn)獲得),并維護(hù)一個(gè)優(yōu)先列表,每次根據(jù)允許的收集時(shí)間,優(yōu)先回收價(jià)值最大的 Region。
? ? ? ? 每個(gè) Region 都有一個(gè) Remembered Set,用來記錄該 Region 對象的引用對象所在的 Region。通過使用 Remembered Set,在做可達(dá)性分析的時(shí)候就可以避免全堆掃描。

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í)間對象變化記錄在線程的 Remembered Set Logs 里面,最終標(biāo)記階段需要把 Remembered Set Logs 的數(shù)據(jù)合并到 Remembered Set 中。這階段需要停頓線程,但是可并行執(zhí)行。
篩選回收:首先對各個(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ù)測的停頓:能讓使用者明確指定在一個(gè)長度為 M 毫秒的時(shí)間片段內(nèi),消耗在 GC 上的時(shí)間不得超過 N 毫秒。
JVM內(nèi)存區(qū)域面試基礎(chǔ)知識點(diǎn)大概就這么多,細(xì)細(xì)理解一遍并不難的哈。
如果對您有所幫助就點(diǎn)贊吧!最近找工作,歡迎大家一起交流。還有,收藏≠學(xué)會(huì)哈!
(文中部分圖片源自書本、某大佬博客,侵刪)