java基礎(chǔ)之JVM1.8工作原理

1 概念

JVM 是 .class文件和硬件系統(tǒng)之間的接口,即JVM屏蔽了與具體平臺相關(guān)的信息,這就是java程序一次編譯到處執(zhí)行的根本原因。

2JVM的構(gòu)成

JVM = ClassLoader + execution engine + runtime data area
java虛擬機(jī) = 類加載器 + 執(zhí)行引擎 + 運(yùn)行時數(shù)據(jù)區(qū)

2.1ClassLoader

ClassLoader的核心作用就是把class文件加載到JVM中。其本質(zhì)是將Class 的字節(jié)碼形式轉(zhuǎn)換成內(nèi)存形式的 Class 對象。字節(jié)碼可以來自于磁盤文件 *.class,也可以是 jar 包里的 *.class,也可以來自遠(yuǎn)程服務(wù)器提供的字節(jié)流,字節(jié)碼的本質(zhì)就是一個字節(jié)數(shù)組 []byte,它有特定的復(fù)雜的內(nèi)部格式。


classloader.png

JVM 運(yùn)行并不是一次性加載所需要的全部類的,而是在運(yùn)行過程中按需加載,也就是延遲加載。程序在運(yùn)行的過程中會逐漸遇到很多不認(rèn)識的新類,這時候就會調(diào)用 ClassLoader 來加載這些類。加載完成后就會將 Class 對象存在 ClassLoader 里面,下次就不需要重新加載了。類加載時機(jī)有以下幾種:
1)使用new關(guān)鍵字實(shí)例化對象
2)訪問類的靜態(tài)變量(不是常量)
3)訪問類的靜態(tài)方法
4)反射如( Class.forName,這里只加載類不創(chuàng)建對象實(shí)例 )
5)當(dāng)初始化一個類時,發(fā)現(xiàn)其父類還未初始化,則先觸發(fā)父類的初始化
6)虛擬機(jī)啟動時,定義了main()方法的那個類先初始化

2.1.1ClassLoader的種類

  • 1.啟動類加載器(Bootstrap ClassLoader)
    這個類加載使用C++語言實(shí)現(xiàn)的,是虛擬機(jī)自身的一部分。負(fù)責(zé)將$JAVA_HOME/lib或者 -Xbootclasspath參數(shù)指定路徑下面的文件(按照文件名識別,如 rt.jar) 加載到虛擬機(jī)內(nèi)存中.啟動類加載器無法直接被 java 代碼引用。
  • 2.擴(kuò)展類加載器(Extension ClassLoader)
    擴(kuò)展類加載器是指sun.misc.Launcher\$ExtClassLoader類,是Launcher的靜態(tài)內(nèi)部類,負(fù)責(zé)加載$JAVA_HOME/lib/ext目錄中的文件,或者java.ext.dirs系統(tǒng)變量所指定的路徑的類庫。
  • 3.應(yīng)用程序類加載器(Application ClassLoader)
    應(yīng)用程序類加載器是指sun.misc.Launcher$AppClassLoader。它負(fù)責(zé)加載系統(tǒng)類路徑java -classpath-D java.class.path指定路徑下的類庫,也就是我們經(jīng)常用到的classpath路徑,開發(fā)者可以直接使用系統(tǒng)類加載器,一般情況下該類加載是程序中默認(rèn)的類加載器,通過ClassLoader#getSystemClassLoader()方法可以獲取到該類加載器。

2.1.2類加載順序

類的即在順序遵循雙親委派機(jī)制(Parent delegation),即:在需要加載一個類的時候,我們首先判斷該類是否已被加載,如果沒有就判斷是否已被父加載器加載,如果還沒有再調(diào)用自己的findClass()方法嘗試加載。這個加載順序定義在java.lang.ClassLoader#loadClass()方法里。我們在自定義ClassLoader時不建議重寫loadClass()方法,而是 重寫findClass()方法。雙親委派機(jī)制的好處是提高了安全性,避免用戶自己編寫的類動態(tài)替換 Java 的一些核心類,比如 String,同時也避免了重復(fù)加載,因?yàn)?JVM 中區(qū)分不同類,不僅僅是根據(jù)類名,相同的 class 文件被不同的 ClassLoader 加載就是不同的兩個類,如果相互轉(zhuǎn)型的話會拋java.lang.ClassCaseException。

parent_delegation.png

需要注意的是,圖中各個類加載器之間不是繼承關(guān)系,而是關(guān)聯(lián)關(guān)系,具體是java.lang.ClassLoader類定義了一個parent屬性。

public abstract class ClassLoader {

    private static native void registerNatives();
    static {
        registerNatives();
    }

    // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;
    ……

2.1.3自定義類加載器

ClassLoader 里面有三個重要的方法 loadClass()findClass()defineClass()。
loadClass() 方法是加載目標(biāo)類的入口,它首先會查找當(dāng)前 ClassLoader 以及它的雙親里面是否已經(jīng)加載了目標(biāo)類,如果沒有找到就會讓雙親嘗試加載,如果雙親都加載不了,就會調(diào)用 findClass() 讓自定義加載器自己來加載目標(biāo)類。ClassLoader 的 findClass()方法是需要子類來覆蓋的,不同的加載器將使用不同的邏輯來獲取目標(biāo)類的字節(jié)碼。拿到這個字節(jié)碼之后再調(diào)用 defineClass() 方法將字節(jié)碼轉(zhuǎn)換成 Class 對象。
適用場景:
1.加載加密的.class文件。
將正常的.class文件用代碼加密以后,自定義ClassLoader 類,重寫findClass()方法,用同樣的加密算法對讀出文件的字節(jié)流進(jìn)行解密。參考:自定義ClassLoader對Class加密并解密
2.解決同名class不同版本共存問題(鉆石引用)
參考:老大難的 Java ClassLoader,到了該徹底理解它的時候了

2.2runtime data area

JVM在運(yùn)行時將數(shù)據(jù)劃分為了5個區(qū)域來存儲:heap, java stack, native method stack, PC register, method area。

runtime-data-area.png

1、程序計(jì)數(shù)器
程序計(jì)數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,它可以是看作當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。說簡單一點(diǎn)就是一個計(jì)數(shù)器,當(dāng)字節(jié)碼解釋器工作是能夠通過改變這個計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令。在說明一點(diǎn),各條線程之間計(jì)數(shù)器互不影響,獨(dú)立存儲,程序計(jì)數(shù)器器內(nèi)存區(qū)域?yàn)?線程私有的。
2.本地方法棧
本地方法棧和虛擬機(jī)棧所發(fā)揮的作用是很相似的,它們之間的區(qū)別不過是 虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(字節(jié)碼)服務(wù),而本地方法棧則為虛擬機(jī)使用到的Native方法服務(wù)。Sun HotSpot 直接就把本地方法棧和虛擬機(jī)棧合二為一。本地方法棧也會拋出StackOverflowErrorOutOfMemoryError異常。
3、Java虛擬機(jī)棧
 Java棧也稱作虛擬機(jī)棧(Java Vitual Machine Stack),也是常說的棧。Java棧是Java方法執(zhí)行的內(nèi)存模型。Java棧中存放的是一個個的棧幀,每個棧幀對應(yīng)一個被調(diào)用的方法,在棧幀中包括局部變量表(Local Variables)、操作數(shù)棧(Operand Stack)、指向當(dāng)前方法所屬的類的運(yùn)行時常量池(運(yùn)行時常量池的概念在方法區(qū)部分會談到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些額外的附加信息。棧也是線程私有的。
Java虛擬機(jī)棧模型.png

4、Java堆
堆是jvm內(nèi)存管理的最大的一塊區(qū)域,此內(nèi)存區(qū)域的唯一目的就是存放對象的實(shí)例,所有對象實(shí)例與數(shù)組都要在堆上分配內(nèi)存。它也是垃圾收集器的主要管理區(qū)域。java對可以處于物理上不連續(xù)的空間,只要邏輯上是連續(xù)的即可。線程共享的區(qū)域。如果在堆中沒有內(nèi)存完成實(shí)例分配,并且堆也無法再擴(kuò)展時,將拋出OutOfMemoryError異常。
image.png

1).Young (新生代)
新生代 分為三部分。Eden區(qū)(new 的對象)和兩個大小相同的Survivior區(qū)(某一時刻,只有一個被使用),另外一個,當(dāng)Eden區(qū)滿了,GC就會將存活的對象移動到空閑的Survivor區(qū),根據(jù)JVM的策略,在經(jīng)過幾次垃圾收集后,依然存活在Survivor區(qū)的對象,將移動到Tenured區(qū)(老年代)
2).Tenured(老年代)
老年代 主要保存生命周期長的對象。(new 的大對象,會直接進(jìn)入老年代)

5、方法區(qū)
方法區(qū)在JVM中也是一個非常重要的區(qū)域,它與堆一樣,是被線程共享的區(qū)域。在方法區(qū)中,存儲了每個類的信息(包括類的名稱、方法信息、字段信息)、靜態(tài)變量、常量以及編譯器編譯后的代碼等。方法區(qū)是堆的一個邏輯部分,為了區(qū)分Java堆,它還有一個別名Non-Heap(非堆)。相對而言,GC對于這個區(qū)域的收集是很少出現(xiàn)的。當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時,將拋出OutOfMemoryError異常。在Java 7及之前版本,我們也習(xí)慣稱方法區(qū)它為“永久代”(Permanent Generation),更確切來說,應(yīng)該是“HotSpot使用永久代實(shí)現(xiàn)了方法區(qū)”!
6、運(yùn)行時常量池
運(yùn)行時常量池是方法區(qū)的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池( Constant pool table),用于存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后進(jìn)入運(yùn)行時常量池中存放。運(yùn)行時常量池相對于class文件常量池的另外一個特性是具備動態(tài)性,java語言并不要求常量一定只有編譯器才產(chǎn)生,也就是并非預(yù)置入class文件中常量池的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時常量池,運(yùn)行期間也可能將新的常量放入池中。
7、直接內(nèi)存
直接內(nèi)存(Direct Memory)并不是虛擬機(jī)運(yùn)行時數(shù)據(jù)區(qū)的一部分,也不是JVM規(guī)范中定義的內(nèi)存區(qū)域。但這部分內(nèi)存也被頻繁的使用,而且也可能導(dǎo)致OutOfMemoryError異常出現(xiàn)。JDK1.4中新引入了NIO機(jī)制,它是一種基于通道與緩沖區(qū)的新I/O方式,可以直接從操作系統(tǒng)中分配直接內(nèi)存,即直接堆外分配內(nèi)存,這樣能在一些場景中提高性能,因?yàn)楸苊饬嗽贘ava堆和Native堆中來回復(fù)制數(shù)據(jù)。


各個區(qū)域共享情況.png

詳情參考:Java內(nèi)存管理-JVM內(nèi)存模型以及JDK7和JDK8內(nèi)存模型對比總結(jié)(三)

3 GC

GC動作發(fā)生之前,需要確定堆內(nèi)存中哪些對象是存活的,一般有兩種方法:引用計(jì)數(shù)法和可達(dá)性分析法。
1、引用計(jì)數(shù)法
在對象上添加一個引用計(jì)數(shù)器,每當(dāng)有一個對象引用它時,計(jì)數(shù)器加1,當(dāng)使用完該對象時,計(jì)數(shù)器減1,計(jì)數(shù)器值為0的對象表示不可能再被使用。引用計(jì)數(shù)法實(shí)現(xiàn)簡單,判定高效,但不能解決對象之間相互引用的問題。
2、可達(dá)性分析法
通過一系列稱為 “GC Roots” 的對象作為起點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索路徑稱為 “引用鏈”,以下對象可作為GC Roots:

  • 本地變量表中引用的對象
  • 方法區(qū)中靜態(tài)變量引用的對象
  • 方法區(qū)中常量引用的對象
  • Native方法引用的對象
    當(dāng)一個對象到 GC Roots 沒有任何引用鏈時,意味著該對象可以被回收。


    image

    在可達(dá)性分析法中,判定一個對象objA是否可回收,至少要經(jīng)歷兩次標(biāo)記過程:
    1)如果對象objA到 GC Roots沒有引用鏈,則進(jìn)行第一次標(biāo)記。
    2)如果對象objA重寫了finalize()方法,且還未執(zhí)行過,那么objA會被插入到F-Queue隊(duì)列中,由一個虛擬機(jī)自動創(chuàng)建的、低優(yōu)先級的Finalizer線程觸發(fā)其finalize()方法。finalize()方法是對象逃脫死亡的最后機(jī)會,GC會對隊(duì)列中的對象進(jìn)行第二次標(biāo)記,如果objA在finalize()方法中與引用鏈上的任何一個對象建立聯(lián)系,那么在第二次標(biāo)記時,objA會被移出“即將回收”集合。

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

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