JVM內(nèi)存結(jié)構(gòu)、運(yùn)行時(shí)內(nèi)存以及類加載過程

以下內(nèi)容都是基于jdk1.8

1、JVM 內(nèi)存管理

image.png

2、JVM內(nèi)存區(qū)域

image.png

JVM內(nèi)存區(qū)域主要分為線程私有Thread Local區(qū)域(程序計(jì)數(shù)器,虛擬機(jī)棧,本地方法區(qū))、線程共享Thread Shared區(qū)域(java heap堆、方法區(qū))、直接內(nèi)存Direct Memory。

  • 線程私有Thread Local數(shù)據(jù)區(qū)域:生命周期與線程相同,依賴用戶線程的啟動(dòng)/結(jié)束 而 創(chuàng)建/銷毀。

  • 線程共享Thread shared區(qū)域:隨虛擬機(jī)的啟動(dòng)/關(guān)閉 而 創(chuàng)建/銷毀。

  • 直接內(nèi)存Direct Memory:并不是JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分。
    但也會(huì)被頻繁的使用:在 JDK 1.4 引入的 NIO 提供了基于 Channel 與 Buffer 的 IO 方式, 它可以使用 Native 函數(shù)庫直接分配堆外內(nèi)存, 然后使用DirectByteBuffer 對象作為這塊內(nèi)存的引用進(jìn)行操作, 這樣就避免了在 Java堆和 Native 堆中來回復(fù)制數(shù)據(jù), 因此在一些場景中可以顯著提高性能。

image.png
image.png

1、程序計(jì)數(shù)器(線程私有Thread Local)

一塊較小的內(nèi)存空間, 是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器,每個(gè)線程都要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,這類內(nèi)存也稱為“線程私有”的內(nèi)存。
正在執(zhí)行 java 方法的話,計(jì)數(shù)器記錄的是虛擬機(jī)字節(jié)碼指令的地址(當(dāng)前指令的地址)。如果還是 Native 方法,則為空。
這個(gè)內(nèi)存區(qū)域是唯一一個(gè)在虛擬機(jī)中沒有規(guī)定任何 OutOfMemoryError 情況的區(qū)域。

2、虛擬機(jī)棧(線程私有Thread Local)

描述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ī)棧中入棧到出棧的過程。
棧幀( Frame)是用來存儲(chǔ)數(shù)據(jù)和部分過程結(jié)果的數(shù)據(jù)結(jié)構(gòu),同時(shí)也被用來處理動(dòng)態(tài)鏈接(Dynamic Linking)、 方法返回值和異常分派( Dispatch Exception)。棧幀隨著方法調(diào)用而創(chuàng)建,隨著方法結(jié)束而銷毀——無論方法是正常完成還是異常完成(拋出了在方法內(nèi)未被捕獲的異常)都算作方法結(jié)束。

棧用來存儲(chǔ)線程的局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。如果請求棧的深度不足時(shí)拋出的錯(cuò)誤會(huì)包含類似下面的信息:
java.lang.StackOverflowError

另外,由于每個(gè)線程占的內(nèi)存大概為1M,因此線程的創(chuàng)建也需要內(nèi)存空間。操作系統(tǒng)可用內(nèi)存-Xmx-MaxPermSize即是??捎玫膬?nèi)存,如果申請創(chuàng)建的線程比較多超過剩余內(nèi)存的時(shí)候,也會(huì)拋出如下類似錯(cuò)誤:
java.lang.OutofMemoryError: unable to create new native thread

相關(guān)的JVM參數(shù)有:
-Xss: 每個(gè)線程的堆棧大小,JDK5.0以后每個(gè)線程堆棧大小為1M,以前每個(gè)線程堆棧大小為256K.
在相同物理內(nèi)存下,減小這個(gè)值能生成更多的線程.但是操作系統(tǒng)對一個(gè)進(jìn)程內(nèi)的線程數(shù)還是有限制的,不能無限生成,經(jīng)驗(yàn)值在3000~5000左右。

3、本地方法棧(線程私有Thread Local)

本地方法區(qū)和 Java Stack 作用類似,區(qū)別是虛擬機(jī)棧為執(zhí)行 Java 方法服務(wù), 而本地方法棧則為Native 方法服務(wù),
如果一個(gè) VM 實(shí)現(xiàn)使用 C-linkage 模型來支持 Native 調(diào)用, 那么該棧將會(huì)是一個(gè)C 棧,但 HotSpot VM 直接就把本地方法棧和虛擬機(jī)棧合二為一。

4、堆heap(線程共享Thread shared)——運(yùn)行時(shí)數(shù)據(jù)區(qū)

Heap堆是被線程共享的一塊內(nèi)存區(qū)域,創(chuàng)建的對象和數(shù)組都保存在Java堆內(nèi)存中,也是垃圾收集器進(jìn)行垃圾收集的最重要的內(nèi)存區(qū)域。由于現(xiàn)代VM采用分代收集算法,因此Java堆從GC的角度還可以細(xì)分為:新生代(Eden取、From Survivor區(qū)和To Survivor區(qū))和老年代。

Java堆heap內(nèi)存主要用來存放運(yùn)行過程中生成的對象,該區(qū)域OOM異常一般會(huì)有如下錯(cuò)誤信息:
java.lang.OutofMemoryError:Java heap space;
通過設(shè)置-XX:-HeapDumpOnOutOfMemoryError,可以使其在OOM時(shí),輸出一個(gè)dump.core文件,記錄當(dāng)時(shí)的堆內(nèi)存快照。
然后我們就可以通過分析這個(gè)dump的內(nèi)存快照來找到問題原因,比如說,是由于程序原因?qū)е碌膬?nèi)存泄露,還是由于沒有估計(jì)好JVM內(nèi)存的大小而導(dǎo)致的內(nèi)存溢出。

Java堆常用的JVM常數(shù):

  • -Xms:初始堆大小,默認(rèn)值為物理內(nèi)存的1/64(<1GB),默認(rèn)(MinHeapFreeRatio參數(shù)可以調(diào)整)空余堆內(nèi)存小于40%時(shí),JVM就會(huì)增大堆直到-Xmx的最大限制.
  • -Xmx:最大堆大小,默認(rèn)值為物理內(nèi)存的1/4(<1GB),默認(rèn)(MaxHeapFreeRatio參數(shù)可以調(diào)整)空余堆內(nèi)存大于70%時(shí),JVM會(huì)減少堆直到 -Xms的最小限制
  • -Xmn:年輕代大小(1.4or lator),此處的大小是(eden + 2 survivor space),與jmap -heap中顯示的New gen是不同的。

5、方法區(qū)(線程共享Thread shared)

即我們常說的永久代(Permanent Generation),用于存儲(chǔ)被 JVM 加載的類信息、常量(final)、靜態(tài)變量(static)、即時(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)是針對常量池的回收和類型的卸載, 因此收益一般很小)。

運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項(xiàng)信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中。
Java 虛擬機(jī)對 Class 文件的每一部分(自然也包括常量池)的格式都有嚴(yán)格的規(guī)定,每一個(gè)字節(jié)用于存儲(chǔ)哪種數(shù)據(jù)都必須符合規(guī)范上的要求,這樣才會(huì)被虛擬機(jī)認(rèn)可、裝載和執(zhí)行。

方法區(qū)主要存儲(chǔ)被虛擬機(jī)加載的類信息,如類名、訪問修飾符、常量池、字段描述、方法描述等。理論上在JVM啟動(dòng)后該區(qū)域大小應(yīng)該比較穩(wěn)定,但是目前很多框架,比如Spring和Hibernate等在運(yùn)行過程中都會(huì)動(dòng)態(tài)生成類,因此也存在OOM的風(fēng)險(xiǎn)。
如果該區(qū)域OOM,錯(cuò)誤結(jié)果會(huì)包含類似下面的信息:
java.lang.OutofMemoryError: PermGen space

相關(guān)的JVM參數(shù)可以參考運(yùn)行時(shí)常量。

  • -XX:PermSize:設(shè)置永久代(perm gen)初始值,默認(rèn)值為物理內(nèi)存的1/64
  • -XX:MaxPermSize:設(shè)置永久代最大值,默認(rèn)為物理內(nèi)存的1/4

上面說的是jdk1.8之前的內(nèi)存模型,其中方法區(qū)和堆是是線程共享的,但是在jdk1.8之后元數(shù)據(jù)區(qū)取代了永久代。元空間的本質(zhì)和永久代類似,都是對JVM規(guī)范中方法區(qū)的實(shí)現(xiàn)。不過元空間與永久代之間最大的區(qū)別在于:元數(shù)據(jù)空間并不在虛擬機(jī)中,而是使用本地內(nèi)存,同時(shí)類的元數(shù)據(jù)放入 native memory,運(yùn)行時(shí)常量池和類的靜態(tài)變量放入 java 堆中,這樣可以加載多少類的元數(shù)據(jù)就不再由MaxPermSize 控制,而由系統(tǒng)的實(shí)際可用空間來控制。

image.png

注:數(shù)組和對象是保存在堆中,類信息(類中字段、變量)、常量(final)、靜態(tài)變量(static)是保存在方法區(qū)(永久代);1.8以后,類信息保存在元空間,常量(final)、靜態(tài)變量(static)保存在堆中

3、JVM運(yùn)行時(shí)內(nèi)存——堆heap

Java 堆從 GC 的角度還可以細(xì)分為: 新生代(Eden 區(qū)、From Survivor 區(qū)和 To Survivor 區(qū))和老年代。

image.png

新生代

是用來存放新生的對象。一般占據(jù)堆的 1/3 空間。由于頻繁創(chuàng)建對象,所以新生代會(huì)頻繁觸發(fā)MinorGC 進(jìn)行垃圾回收。新生代又分為 Eden 區(qū)、ServivorFrom、ServivorTo 三個(gè)區(qū)。

  • Eden 區(qū)
    Java 新對象的出生地(如果新創(chuàng)建的對象占用內(nèi)存很大,則直接分配到老
    年代)。當(dāng) Eden 區(qū)內(nèi)存不夠的時(shí)候就會(huì)觸發(fā) MinorGC,對新生代區(qū)進(jìn)行
    一次垃圾回收。

  • ServivorFrom
    上一次 GC 的幸存者,作為這一次 GC 的被掃描者

  • ServivorTo
    保留了一次 MinorGC 過程中的幸存者。

MinorGC(也可以稱之為young gc) 的過程(復(fù)制->清空->互換)——MinorGC 采用復(fù)制算法。

  • 1:eden、servicorFrom 復(fù)制到 ServicorTo,年齡+1
    首先,把 Eden 和 ServivorFrom 區(qū)域中存活的對象復(fù)制到 ServicorTo 區(qū)域(如果有對象的年齡以及達(dá)到了老年的標(biāo)準(zhǔn),則復(fù)制到老年代區(qū)),同時(shí)把這些對象的年齡+1(如果 ServicorTo 無法足夠存儲(chǔ)某個(gè)對象,則將這個(gè)對象存儲(chǔ)到老年代),默認(rèn)情況下年齡到達(dá)15的對象會(huì)被移到老生代中;

  • 2:清空 eden、servicorFrom
    然后,清空 Eden 和 ServicorFrom 中的對象;

  • 3:ServicorTo 和 ServicorFrom 互換
    最后,ServicorTo 和 ServicorFrom 互換,原 ServicorTo 成為下一次 GC 時(shí)的 ServicorFrom區(qū)。

優(yōu)點(diǎn):實(shí)現(xiàn)簡單,內(nèi)存效率高,不易產(chǎn)生碎片,每次垃圾收集都能發(fā)現(xiàn)大批對象已死, 只有少量存活. 因此選用復(fù)制算法, 只需要付出少量存活對象的復(fù)制成本就可以完成收集

缺點(diǎn):可用內(nèi)存被壓縮了,且存活對象增多的話,Copying 算法的效率會(huì)大大降低

image.png

老年代

主要存放應(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)記清除算法(Mark-Sweep):首先掃描一次所有老年代,標(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)異常。
image.png

缺點(diǎn):該算法最大的問題是內(nèi)存碎片化嚴(yán)重,后續(xù)可能發(fā)生大對象不能找到可利用空間的問題。

標(biāo)記整理(Mark-Compact)算法:標(biāo)記階段和 Mark-Sweep 算法相同,但是標(biāo)記后不是清理對象,而是將存活對象移向內(nèi)存的一端。然后清除端邊界外的對象
image.png

永久代

即上面說的線程共享Thread shared的方法區(qū)
指內(nèi)存的永久保存區(qū)域,主要存放 Class 和 Meta(元數(shù)據(jù))的信息,Class 在被加載的時(shí)候被放入永久區(qū)域,它和和存放實(shí)例的區(qū)域不同(數(shù)組和對象是保存在堆中),GC 不會(huì)在主程序運(yùn)行期對永久區(qū)域進(jìn)行清理。所以這也導(dǎo)致了永久代的區(qū)域會(huì)隨著加載的 Class 的增多而脹滿,最終拋出 OOM 異常。

在 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í)際可用空間來控制。

4、Minor GC VS Major GC vs Full GC

Minor GC:清理年輕代的垃圾;
Major GC:清理老年代的垃圾;
Full GC:清理整個(gè)堆空間—包括年輕代和老年代;

5、垃圾回收器

CMS收集器(多線程標(biāo)記清除算法)

Concurrent mark sweep(CMS)收集器是一種老年代垃圾收集器,其最主要目標(biāo)是獲取最短垃圾回收停頓時(shí)間,和其他年老代使用標(biāo)記-整理算法不同,它使用多線程的標(biāo)記-清除算法。
最短的垃圾收集停頓時(shí)間可以為交互比較高的程序提高用戶體驗(yàn)。

CMS 工作機(jī)制相比其他的垃圾收集器來說更復(fù)雜,整個(gè)過程分為以下 4 個(gè)階段:

    1. 初始標(biāo)記
      只是標(biāo)記一下 GC Roots 能直接關(guān)聯(lián)的對象,速度很快,仍然需要暫停所有的工作線程。
    1. 并發(fā)標(biāo)記
      進(jìn)行 GC Roots 跟蹤的過程,和用戶線程一起工作,不需要暫停工作線程。
    1. 重新標(biāo)記
      為了修正在并發(fā)標(biāo)記期間,因用戶程序繼續(xù)運(yùn)行而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對象的標(biāo)記記錄,仍然需要暫停所有的工作線程。
    1. 并發(fā)清除
      清除 GC Roots 不可達(dá)對象,和用戶線程一起工作,不需要暫停工作線程。由于耗時(shí)最長的并發(fā)標(biāo)記和并發(fā)清除過程中,垃圾收集線程可以和用戶現(xiàn)在一起并發(fā)工作,所以總體上來看CMS 收集器的內(nèi)存回收和用戶線程是一起并發(fā)地執(zhí)行。
image.png

CMS 出現(xiàn)FullGC的原因:
1、年輕代晉升到老年代沒有足夠的連續(xù)空間,很有可能是內(nèi)存碎片導(dǎo)致的,因此會(huì)觸發(fā)FULL GC

2、在并發(fā)過程中JVM覺得在并發(fā)過程結(jié)束之前堆就會(huì)滿,需要提前觸發(fā)FullGC

G1收集器

Garbage first 垃圾收集器是目前垃圾收集器理論發(fā)展的最前沿成果,是一款面向服務(wù)端應(yīng)用的垃圾收集器,目標(biāo)是替換掉CMS收集器

相比與 CMS 收集器,G1 收集器兩個(gè)最突出的改進(jìn)是:

  1. 基于標(biāo)記-整理算法,不產(chǎn)生內(nèi)存碎片。
  2. 可以非常精確控制停頓時(shí)間,在不犧牲吞吐量前提下,實(shí)現(xiàn)低停頓垃圾回收。

與其它收集器相比,G1變化較大的是它將整個(gè)Java堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域(Region),雖然還保留了新生代和來年代的概念,但新生代和老年代不再是物理隔離的了它們都是一部分Region(不需要連續(xù))的集合。
G1 收集器避免全區(qū)域垃圾收集,它把堆內(nèi)存劃分為大小固定的幾個(gè)獨(dú)立區(qū)域,并且跟蹤這些區(qū)域的垃圾收集進(jìn)度,同時(shí)在后臺(tái)維護(hù)一個(gè)優(yōu)先級列表,每次根據(jù)所允許的收集時(shí)間,優(yōu)先回收垃圾最多的區(qū)域。區(qū)域劃分和優(yōu)先級區(qū)域回收機(jī)制,確保 G1 收集器可以在有限時(shí)間獲得最高的垃圾收集效率

G1收集器的運(yùn)作大致可劃分為以下幾個(gè)步驟:

  • 1、初始標(biāo)記(Initial Making)
  • 2、并發(fā)標(biāo)記(Concurrent Marking)
    3、最終標(biāo)記(Final Marking)
    4、篩選回收(Live Data Counting and Evacuation)

看上去跟CMS收集器的運(yùn)作過程有幾分相似,不過確實(shí)也這樣。初始階段僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對象,并且修改TAMS(Next Top Mark Start)的值,讓下一階段用戶程序并發(fā)運(yùn)行時(shí),能在正確可以用的Region中創(chuàng)建新對象,這個(gè)階段需要停頓線程,但耗時(shí)很短。并發(fā)標(biāo)記階段是從GC Roots開始對堆中對象進(jìn)行可達(dá)性分析,找出存活對象,這一階段耗時(shí)較長但能與用戶線程并發(fā)運(yùn)行。而最終標(biāo)記階段需要吧Remembered Set Logs的數(shù)據(jù)合并到Remembered Set中,這階段需要停頓線程,但可并行執(zhí)行。最后篩選回收階段首先對各個(gè)Region的回收價(jià)值和成本進(jìn)行排序,根據(jù)用戶所期望的GC停頓時(shí)間來制定回收計(jì)劃,這一過程同樣是需要停頓線程的,但Sun公司透露這個(gè)階段其實(shí)也可以做到并發(fā),但考慮到停頓線程將大幅度提高收集效率,所以選擇停頓。

image.png

6、類加載過程

在代碼編譯后,就會(huì)生成JVM(Java虛擬機(jī))能夠識(shí)別的二進(jìn)制字節(jié)流文件(*.class)。而JVM把Class文件中的類描述數(shù)據(jù)從文件加載到內(nèi)存,并對數(shù)據(jù)進(jìn)行校驗(yàn)、準(zhǔn)備、解析、初始化,使這些數(shù)據(jù)最終成為可以被JVM直接使用的Java類型,這個(gè)說來簡單但實(shí)際復(fù)雜的過程叫做JVM的類加載機(jī)制。

類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的生命周期包括7個(gè)階段,加載(Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸載(Unloading),其中驗(yàn)證其中驗(yàn)證、準(zhǔn)備、解析三個(gè)階段統(tǒng)稱為連接。

image.png

注:加載、驗(yàn)證、準(zhǔn)備、初始化、卸載這五個(gè)階段順序是一定的,而解析階段在某些情況下可以在初始化之后再開始。

6.1 加載階段:

在這個(gè)階段,JVM主要完成三件事:

  • 1、通過一個(gè)類的全限定名(包名與類名)來獲取定義此類的二進(jìn)制字節(jié)流(Class文件)。而獲取的方式,可以通過jar包、war包、網(wǎng)絡(luò)中獲取、JSP文件生成等方式。

  • 2、將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。這里只是轉(zhuǎn)化了數(shù)據(jù)結(jié)構(gòu),并未合并數(shù)據(jù)。(方法區(qū)就是用來存放已被加載的類信息,常量,靜態(tài)變量,編譯后的代碼的運(yùn)行時(shí)內(nèi)存區(qū)域)

  • 3、在內(nèi)存中生成一個(gè)代表這個(gè)類的java.lang.Class對象,作用來訪問方法區(qū)這些數(shù)據(jù)。這個(gè)Class對象并沒有規(guī)定是在Java堆內(nèi)存中,它比較特殊,雖為對象,但存放在方法區(qū)中。

對于數(shù)組而言,加載情況有所不同,數(shù)組類本身不通過類加載器創(chuàng)建,是由 JVM 直接創(chuàng)建的。但是數(shù)組中的元素還是要靠類加載器去創(chuàng)建,如果數(shù)組去掉一個(gè)維度后是引用類型,就采用類加載器去加載,否則就交給啟動(dòng)類加載器去加載。

另外加載階段與連接階段的部分內(nèi)容(如一部分字節(jié)碼文件格式驗(yàn)證動(dòng)作)是交叉進(jìn)行的,加載階段尚未完成,連接階段可能已經(jīng)開始。

6.2 連接階段:

類的加載過程后生成了類的java.lang.Class對象,接著會(huì)進(jìn)入連接階段,連接階段負(fù)責(zé)將類的二進(jìn)制數(shù)據(jù)合并入JRE(Java運(yùn)行時(shí)環(huán)境)中。類的連接大致分三個(gè)階段。

  • 第一步,驗(yàn)證:驗(yàn)證被加載后的類是否有正確的結(jié)構(gòu),類數(shù)據(jù)是否會(huì)符合虛擬機(jī)的要求,確保不會(huì)危害虛擬機(jī)安全。

  • 第二步,準(zhǔn)備:是為類的靜態(tài)變量(常量除外)在方法區(qū)分配內(nèi)存并設(shè)置默認(rèn)值(如static int a=123 此時(shí)a值為0,在初始化階段才會(huì)變成123),這些內(nèi)存都將在方法區(qū)中進(jìn)行分配。
    這一階段不分配類中的實(shí)例變量的內(nèi)存,實(shí)例變量將會(huì)在對象實(shí)例化時(shí)隨著對象一起分配在 Java 堆中。
    另外,靜態(tài)常量(static final filed)會(huì)在準(zhǔn)備階段賦程序設(shè)定的初值,如static final int a = 666; 靜態(tài)常量a就會(huì)在準(zhǔn)備階段被直接賦值為666,對于靜態(tài)變量,這個(gè)操作是在初始化階段進(jìn)行的。

  • 第三步,將類的二進(jìn)制數(shù)據(jù)中的符號(hào)引用換為直接引用。
6.3 初始化階段:

類初始化是類加載的最后一步,除了加載階段,用戶可以通過自定義的類加載器參與,其他階段都完全由虛擬機(jī)主導(dǎo)和控制。到了初始化階段才真正執(zhí)行Java代碼。

初始化的過程包括執(zhí)行類構(gòu)造器方法,static變量賦值語句,static{}代碼塊,如果是一個(gè)子類進(jìn)行初始化會(huì)先對其父類進(jìn)行初始化,保證其父類在子類之前進(jìn)行初始化;所以其實(shí)在java中初始化一個(gè)類,那么必然是先初始化java.lang.Object,因?yàn)樗械膉ava類都繼承自java.lang.Object。

如static int a = 100;在準(zhǔn)備階段,a被賦默認(rèn)值0,在初始化階段就會(huì)被賦值為100。

首先什么情況下類會(huì)初始化?

Java虛擬機(jī)規(guī)范中嚴(yán)格規(guī)定了有且只有五種情況必須對類進(jìn)行初始化:

  • 1、使用new字節(jié)碼指令創(chuàng)建類的實(shí)例,或者使用getstatic、putstatic讀取或設(shè)置一個(gè)靜態(tài)字段的值(放入常量池中的常量除外),或者調(diào)用一個(gè)靜態(tài)方法的時(shí)候,對應(yīng)類必須進(jìn)行過初始化。

  • 2、通過java.lang.reflect包的方法對類進(jìn)行反射調(diào)用的時(shí)候,如果類沒有進(jìn)行過初始化,則要首先進(jìn)行初始化。

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

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

  • 5、使用jdk1.7的動(dòng)態(tài)語言支持時(shí),如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果REF_getStatic、REF_putStatic、RE_invokeStatic的方法句柄,并且這個(gè)方法句柄對應(yīng)的類沒有進(jìn)行初始化,則需要先觸發(fā)其初始化。
    接口的加載過程與類的加載過程稍有不同,接口中不能使用static{}快。當(dāng)一個(gè)接口在初始化時(shí),并不要求其父接口全部都完成初始化,只有在真正用到父接口時(shí)(如引用接口中定義的變量)才會(huì)初始化。

注意,虛擬機(jī)規(guī)范使用了“有且只有”這個(gè)詞描述,這五種情況被稱為“主動(dòng)引用”,除了這五種情況,所有其他的類引用方式都不會(huì)觸發(fā)類初始化,被稱為“被動(dòng)引用”。

6.4 總結(jié)

前面講了這么多,那還是有一個(gè)疑問,一個(gè)類是啥時(shí)候開始加載的呢?

其實(shí),Java虛擬機(jī)規(guī)范中并沒有進(jìn)行強(qiáng)制約束,這點(diǎn)虛擬機(jī)根據(jù)自身實(shí)現(xiàn)來把握。但對于初始化階段,虛擬機(jī)規(guī)范則是嚴(yán)格規(guī)定了有且只有5種情況必須立即對類進(jìn)行初始化(加載,驗(yàn)證,準(zhǔn)備肯定要在此之前進(jìn)行了),這5種情況我們上面有過介紹。

7、類加載器

類加載器實(shí)現(xiàn)的功能是即為加載階段獲取二進(jìn)制字節(jié)流的時(shí)候。

JVM提供了以下3種系統(tǒng)的類加載器:

  • 啟動(dòng)類加載器(Bootstrap ClassLoader):最頂層的類加載器,負(fù)責(zé)加載 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath參數(shù)指定路徑中的,且被虛擬機(jī)認(rèn)可(按文件名識(shí)別,如rt.jar)的類。

  • 擴(kuò)展類加載器(Extension ClassLoader):負(fù)責(zé)加載 JAVA_HOME\lib\ext 目錄中的,或通過java.ext.dirs系統(tǒng)變量指定路徑中的類庫。

  • 應(yīng)用程序類加載器(Application ClassLoader):也叫做系統(tǒng)類加載器,可以通過getSystemClassLoader()獲取,負(fù)責(zé)加載用戶路徑(classpath)上的類庫。如果沒有自定義類加載器,一般這個(gè)就是默認(rèn)的類加載器。

類加載器之間的層次關(guān)系如下:

image.png

類加載器之間的這種層次關(guān)系叫做雙親委派模型。
雙親委派模型要求除了頂層的啟動(dòng)類加載器(Bootstrap ClassLoader)外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器。這里的類加載器之間的父子關(guān)系一般不是以繼承關(guān)系實(shí)現(xiàn)的,而是用組合實(shí)現(xiàn)的。

雙親委派模型的工作過程:
如果一個(gè)類接受到類加載請求,他自己不會(huì)去加載這個(gè)請求,而是將這個(gè)類加載請求委派給父類加載器,這樣一層一層傳送,直到到達(dá)啟動(dòng)類加載器(Bootstrap ClassLoader)。
只有當(dāng)父類加載器無法加載這個(gè)請求時(shí),子加載器才會(huì)嘗試自己去加載。

雙親委派模型很好的解決了各個(gè)類加載器加載基礎(chǔ)類的統(tǒng)一性問題。即越基礎(chǔ)的類由越上層的加載器進(jìn)行加載。

破壞雙親委派模型:
若加載的基礎(chǔ)類中需要回調(diào)用戶代碼,而這時(shí)頂層的類加載器無法識(shí)別這些用戶代碼,怎么辦呢?這時(shí)就需要破壞雙親委派模型了。

Spring破壞雙親委派模型
Spring要對用戶程序進(jìn)行組織和管理,而用戶程序一般放在WEB-INF目錄下,由WebAppClassLoader類加載器加載,而Spring由Common類加載器或Shared類加載器加載。
那么Spring是如何訪問WEB-INF下的用戶程序呢?
使用線程上下文類加載器。 Spring加載類所用的classLoader都是通過Thread.currentThread().getContextClassLoader()獲取的。當(dāng)線程創(chuàng)建時(shí)會(huì)默認(rèn)創(chuàng)建一個(gè)AppClassLoader類加載器(對應(yīng)Tomcat中的WebAppclassLoader類加載器): setContextClassLoader(AppClassLoader)。
利用這個(gè)來加載用戶程序。即任何一個(gè)線程都可通過getContextClassLoader()獲取到WebAppclassLoader。

8、tomcat類加載架構(gòu)

image.png

Tomcat目錄下有4組目錄:

/common目錄下:類庫可以被Tomcat和Web應(yīng)用程序共同使用;由 Common ClassLoader類加載器加載目錄下的類庫;
/server目錄:類庫只能被Tomcat可見;由 Catalina ClassLoader類加載器加載目錄下的類庫;
/shared目錄:類庫對所有Web應(yīng)用程序可見,但對Tomcat不可見;由 Shared ClassLoader類加載器加載目錄下的類庫;
/WebApp/WEB-INF目錄:僅僅對當(dāng)前web應(yīng)用程序可見。由 WebApp ClassLoader類加載器加載目錄下的類庫;
每一個(gè)JSP文件對應(yīng)一個(gè)JSP類加載器。

9、總結(jié)

我們前面寫了JVM內(nèi)存管理,OOM,垃圾回收,類加載,下面我們通過一個(gè)對象的生命周期來把這些知識(shí)點(diǎn)串聯(lián)起來。

對象的生命周期可以從類加載開始算起,但是JVM并沒有嚴(yán)格規(guī)定類加載開始的時(shí)機(jī),我們這里以new 一個(gè)對象開始。

Java在new一個(gè)對象的時(shí)候,會(huì)先查看對象所屬的類有沒有被加載到內(nèi)存,如果沒有的話,就會(huì)先通過類的全限定名來加載。

加載并初始化類完成后,再進(jìn)行對象的創(chuàng)建工作。

我們先假設(shè)是第一次使用該類,這樣的話new一個(gè)對象就可以分為兩個(gè)過程:加載并初始化類和創(chuàng)建對象。

https://mp.weixin.qq.com/s/QXDINKJ_5PfUgvDRREctwQ

9.1 類加載
    1. 加載階段
      由類加載器負(fù)責(zé)根據(jù)一個(gè)類的全限定名來讀取此類的二進(jìn)制字節(jié)流到JVM內(nèi)部,并存儲(chǔ)在運(yùn)行時(shí)內(nèi)存區(qū)的方法區(qū),然后將其轉(zhuǎn)換為一個(gè)與目標(biāo)類型對應(yīng)的java.lang.Class對象實(shí)例。
    1. 連接階段:將加載到JVM中的二進(jìn)制字節(jié)流的類數(shù)據(jù)信息合并到JVM的運(yùn)行時(shí)內(nèi)存中。
    • 驗(yàn)證:驗(yàn)證是否符合class文件規(guī)范
    • 準(zhǔn)備:為類中的所有靜態(tài)變量分配內(nèi)存空間,并為其設(shè)置一個(gè)初始值(由于還沒有產(chǎn)生對象,實(shí)例變量不在此操作范圍內(nèi))
      被final修飾的static變量(常量),會(huì)直接賦值。
    • 解析:將常量池中的符號(hào)引用轉(zhuǎn)為直接引用(得到類或者字段、方法在內(nèi)存中的指針或者偏移量,以便直接調(diào)用該方法),這個(gè)可以在初始化之后再執(zhí)行。
  • 3.初始化階段

    • 為靜態(tài)變量賦值
    • 執(zhí)行static代碼塊

最終,方法區(qū)會(huì)存儲(chǔ)當(dāng)前類類信息,包括類的靜態(tài)變量、類初始化代碼(定義靜態(tài)變量時(shí)的賦值語句 和 靜態(tài)初始化代碼塊)、實(shí)例變量定義、實(shí)例初始化代碼(定義實(shí)例變量時(shí)的賦值語句實(shí)例代碼塊和構(gòu)造方法)和實(shí)例方法,還有父類的類信息引用。

7.2 創(chuàng)建對象

1、在堆區(qū)分配對象需要的內(nèi)存
分配的內(nèi)存包括本類和父類的所有實(shí)例變量,但不包括任何靜態(tài)變量。
2、對所有實(shí)例變量賦默認(rèn)值
將方法區(qū)內(nèi)對實(shí)例變量的定義拷貝一份到堆區(qū),然后賦默認(rèn)值。
3、執(zhí)行實(shí)例初始化代碼
初始化順序是先初始化父類再初始化子類,初始化時(shí)先執(zhí)行實(shí)例代碼塊然后是構(gòu)造方法。
4、如果有類似于Child c = new Child()形式的c引用的話,在棧區(qū)定義Child類型引用變量c,然后將堆區(qū)對象的地址賦值給它

需要注意的是,每個(gè)子類對象持有父類對象的引用,可在內(nèi)部通過super關(guān)鍵字來調(diào)用父類對象,但在外部不可訪問。

7.3 垃圾回收

觸發(fā)GC運(yùn)行的條件要分新生代和老年代的情況來進(jìn)行討論,有以下幾點(diǎn)會(huì)觸發(fā)GC:

  • 當(dāng)Eden區(qū)和From Survivor區(qū)滿時(shí);
  • 老年代空間不足
  • 通過Minor GC后進(jìn)入老年代的平均大小大于老年代的可用內(nèi)存
  • 由Eden區(qū)、From Space區(qū)向To Space區(qū)復(fù)制時(shí),對象大小大于To Space可用內(nèi)存,則把該對象轉(zhuǎn)存到老年代,且老年代的可用內(nèi)存小于該對象大小
7.4 OOM

內(nèi)存溢出:當(dāng)申請的內(nèi)存超出了JVM能提供的內(nèi)存大小,此時(shí)稱之為溢出。
從上面內(nèi)存模型中我們可以總結(jié)出最常見的OOM情況:

  • java.lang.OutOfMemoryError: Java heap space ------>java堆內(nèi)存溢出,此種情況最常見,一般由于內(nèi)存泄露或者堆的大小設(shè)置不當(dāng)引起。對于內(nèi)存泄露,需要通過內(nèi)存監(jiān)控軟件查找程序中的泄露代碼,而堆大小可以通過虛擬機(jī)參數(shù)-Xms,-Xmx等修改。

  • java.lang.OutOfMemoryError: PermGen space ------>java永久代溢出,即方法區(qū)溢出了,一般出現(xiàn)于大量Class或者jsp頁面,或者采用cglib等反射機(jī)制的情況,因?yàn)樯鲜銮闆r會(huì)產(chǎn)生大量的Class信息存儲(chǔ)于方法區(qū)。此種情況可以通過更改方法區(qū)的大小來解決,使用類似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,過多的常量尤其是字符串也會(huì)導(dǎo)致方法區(qū)溢出。

  • java.lang.StackOverflowError ------> 不會(huì)拋OOM error,但也是比較常見的Java內(nèi)存溢出。JAVA虛擬機(jī)棧溢出,一般是由于程序中存在死循環(huán)或者深度遞歸調(diào)用造成的,棧大小設(shè)置太小也會(huì)出現(xiàn)此種溢出。可以通過虛擬機(jī)參數(shù)-Xss來設(shè)置棧的大小。

4.2 如何排查

JVM Heap dump和Thread dump

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容