JVM 類加載概述
來源:SegmentFault 思否社區(qū)
作者:又壞又迷人
JVM簡(jiǎn)介
JVM是Java Virtual Machine(Java虛擬機(jī))的縮寫,JVM是一種用于計(jì)算設(shè)備的規(guī)范,它是一個(gè)虛構(gòu)出來的計(jì)算機(jī),是通過在實(shí)際的計(jì)算機(jī)上仿真模擬各種計(jì)算機(jī)功能來實(shí)現(xiàn)的。Java虛擬機(jī)包括一套字節(jié)碼指令集、一組寄存器、一個(gè)棧、一個(gè)垃圾回收堆和一個(gè)存儲(chǔ)方法域。JVM屏蔽了與具體操作系統(tǒng)平臺(tái)相關(guān)的信息,使Java程序只需生成在Java虛擬機(jī)上運(yùn)行的目標(biāo)代碼(字節(jié)碼),就可以在多種平臺(tái)上不加修改地運(yùn)行。JVM在執(zhí)行字節(jié)碼時(shí),實(shí)際上最終還是把字節(jié)碼解釋成具體平臺(tái)上的機(jī)器指令執(zhí)行。
Java語(yǔ)言的一個(gè)非常重要的特點(diǎn)就是與平臺(tái)的無關(guān)性。而使用Java虛擬機(jī)是實(shí)現(xiàn)這一特點(diǎn)的關(guān)鍵。一般的高級(jí)語(yǔ)言如果要在不同的平臺(tái)上運(yùn)行,至少需要編譯成不同的目標(biāo)代碼。而引入Java語(yǔ)言虛擬機(jī)后,Java語(yǔ)言在不同平臺(tái)上運(yùn)行時(shí)不需要重新編譯。Java語(yǔ)言使用Java虛擬機(jī)屏蔽了與具體平臺(tái)相關(guān)的信息,使得Java語(yǔ)言編譯程序只需生成在Java虛擬機(jī)上運(yùn)行的目標(biāo)代碼(字節(jié)碼),就可以在多種平臺(tái)上不加修改地運(yùn)行。Java虛擬機(jī)在執(zhí)行字節(jié)碼時(shí),把字節(jié)碼解釋成具體平臺(tái)上的機(jī)器指令執(zhí)行。這就是Java的能夠“一次編譯,到處運(yùn)行”的原因。
內(nèi)存結(jié)構(gòu)概述


類加載子系統(tǒng)(Class Loader)
類加載器分為:自定義類加載器 < 系統(tǒng)類加載器 < 擴(kuò)展類加載器 < 引導(dǎo)類加載器
類加載過程分為:加載、鏈接、驗(yàn)證、初始化。
程序計(jì)數(shù)器(Program Counter Register)
是一塊較小的內(nèi)存空間,可以看作是當(dāng)前線程所執(zhí)行字節(jié)碼的行號(hào)指示器,指向下一個(gè)將要執(zhí)行的指令代碼,由執(zhí)行引擎來讀取下一條指令。
虛擬機(jī)棧 (Stack Area)
棧是線程私有,棧幀是棧的元素。每個(gè)方法在執(zhí)行時(shí)都會(huì)創(chuàng)建一個(gè)棧幀。棧幀中存儲(chǔ)了局部變量表、操作數(shù)棧、動(dòng)態(tài)連接和方法出口等信息。每個(gè)方法從調(diào)用到運(yùn)行結(jié)束的過程,就對(duì)應(yīng)著一個(gè)棧幀在棧中壓棧到出棧的過程。
本地方法棧 (Native Method Area)
JVM 中的棧包括 Java 虛擬機(jī)棧和本地方法棧,兩者的區(qū)別就是,Java 虛擬機(jī)棧為 JVM 執(zhí)行 Java 方法服務(wù),本地方法棧則為 JVM 使用到的 Native 方法服務(wù)。
堆 (Heap Area)
堆是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊存儲(chǔ)區(qū)域。堆內(nèi)存被所有線程共享。主要存放使用new關(guān)鍵字創(chuàng)建的對(duì)象。所有對(duì)象實(shí)例以及數(shù)組都要在堆上分配。垃圾收集器就是根據(jù)GC算法,收集堆上對(duì)象所占用的內(nèi)存空間。
Java堆分為年輕代(Young Generation)和老年代(Old Generation);年輕代又分為伊甸園(Eden)和幸存區(qū)(Survivor區(qū));幸存區(qū)又分為From Survivor空間和 To Survivor空間。
方法區(qū)(Method Area)
方法區(qū)同 Java 堆一樣是被所有線程共享的區(qū)間,用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼。更具體的說,靜態(tài)變量+常量+類信息(版本、方法、字段等)+運(yùn)行時(shí)常量池存在方法區(qū)中。常量池是方法區(qū)的一部分。
JDK 8 使用元空間 MetaSpace 代替方法區(qū),元空間并不在JVM中,而是在本地內(nèi)存中
類加載過程概述
類加載器子系統(tǒng)負(fù)責(zé)從文件系統(tǒng)或者網(wǎng)絡(luò)中在家Class文件,class文件在文件開頭又特定的文件標(biāo)識(shí)。
ClassLoader只負(fù)責(zé)class文件的加載,至于它是否可以運(yùn)行,則由ExecutionEngine決定。
加載類的信息存放于一塊被稱為方法區(qū)的內(nèi)存空間。除了類的信息外,方法區(qū)中還會(huì)存放運(yùn)行時(shí)常量池信息,可能還包括字符串字面量和數(shù)字常量(這部分常量信息是Class文件中常量池部分的內(nèi)存映射)
類加載器ClassLoader角色

class文件存在本地硬盤上,在執(zhí)行時(shí)加載到JVM中,根據(jù)這個(gè)文件可以實(shí)例化出n個(gè)一模一樣的實(shí)例。 class文件加載到JVM中,被稱為DNA元數(shù)據(jù)模板,放在方法區(qū)中。 在.class文件 -> JVM -> 最終成為元數(shù)據(jù)模板的過程中,ClassLoader就扮演一個(gè)快遞員的角色。
類加載過程概述



類的加載過程大致分為三個(gè)階段:加載,鏈接,初始化。
類的加載過程一:加載(Loading)
通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu) 在內(nèi)存中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口
類的加載過程二:鏈接(Linking)
驗(yàn)證(Verify)
目的在于確保Class文件的字節(jié)流中包含信息符合當(dāng)前虛擬機(jī)要求,保證被加載類的正確性,不會(huì)危害到虛擬機(jī)的安全。
準(zhǔn)備(Prepare)
準(zhǔn)備階段是進(jìn)行內(nèi)存分配。為類變量也就是類中由static修飾的變量分配內(nèi)存,并且設(shè)置初始值,這里要注意,初始值是默認(rèn)初始值0、null、0.0、false等,而不是代碼中設(shè)置的具體值,代碼中設(shè)置的值是在初始化階段完成的。另外這里也不包含用final修飾的靜態(tài)變量,因?yàn)閒inal在編譯的時(shí)候就會(huì)分配了。這里不會(huì)為實(shí)例變量分配初始化,類變量會(huì)分配在方法區(qū)中,而實(shí)例對(duì)象會(huì)隨著對(duì)象一起分配到Java堆中。
public class HelloApp {private static int a = 1; // 準(zhǔn)備階段為0,而不是1public static void main(String[] args) {System.out.println(a);}}
解析(Resolve)
解析主要是解析字段、接口、方法。主要是將常量池中的符號(hào)引用替換為直接引用的過程。直接引用就是直接指向目標(biāo)的指針、相對(duì)偏移量等。
類的加載過程三:初始化(initialization)
初始化階段就是執(zhí)行類構(gòu)造器方法 ()的過程 此方法不需要定義,是javac編譯期自動(dòng)收集類中所有類變量的賦值動(dòng)作和靜態(tài)代碼塊中的語(yǔ)句合并而來 構(gòu)造器方法中指令按語(yǔ)句在源文件中出現(xiàn)的順序執(zhí)行。 ()不同于類的構(gòu)造器(構(gòu)造器是虛擬機(jī)視角下的 ()) 若該類具有父類,JVM會(huì)保證子類的 ()執(zhí)行前,父類的 ()已經(jīng)執(zhí)行完畢 虛擬機(jī)必須保證一個(gè)類的 ()方法在多線程下被同步加鎖
需要注意,如果沒有定義靜態(tài)變量或靜態(tài)代碼塊的話則沒有
案例如下:
public class HelloApp {static {code = 20;}private static int code = 10;//第一步:在準(zhǔn)備階段初始化了code默認(rèn)值為0。//第二步:根據(jù)類執(zhí)行順序先執(zhí)行靜態(tài)代碼塊,賦值為20.//第三步:最后賦值為10,輸出結(jié)果為10.public static void main(String[] args) {System.out.println(code); // 10}}
通過字節(jié)碼文件可以很清楚的看到結(jié)果:
0 bipush 202 putstatic #35 bipush 107 putstatic #310 return
先被賦值為20,然后改為10。
類加載器概述
JVM支持兩種類型的類加載器,分別為引導(dǎo)類加載器(Bootstrap ClassLoader)?和?自定義類加載器(User-Defined ClassLoader)
從概念上講,自定義類加載器一般指的是程序中由開發(fā)人員自定義的一類類加載器,但是Java虛擬機(jī)是將所有派生于抽象類ClassLoader的類加載器都劃分為自定義類加載器
無論怎么劃分,在程序中最常見的類加載器始終只有三個(gè):
系統(tǒng)類加載器(System Class Loader) < 擴(kuò)展類加載器(Extension Class Loader) < 引導(dǎo)類加載器(Bootstrap Class Loader)
它們之間的關(guān)系不是繼承關(guān)系,而是level關(guān)系。

系統(tǒng)類加載器和擴(kuò)展類加載器間接或直接繼承ClassLoader。劃線分為兩大類。

public class HelloApp {public static void main(String[] args) {//獲取系統(tǒng)類加載器ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2//獲取其上層:擴(kuò)展類加載器ClassLoader extClassLoader = systemClassLoader.getParent();System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@60e53b93//獲取其上層:獲取不到引導(dǎo)類加載器ClassLoader bootStrapLoader = extClassLoader.getParent();System.out.println(bootStrapLoader);//null//我們自己定義的類是由什么類加載器加載的:使用系統(tǒng)類加載器ClassLoader classLoader = HelloApp.class.getClassLoader();System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2//看看String是由什么類加載器加載的:使用引導(dǎo)類加載器ClassLoader classLoaderString = String.class.getClassLoader();System.out.println(classLoaderString);//null}}
引導(dǎo)類加載器(Bootstrap ClassLoader)
這個(gè)類加載使用c/c++語(yǔ)言實(shí)現(xiàn),嵌套在JVM內(nèi)部。 他用來加載Java的核心庫(kù),(JAVA_HOME/jre/lib/rt.jar、resources.jar、或sun.boot.class.path路徑下的內(nèi)容),用于提供JVM自身需要的類。 并不繼承自java.lang.ClassLoader ,沒有父加載器。 加載擴(kuò)展類和應(yīng)用程序類加載器,并指定為他們的父類加載器。 出于安全考慮,Bootstrap啟動(dòng)類加載器只加載包名為java、javax、sun等開頭的類。
擴(kuò)展類加載器(Extension ClassLoader)
Java語(yǔ)言編寫,由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn)。 派生于ClassLoader類。 上一層類加載器為啟動(dòng)類加載器。 從java.ext.dirs系統(tǒng)屬性所指定的目標(biāo)中加載類庫(kù),或從JDK的安裝目錄jre/lib/ext子目錄(擴(kuò)展目錄)下加載類庫(kù)。如果用戶創(chuàng)建的Jar放在此目錄下,也會(huì)自動(dòng)由擴(kuò)展類加載器加載。
系統(tǒng)類加載器(System ClassLoader)
Java語(yǔ)言編寫,由sun.misc.Launcher$AppClassLoader實(shí)現(xiàn)。 派生于ClassLoader類。 上一層類加載器為擴(kuò)展類加載器。 它負(fù)責(zé)加載環(huán)境變量classpath或系統(tǒng)屬性 java.class.path指定下的類庫(kù)。 該類加載是程序中默認(rèn)的類加載器,一般來說,Java應(yīng)用的類都有由它來完成加載。 通過ClassLoader.getSystemClassLoader()方法可以獲取該類加載器。
為什么需要用戶自定義類加載器?
隔離加載類 修改類加載的方式 擴(kuò)展加載源 防止源碼泄露
用戶自定義類加載器實(shí)現(xiàn)步驟
通過集成抽象類java.lang.ClassLoader類的方式,實(shí)現(xiàn)自己的類加載器。 在JDK1.2之前,在自定義類加載器時(shí),總會(huì)去繼承ClassLoader類并重寫loadClass()方法,從而實(shí)現(xiàn)自定義的類加載器,但是在JDK1.2之后不再建議用戶去覆蓋loadClass()方法,而是建議把自定義類加載邏輯寫在findClass()方法中。 在編寫自定義類加載器時(shí),如果沒有太過于復(fù)雜的需求,可以直接繼承URLClassLoader類,這樣就可以避免自己編寫findClass()方法其獲取字節(jié)碼流的方式,使得自定義加載器編寫更為簡(jiǎn)潔。
關(guān)于ClassLoader
ClassLoader是一個(gè)抽象類,系統(tǒng)類加載器和擴(kuò)展類加載器間接或直接繼承ClassLoader。
常用方法如下:

雙親委派機(jī)制
Java虛擬機(jī)對(duì)class文件采用的是按需加載的方式,也就是說需要使用該類的時(shí)候才會(huì)將它的class文件加載到內(nèi)存生成class對(duì)象。
當(dāng)某個(gè)類加載器需要加載某個(gè).class文件時(shí),它首先把這個(gè)任務(wù)委托給他的上級(jí)類加載器,遞歸這個(gè)操作,如果上級(jí)的類加載器沒有加載,自己才會(huì)去加載這個(gè)類。
工作原理
如果一個(gè)類加載器收到了類加載請(qǐng)求,他并不會(huì)自己先去加載,而是把這個(gè)請(qǐng)求向上委托給上一級(jí)類加載器去執(zhí)行。 如果上一級(jí)類加載器還存在上一級(jí),則進(jìn)一步向上委托,依次遞歸,請(qǐng)求最終會(huì)達(dá)到引導(dǎo)類加載器。 如果引導(dǎo)類加載器可以完成類加載任務(wù),就成功返回。如果無法完成類加載任務(wù),會(huì)依次往下再去執(zhí)行加載任務(wù)。這就是雙親委派機(jī)制。
比如我們現(xiàn)在在自己項(xiàng)目中創(chuàng)建一個(gè)包名java.lang下創(chuàng)建一個(gè)String類。
package java.lang;public class String {static {System.out.println("我是自己創(chuàng)建的String");}}
public class HelloApp {public static void main(String[] args) {String s = new String();}}
執(zhí)行之后并不會(huì)輸出我們的語(yǔ)句,因?yàn)槲覀兊腟tring類加載器一開始由系統(tǒng)類加載器一級(jí)一級(jí)往上委托,最終交給引導(dǎo)類加載器,引導(dǎo)類加載器一看是java.lang包下的,ok,我來執(zhí)行,最終執(zhí)行的并不是我們自己創(chuàng)建String類,保證了核心api無法被纂改,避免類的重復(fù)加載。
package java.lang;public class String {static {System.out.println("我是自己創(chuàng)建的String");}public static void main(String[] args) {System.out.println("Hello World !!!");}}
如果我們想運(yùn)行如上代碼,我們會(huì)得到如下錯(cuò)誤:
錯(cuò)誤: 在類 java.lang.String 中找不到 main 方法, 請(qǐng)將 main 方法定義為:public static void main(String[] args)否則 JavaFX 應(yīng)用程序類必須擴(kuò)展javafx.application.Application
因?yàn)槲覀冎涝诤诵腶pi String類中是沒有main方法的,所以我們可以確定加載的并不是我們自己創(chuàng)建的String類。
在JVM中表示兩個(gè)Class對(duì)象是否為同一個(gè)類存在的必要條件:
類的完整類名必須一致,包括包名。 加載這個(gè)類的ClassLoader也必須相同。
順便說一句,我們包名如果為java.lang則會(huì)報(bào)錯(cuò)。

