JVM學(xué)習筆記

什么是Java虛擬機

各個硬件平臺本身不支持Java字節(jié)碼,Java虛擬機就是在不同硬件平臺上支持Java字節(jié)碼的軟件系統(tǒng)。

Java語言寫的源程序通過Java編譯器,編譯成與平臺無關(guān)的字節(jié)碼程序(.class文件),字節(jié)碼文件是一種和任何具體機器環(huán)境及操作系統(tǒng)環(huán)境無關(guān)的中間代碼,它是一種二進制文件,是Java源文件由Java編譯器編譯后生成的目標代碼文件。Java解釋 器負責將字節(jié)碼文件翻譯成具體硬件環(huán)境和操作系統(tǒng)平臺下的機器代碼,以便執(zhí)行。因此Java程序不能直接運行在現(xiàn)有的操 作系統(tǒng)平臺上,它必須運行在被稱為Java虛擬機的軟件平臺之上。

Java虛擬機(JVM)是運行Java程序的軟件環(huán)境,Java 解釋器就是Java虛擬機的一部分。在運行Java程序時,首先會啟動JVM,然 后由它來負責解釋執(zhí)行Java的字節(jié)碼,并且 Java字節(jié)碼只能運行于JVM之上。這樣利用JVM就可以把Java字節(jié)碼程序和具體的硬件平臺以及操作系統(tǒng)環(huán)境分隔開來,只 要在不同的計算機上安裝了針對于特定具體平臺的JVM,Java程序就可以運行,而不用考慮當前具體的硬件平臺及操作系統(tǒng) 環(huán)境,也不用考慮字節(jié)碼文件是在何種平臺上生成的。JVM把這種不同軟硬件平臺的具體差別隱藏起來,從而實現(xiàn)了真正的 二進制代碼級的跨平臺移植。JVM是Java平臺 無關(guān)的基礎(chǔ),Java的跨平臺特性正是通過在JVM中運行Java程序?qū)崿F(xiàn)的。

Java的這種運行機Java語言這種“一次編寫,到處運行(writeonce,run anywhere)”的方式,有效地解決了目前大多數(shù)高 級程序設(shè)計語言需要針對不同系統(tǒng)來編譯產(chǎn)生不同機器代碼的問題,即硬件環(huán)境和操作平臺的異構(gòu)問題,大大降低了程序開 發(fā)、維護和管理的開銷。

JVM的內(nèi)存管理機制

Java虛擬機在運行時會將虛擬機管理的內(nèi)存分成若干塊。根據(jù)《Java虛擬機規(guī)范(Java SE 7版)》的規(guī)定,分為以下幾塊


jvm01.PNG

我們知道Java是支持多線程的,所以JVM虛擬機的數(shù)據(jù)區(qū)分為了兩大塊,一塊是所有線程共用
,包括堆(Heap)和方法 區(qū)(Method Area);另一塊是各個線程獨有的,與線程生命周期一致,包括程序計數(shù)器(Program Counter Register), 虛擬機棧(VM Stack)和本地方法棧。

  • 程序計數(shù)器 程序計數(shù)器用來保證程序按照正確地順序執(zhí)行,正因為每個線程都有一個自己的程序計數(shù)器,所以保證了 程序在不同線程切換時可以恢復(fù)到正確的位置。如果程序執(zhí)行的是Java方法,計數(shù)器記錄的是正在執(zhí)行的字節(jié)碼指令 的地址;如果正在執(zhí)行的是native方法,則計數(shù)器值為空。
  • Java 虛擬機棧 描述的是Java方法執(zhí)行的內(nèi)存模型,它也是線程私有的。 每個Java method在執(zhí)行的時候都會創(chuàng)建一個 stack frame用來存儲局部變量表、操作數(shù)棧、方法出口等信息,每個 Java method的執(zhí)行過程都對應(yīng)著一個stack frame從入棧到出棧的過程。 局部變量表 用于存放編譯期的各種基本數(shù)據(jù)類型和對象引用。有 boolean,byte,char,short,int,?oat,long,double,reference,returnAdress。 局部變量表中的單位為Slot(2^32),除了long和double占用兩個Slot其余均占用一個Slot.
  • 本地方法棧 和虛擬機棧的作用非常相似,他們的區(qū)別不過是虛擬機棧為Java method服務(wù),而本地方法棧為Native method服務(wù)。
  • Java 堆 是Java虛擬機管理的內(nèi)存中最大的一塊,它是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機啟動的時候創(chuàng)建。 此內(nèi)存區(qū)域的目的是存放對象實例,幾乎所有的對象實例以及數(shù)組都要在堆上分配。Java堆也被稱為“GC 堆”(Garbage Collected Heap)。是GC發(fā)生的主要區(qū)域。Java堆可以處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上 連續(xù)即可。 方法區(qū) 與Java堆一樣,是各個線程共享的內(nèi)存區(qū)域,用于存儲被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯后 的代碼等數(shù)據(jù)。 運行時常量池 常量池是方法區(qū)的一部分。用于存放編譯器生成的各種字面量和符號引用,這部分內(nèi)容 將在類加載后進入方法區(qū)的運行時常量池中存放。
  • 元空間 從jdk7開始,就開始了永久代的轉(zhuǎn)移工作,將譬如符號引用(Symbols)轉(zhuǎn)移到了native heap;字面量 (interned strings)轉(zhuǎn)移到了java堆;類的靜態(tài)變量(class statics)轉(zhuǎn)移到了java堆。但是永久代在還存在于JDK7中,直 到JDK8,永久代才完全消失,轉(zhuǎn)而使用元空間。而元空間是直接存在內(nèi)存中,不在java虛擬機中的,因此元空間依賴 于內(nèi)存大小。當然你也可以自定義元空間大小。元空間的本質(zhì)和永久代類似,都是對JVM規(guī)范中方法區(qū)的實現(xiàn)。不過 元空間與永久代之間的最大區(qū)別在于:元空間并不在虛擬機中,而是使用本地內(nèi)存。 (http://openjdk.java.net/jeps/122 JEP 122: Remove the Permanent Generation) Java方法如果想訪問對象,需要通過reference數(shù)據(jù)操作來,reference如何定位、訪問Java堆中的對象虛擬機規(guī)范沒有規(guī) 定,一般有以下兩種主流方式
    jvm02.PNG

垃圾回收

垃圾回收要考慮三個問題:哪些內(nèi)存需要回收,什么時候回收,如何回收。
我們先來看第一個問題:
在主流的商用程序語言(Java、C#、甚至更古老的Lisp)的主流實現(xiàn)中都是通過可達性分析來判斷對象是否存活。這個算法 的基本思路就是通過一系列名為GC Roots的對象作為起始點,從這些節(jié)點開始向下搜索,搜索所走過的路徑稱為引用鏈 (Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的,下圖對象object5, object6, object7雖然有互相判斷,但它們到GC Roots是不可達的,所以它們將會判定為是可回收對象。


jvm03.PNG

那么那些點可以作為GC Roots呢?一般來說,如下情況的對象可以作為GC Roots:

  • 虛擬機棧(棧楨中的本地變量表)中的引用的對象
  • 方法區(qū)中的類靜態(tài)屬性引用的對象
  • 方法區(qū)中的常量引用的對象
  • 本地方法棧中JNI(Native方法)的引用的對象

即使在可達性分析算法中的不可達的對象,也并非是“非死不可”的,這個時候它們暫時進入“緩刑”階段。要真正宣告死亡需 要經(jīng)歷兩次標記:如果對象在進行可達性分析后沒有與GC Roots相連接的引用鏈,那它將會被第一次標記并且進行一次篩 選,篩選條件是此對象是否有必要執(zhí)行?nalize()方法。當對象沒有覆蓋?nalize()方法,或者?nalize()方法已經(jīng)被虛擬機調(diào)用 過,這兩種情況都沒必要執(zhí)行?nalize()方法。 如果這個對象被判定為有必要執(zhí)行?nalize()方法,那么這個對象會被放置在一 個叫做F-Queue的隊列中。并在稍后由一個由虛擬機自動建立的,低優(yōu)先級的Finalizer線程去執(zhí)行它。這里所謂的“執(zhí)行”是 指虛擬機會觸發(fā)這個方法,注意如果一個對象的?nalize()方法執(zhí)行緩慢,或者發(fā)生了死循環(huán),將可能導(dǎo)致隊列中其他對象處 于等待,導(dǎo)致整個內(nèi)存回收系統(tǒng)崩潰。?nalize()方法是對象逃脫死亡命運的最后一次機會,稍后GC將會對F-Queue中的對象 進行第二次小規(guī)模標記。

內(nèi)存回收算法 常見的內(nèi)存回收算法有以下幾種

  1. 標記-清除算法: 最基礎(chǔ)的垃圾收集算法,算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成之后統(tǒng)一回收掉 所有被標記的對象。
    標記-清除算法的缺點有兩個:首先,效率問題,標記和清除效率都不高。其次,標記清除之后會產(chǎn)生大量的不連續(xù)的內(nèi)存 碎片,空間碎片太多會導(dǎo)致當程序需要為較大對象分配內(nèi)存時無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動 作。
  2. 復(fù)制算法:
    將可用內(nèi)存按容量分成大小相等的兩塊,每次只使用其中一塊,當這塊內(nèi)存使用完了,就將還存活的對象復(fù)制到另一塊內(nèi)存 上去,然后把使用過的內(nèi)存空間一次清理掉。這樣使得每次都是對其中一塊內(nèi)存進行回收,內(nèi)存分配時不用考慮內(nèi)存碎片等 復(fù)雜情況,只需要移動堆頂指針,按順序分配內(nèi)存即可,實現(xiàn)簡單,運行高效。
    復(fù)制算法的缺點顯而易見,可使用的內(nèi)存降為原來一半。
  3. 標記-整理算法: 標記-整理算法在標記-清除算法基礎(chǔ)上做了改進,標記階段是相同的標記出所有需要回收的對象,在標記完成之后不是直接 對可回收對象進行清理,而是讓所有存活的對象都向一端移動,在移動過程中清理掉可回收的對象,這個過程叫做整理。
    標記-整理算法相比標記-清除算法的優(yōu)點是內(nèi)存被整理以后不會產(chǎn)生大量不連續(xù)內(nèi)存碎片問題。 復(fù)制算法在對象存活率高的情況下就要執(zhí)行較多的復(fù)制操作,效率將會變低,而在對象存活率高的情況下使用標記-整理算 法效率會大大提高。
  4. 分代收集算法:
    根據(jù)內(nèi)存中對象的存活周期不同,將內(nèi)存劃分為幾塊,java的虛擬機中一般把內(nèi)存劃分為新生代和年老代,當新創(chuàng)建對象時 一般在新生代中分配內(nèi)存空間,當新生代垃圾收集器回收幾次之后仍然存活的對象會被移動到年老代內(nèi)存中,當大對象在新 生代中無法找到足夠的連續(xù)內(nèi)存時也直接在年老代中創(chuàng)建。

堆內(nèi)存被分成新生代和年老代兩個部分,整個堆內(nèi)存使用分代復(fù)制垃圾收集算法。

  1. 新生代: 新生代使用復(fù)制和標記-清除垃圾收集算法,研究表明,新生代中98%的對象是朝生夕死的短生命周期對象,所以不需要將 新生代劃分為容量大小相等的兩部分內(nèi)存,而是將新生代分為Eden區(qū),Survivor from和Survivor to三部分,其占新生代內(nèi)存 容量默認比例分別為8:1:1,其中Survivor from和Survivor to總有一個區(qū)域是空白,只有Eden和其中一個Survivor總共 90%的新生代容量用于為新創(chuàng)建的對象分配內(nèi)存,只有10%的Survivor內(nèi)存浪費,當新生代內(nèi)存空間不足需要進行垃圾回收 時,仍然存活的對象被復(fù)制到空白的Survivor內(nèi)存區(qū)域中,Eden和非空白的Survivor進行標記-清理回收,兩個Survivor區(qū)域 是輪換的。
    新生代中98%情況下空白Survivor都可以存放垃圾回收時仍然存活的對象,2%的極端情況下,如果空白Survivor空間無法存 放下仍然存活的對象時,使用內(nèi)存分配擔保機制,直接將新生代依然存活的對象復(fù)制到年老代內(nèi)存中,同時對于創(chuàng)建大對象 時,如果新生代中無足夠的連續(xù)內(nèi)存時,也直接在年老代中分配內(nèi)存空間。
    Java虛擬機對新生代的垃圾回收稱為Minor GC,次數(shù)比較頻繁,每次回收時間也比較短。 使用java虛擬機-Xmn參數(shù)可以指定新生代內(nèi)存大小。
  2. 年老代: 年老代中的對象一般都是長生命周期對象,對象的存活率比較高,因此在年老代中使用標記-整理垃圾回收算法。 Java虛擬機對年老代的垃圾回收稱為MajorGC/Full GC,次數(shù)相對比較少,每次回收的時間也比較長。
    當新生代中無足夠空間為對象創(chuàng)建分配內(nèi)存,年老代中內(nèi)存回收也無法回收到足夠的內(nèi)存空間,并且新生代和年老代空間無 法在擴展時,堆就會產(chǎn)生OutOfMemoryError異常。 java虛擬機-Xms參數(shù)可以指定最小內(nèi)存大小,-Xmx參數(shù)可以指定最大內(nèi)存大小,這兩個參數(shù)分別減去Xmn參數(shù)指定的新生 代內(nèi)存大小,可以計算出年老代最小和最大內(nèi)存容量。


    jvm04.PNG

虛擬機字節(jié)碼執(zhí)行引擎

棧幀(Stack Frame)是用于支持虛擬機進行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機運行時數(shù)據(jù)區(qū)中的虛擬機棧 (Virtual Machine Stack)的棧元素。棧幀存儲了方法的局部變量表、操作數(shù)棧、動態(tài)連接和方法返回地址等信息。每一個 方法從調(diào)用開始至執(zhí)行完成的過程,都對應(yīng)著一個棧幀在虛擬機棧里面從入棧到出棧的過程。

每一個棧幀都包括了局部變量表、操作數(shù)棧、動態(tài)連接、方法返回地址和一些額外的附加信息。在編譯程序代碼的時候,棧 幀中需要多大的局部變量表,多深的操作數(shù)棧都已經(jīng)完全確定了,并且寫入到方法表的 Code 屬性之中,因此一個棧幀需要 分配多少內(nèi)存,不會受到程序運行期變量數(shù)據(jù)的影響,而僅僅取決于具體的虛擬機實現(xiàn)。


jvm05.PNG

局部變量表  
用于存放方法參數(shù)和方法內(nèi)部定義的局變量,Class文件中方法的Code屬性的max_locals確定了該方法所需要分配的局 部變量表的最大容量。局部變量表的容量以變量槽(Variable Slot)為最小單位,虛擬機規(guī)范中并沒有明確指明一個Slot應(yīng) 占用的內(nèi)存空間大小,只是規(guī)定一個Slot應(yīng)該能存放一個boolean、byte、char、short、int、?oat、reference或 returnAddress類型的數(shù)據(jù)。   
對于64位的數(shù)據(jù)類型,虛擬機會以高位對齊的方式為其分配兩個連續(xù)的Slot空間。   
虛擬機通過索引定位(類似數(shù)組)的方式使用局部變量表。   
如果執(zhí)行的是實例方法(非static),那局部變量表中第0個索引的Slot默認適用于傳遞方法所屬獨享實例的引用,在方 法中可以通過關(guān)鍵字“this”來訪問這個隱含的參數(shù)。

操作數(shù)棧   
和局部變量區(qū)一樣,操作數(shù)棧也是被組織成一個以字長為單位的數(shù)組。但是和前者不同的是,它不是通過索引來訪問, 而是通過標準的棧操作—壓棧和出?!獊碓L問的。比如,如果某個指令把一個值壓入到操作數(shù)棧中,稍后另一個指令就可以 彈出這個值來使用。   
Java虛擬機的解釋執(zhí)行引擎稱為“基于棧的執(zhí)行引擎”,其中所指的“?!本褪遣僮鲾?shù)棧。

動態(tài)連接   
每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支持方法調(diào)用過程中的動態(tài)連接 (Dynamic Linking)。我們知道Class文件的常量池中存有大量的符號引用,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方 法的符號引用作為參數(shù)。這些符號引用一部分會在類加載階段或者第一次使用的時候就轉(zhuǎn)化為直接引用,這種轉(zhuǎn)化稱為靜態(tài) 解析。另外一部分將在每一次運行期間轉(zhuǎn)化為直接引用,這部分稱為動態(tài)連接。

方法返回地址   
方法有兩種退出方式:return,或者是拋出異常未被捕獲。   
無論以何種方式退出,都需要返回到方法被調(diào)用的位置,程序才能繼續(xù)執(zhí)行。方法返回時可能需要在棧幀中保存一些信 息,用來幫助恢復(fù)它的上層方法的執(zhí)行狀態(tài)。一般來說,方法正常退出時,調(diào)用者PC計數(shù)器的值就可以作為返回地址,棧 幀中很可能會保存這個計數(shù)器值。

方法調(diào)用
方法調(diào)用并不等同于方法執(zhí)行,方法調(diào)用階段唯一的任務(wù)就是確定被調(diào)用方法的版本(即調(diào)用哪一個方法),暫時還不涉及方法內(nèi) 部的具體運行過程。在程序運行時,進行方法調(diào)用是最普遍、最頻繁的操作,但前面已經(jīng)講過,Class文件的編譯過程中不包含 傳統(tǒng)編譯中的連接步驟,一 切方法調(diào)用在Class文件里面存儲的都只是符號引用,而不是方法在實際運行時內(nèi)存布局中的入口 地址(相當于之前說的直接引用)。這個特性給Java帶來了更強大的動態(tài)擴展能力,但也使得Java方法調(diào)用過程變得相對復(fù)雜起 來,需要在類加載期間,甚至到運行期間才能確定目標方法的直接引用。 解析 繼續(xù)前面關(guān)于方法調(diào)用的話題,所有方法調(diào)用中的目標方法在Class文件里面都是一個常量池中的符號引用,在類加載的解 析階段,會將其中的一部分符號引用轉(zhuǎn)化為直接引用,這種解析能成立的前提是:方法在程序真正運行之前就有一個確定的 調(diào)用版本,并且這個方法的調(diào)用版本在運行期是不可改變的。換句話說,調(diào)用目標在程序代碼寫好、編譯器進行編譯時就必 須確定下來。這類方法的調(diào)用稱為解析(Resolution)。 在Java語言中符合“編譯器可知,運行期不可變”這個要求的方法,主要包括靜態(tài)方法和私有方法兩大類。 與之相對應(yīng)的是,在Java虛擬機里面提供了5條方法調(diào)用字節(jié)碼指令,分別如下。

invokestatic :調(diào)用靜態(tài)方法。
invokespecial :調(diào)用實例構(gòu)造器方法、私有方法和父類方法。
invokevirtual :調(diào)用所有的虛方法。
invokeinterface :調(diào)用接口方法,會在運行時再確定一個實現(xiàn)此接口的對象。
invokedynamic :先在運行時動態(tài)解析出調(diào)用點限定符所引用的方法,然后再執(zhí)行該方法

在此之前的4條調(diào)用指令,分派邏輯是固化在Java虛擬機內(nèi)部的,而invokedynamic指令的分派邏輯是由用戶所設(shè)定的引導(dǎo)方法決定的。 解析調(diào)用一定是個靜態(tài)的過程,在編譯期間就完全確定,在類裝載的解析階段就會把涉及的符號引用全部轉(zhuǎn)變?yōu)榭纱_定的直接 引用,不會延遲到運行期再去完成。而分派(Dispatch)調(diào)用則可能是靜態(tài)的也可能是動態(tài)的,根據(jù)分派依據(jù)的宗量數(shù)可分為單分派和多分派。

分派
眾所周知,Java是一門面向?qū)ο蟮某绦蛘Z言,因為Java具備面向?qū)ο蟮?個基本特征:繼承、封裝和多態(tài)。本節(jié)講解的分派調(diào)用 過程將會揭示多態(tài)性特征的一些最基本的體現(xiàn), 如“重載”和“重寫”在Java虛擬機之中是如何實現(xiàn)的,這里的實現(xiàn)當然不是語法 上該如何寫, 我們關(guān)心的依然是虛擬機如何確定正確的目標方法。 重載時是通過參數(shù)的靜態(tài)類型而不是實際類型作為判定依據(jù)的。所有依賴靜態(tài)類型來定位方法執(zhí)行版本的分派動作稱為靜態(tài) 分派。靜態(tài)分派的典型應(yīng)用是方法重載。靜態(tài)分派發(fā)生在編譯階段,因此確定靜態(tài)分派的動作實際上不是由虛擬機來執(zhí)行的。 另外 ,編譯器雖然能確定出方法的重載版本,但在很多情況下這個重載版本并不 是“唯一的” ,往往只能確定一個“更加合適 的”版本。 動態(tài)分派的一個最直接的例子是重寫。對于重寫,我們已經(jīng)很熟悉了,那么Java虛擬機是如何在程序運行期間確定方法的執(zhí) 行版本的呢?

解釋這個現(xiàn)象,就不得不涉及Java虛擬機的invokevirtual指令了,這個指令的解析過程有助于我們更深刻理解重寫的本質(zhì)。 該指令的具體解析過程如下:
找到操作數(shù)棧棧頂?shù)牡谝粋€元素所指向的對象的實際類型,記為C 如果在類型C中找到與常量中描述符和簡單名稱都相符的方法,則進行訪問權(quán)限的校驗,如果通過則返回這個方法的直接引 用,查找結(jié)束;如果不通過,則返回非法訪問異常 如果在類型C中沒有找到,則按照繼承關(guān)系從下到上依次對C的各個父類進行第2步的搜索和驗證過程 如果始終沒有找到合適的方法,則拋出抽象方法錯誤的異常。
從這個過程可以發(fā)現(xiàn),在第一步的時候就在運行期確定接收對象(執(zhí)行方法的所有者程稱為接受者)的實際類型,所以當調(diào) 用invokevirtual指令就會把運行時常量池中符號引用解析為不同的直接引用,這就是方法重寫的本質(zhì)。

3.虛擬機動態(tài)分派的實現(xiàn) 其實上面的敘述已經(jīng)把虛擬機重寫與重載的本質(zhì)講清楚了,那么Java虛擬機是如何做到這點的呢?

由于動態(tài)分派是非常頻繁的操作,實際實現(xiàn)中不可能真正如此實現(xiàn)。Java虛擬機是通過“穩(wěn)定優(yōu)化”的手段——在方法區(qū)中建 立一個虛方法表(Virtual Method Table),通過使用方法表的索引來代替元數(shù)據(jù)查找以提高性能。虛方法表中存放著各個方 法的實際入口地址(由于Java虛擬機自己建立并維護的方法表,所以沒有必要使用符號引用,那不是跟自己過不去嘛),如 果子類沒有覆蓋父類的方法,那么子類的虛方法表里面的地址入口與父類是一致的;如果重寫父類的方法,那么子類的方法 表的地址將會替換為子類實現(xiàn)版本的地址。

方法表是在類加載的連接階段(驗證、準備、解析)進行初始化,準備了子類的初始化值后,虛擬機會把該類的虛方法表也 進行初始化。

ClassLoader

什么是ClassLoader

這里有甲骨文公司對ClassLoader的解釋,這里我們摘取一些我認為重要的內(nèi)容:

A class loader is an object that is responsible for loading classes.
類加載器是一個負責加載類的對象。

  • 這里Load的不一定是本地文件,也可能是從網(wǎng)絡(luò)或者其他地方的一個二進制流。

Every Class object contains a reference to the ClassLoader that defined it.
每個Class對象都包含對定義它的ClassLoader的引用。

  • xxx.class.getClassLoader()
  • ClassLoader不一定是Java類,對于Java類型的ClassLoader,引用指向他的父ClassLoader。第一個ClassLoader是個C++對象;

The ClassLoader class uses a delegation model to search for classes and resources.
ClassLoader類使用委派模型來搜索類和資源。

classloader01.png
  • Bootstrap ClassLoader:最頂層的加載類,主要加載核心類庫,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等;
  • Extention ClassLoader:擴展的類加載器,加載目錄%JRE_HOME%\lib\ext目錄下的jar包和class文件;
  • Appclass Loader:加載當前應(yīng)用的classpath的所有類;
  • JVM加載一個class時先查看是否已經(jīng)加載過,沒有則通過父加載器,然后遞歸下去,直到BootstrapClassLoader,如果BootstrapClassloader找到了,直接返回,如果沒有找到,則一級一級返回(查看規(guī)定加載路徑),最后到達自身去查找這些對象。這種機制就叫做雙親委托。(一會兒我們還會講到)

類加載

  1. 什么是類加載?
    虛擬機把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進行校驗,轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機直接使用的JAVA類型,這就是虛擬機的類加載機制。
    classloader02.png

    假設(shè)有一個類內(nèi)部有一行靜態(tài)代碼public static int value = 123;
  • 加載:1.通過一個類的全限定名來獲取定義此類的二進制文件;2.將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu);3.在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為這個類的各種數(shù)據(jù)的訪問入口
  • 驗證:魔數(shù),java版本……
  • 準備:正式為類變量分配內(nèi)存,并設(shè)置初始值 value = 0。
  • 解析:符號引用變?yōu)橹苯右?。
  • 初始化:value = 123。
  1. 什么時候類加載?
    Java虛擬機規(guī)范規(guī)定了類的生命周期和類初始化的時機。其中加載到初始化是我們說的類加載的最后一步:
  2. 使用new關(guān)鍵字實例化對象的時候、讀取或設(shè)置一個類的靜態(tài)字段的時候,已經(jīng)調(diào)用一個類的靜態(tài)方法的時候。
  3. 使用java.lang.reflect包的方法對類進行反射調(diào)用的時候,如果類沒有初始化,則需要先觸發(fā)其初始化。
  4. 當初始化一個類的時候,如果發(fā)現(xiàn)其父類沒有被初始化就會先初始化它的父類。
  5. 當虛擬機啟動的時候,用戶需要指定一個要執(zhí)行的主類(就是包含main()方法的那個類),虛擬機會先初始化這個類;
  6. 使用Jdk1.7動態(tài)語言支持的時候的一些情況。

類加載器與類

類加載器雖然只用于實現(xiàn)類的加載動作,但它在Java程序中起到的作用卻遠遠不限于類加載階段。
對于任意一個類,都需要加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性。 這句話可以表達的更通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類是來源于同一個Class文件,只要加載它們的類加載器不同,那這兩個類就必定不相等。

ClassLoader cl = newLoader();
Class<?> cls = cl.loadClass("chenhao.sun.LeetCode");
Class<?> cls1 = cl.loadClass("chenhao.sun.LeetCode");
System.out.println(cls.equals(cls1));

ClassLoader cl2 = newLoader();
Class<?> cls2 = cl2.loadClass("chenhao.sun.LeetCode");
System.out.println(cls.equals(cls2));

對于cls1,在load的時候會先去查找是否已經(jīng)load出來這個類,如果已經(jīng)load出來則直接使用,所以第一個結(jié)果為真
上面的cl和cl2分別是兩個不同的classloader,雖然cl已經(jīng)load過一次LeetCode類,但cl2并沒有,所以cl2需要再load一次,所以第二個為假。(類的唯一性,依賴于ClassLoader,所以統(tǒng)一進程下,同一個類文件的靜態(tài)變量也可能不同(單例不單)

雙親委派模型

classloader01.png

雙親委派模型的工作過程是:如果一個類加載器(當前類的加載器)收到了類加載請求,它首先不會自己去加載這個類,而是將這個類委托給父加載器去完成,每一層加載器都是如此,因此所有加載請求最終應(yīng)該傳送到頂層的啟動類加載器中,只有父加載器反饋無法加載時,子加載器才會自己嘗試去加載。

  • BoostStrap實際上是Java虛擬機的一部分,他負責加載Java中最基礎(chǔ)的一些類如Object.class
  • BoostStrap又加載出ExtensionClassLoader。
  • ExtensionClassLoader會加載一些擴展的類比如提供一些加密算法。
  • Extension加載出AppClassLoader,自此我們可以執(zhí)行我們的入口main()方法了

為什么要采用雙親委派模型?

  1. 不同的ClassLoader加載不同位置的class文件
  2. 采用雙親委派模型確保的一條線上的類不被重復(fù)加載(比如Object類不會有多個)

雙親委托模型并不是強制的(實際上有更復(fù)雜的網(wǎng)狀模型),但在Android上我們可以認為就是雙親委派模型。

ClassLoader花樣

  • 熱部署、插件化……
  • 減少包大小……,一個dex方法不能超過65535,補丁……
  • Google MultiDex
    360 DroidPlugin
    阿里 AndFix
    ……
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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