2018-05-24 11:45:21 1208瀏覽
類(lèi)裝載器子系統(tǒng)
在JVM中負(fù)責(zé)裝載.class文件(一種8位二進(jìn)制流文件,各個(gè)數(shù)據(jù)項(xiàng)按順序緊密的從前向后排列, 相鄰的項(xiàng)之間沒(méi)有間隙,經(jīng)編譯器編譯.java源文件后生成,每個(gè)類(lèi)(或者接口)都單獨(dú)占有一個(gè)class文件)。
運(yùn)行時(shí)數(shù)據(jù)區(qū)
1 方法區(qū)
當(dāng)JVM使用類(lèi)裝載器定位class文件,并將其輸入到內(nèi)存中時(shí)。會(huì)提取class文件的類(lèi)型信息,并將這些信息存儲(chǔ)到方法區(qū)中。同時(shí)放入方法區(qū)中的還有該類(lèi)型中的類(lèi)靜態(tài)變量。
該類(lèi)型的全限定名。如java.io.FileOutputStream
該類(lèi)型的直接超類(lèi)的全限定名。如java.io.OutputStream
該類(lèi)型是類(lèi)類(lèi)型還是接口類(lèi)型。
該類(lèi)型的訪(fǎng)問(wèn)修飾符(public、abstract、final)。
任何直接超接口的全限定名的有序列表。如java.io.Closeable, java.io.Flushable。
該類(lèi)型的常量池。比如所有類(lèi)型(Class)、方法、字段的符號(hào)、######基本數(shù)據(jù)類(lèi)型的直接數(shù)值(final)等。
字段信息:對(duì)類(lèi)型中聲明的每個(gè)字段。
方法信息。
類(lèi)靜態(tài)變量:靜態(tài)變量而不是放在堆里面,所以靜態(tài)屬于類(lèi),不屬于對(duì)象。
指向ClassLoader類(lèi)的引用。
指向Class類(lèi)的引用。
方法表 為了能快速定位到類(lèi)型中的某個(gè)方法,JVM對(duì)每個(gè)裝載的類(lèi)型都會(huì)建立一個(gè)方法表,用于存儲(chǔ)該類(lèi)型對(duì)象可以調(diào)用的方法的直接引用,這些方法就包括從超類(lèi)中繼承來(lái)的。而這張表與Java動(dòng)態(tài)綁定機(jī)制的實(shí)現(xiàn)是密切相關(guān)的。
常量池
常量池指的是在編譯期被確定,并被保存在已編譯的.class文件中的一些數(shù)據(jù)。除了包含代碼中所定義的各種基本數(shù)據(jù)類(lèi)型和對(duì)象型(String及數(shù)組)的常量值(final,在編譯時(shí)確定,并且編譯器會(huì)優(yōu)化)還包含一些以文本形式出現(xiàn)的符號(hào)引用(類(lèi)信息),比如:
類(lèi)和接口的全限定名
字段的名稱(chēng)和描述符
虛擬機(jī)必須給每個(gè)被裝載的類(lèi)型維護(hù)一個(gè)常量池。
常量池就是該類(lèi)型所用到常量的一個(gè)有序集合,包括直接常量(string、integer等)和其他類(lèi)型,字段和方法的符號(hào)引用。
方法區(qū)是多線(xiàn)程共享的
也就是當(dāng)虛擬機(jī)實(shí)例開(kāi)始運(yùn)行程序時(shí),邊運(yùn)行邊加載進(jìn)class文件。不同的Class文件都會(huì)提取出不同類(lèi)型信息存放在方法區(qū)中。同樣,方法區(qū)中不再需要運(yùn)行的類(lèi)型信息會(huì)被垃圾回收線(xiàn)程丟棄掉。
堆內(nèi)存
Java 程序在運(yùn)行時(shí)創(chuàng)建的所有類(lèi)型對(duì)象和數(shù)組都存儲(chǔ)在堆中。JVM會(huì)根據(jù)new指令在堆中開(kāi)辟一個(gè)確定類(lèi)型的對(duì)象內(nèi)存空間。但是堆中開(kāi)辟對(duì)象的空間并沒(méi)有任何人工指令可以回收,而是通過(guò)JVM的垃圾回收器負(fù)責(zé)回收。
堆中對(duì)象存儲(chǔ)的是該對(duì)象以及對(duì)象所有超類(lèi)的實(shí)例數(shù)據(jù)(但不是靜態(tài)數(shù)據(jù))。
其中一個(gè)對(duì)象的引用可能在整個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)中的很多地方存在,比如Java棧,堆,方法區(qū)等。
堆中對(duì)象還應(yīng)該關(guān)聯(lián)一個(gè)對(duì)象的鎖數(shù)據(jù)信息以及線(xiàn)程的等待集合(線(xiàn)程等待池)。這些都是實(shí)現(xiàn)Java線(xiàn)程同步機(jī)制的基礎(chǔ)。
java中數(shù)組也是對(duì)象,那么自然在堆中會(huì)存儲(chǔ)數(shù)組的信息。
程序計(jì)數(shù)器
對(duì)于一個(gè)運(yùn)行的Java而言,每一個(gè)線(xiàn)程都有一個(gè)PC寄存器。當(dāng)線(xiàn)程執(zhí)行Java程序時(shí),PC寄存器的內(nèi)容總是下一條將被執(zhí)行的指令地址。
Java棧
每啟動(dòng)一個(gè)線(xiàn)程,JVM都會(huì)為它分配一個(gè)Java棧,用于存放方法中的局部變量,操作數(shù)以及異常數(shù)據(jù)等。當(dāng)線(xiàn)程調(diào)用某個(gè)方法時(shí),JVM會(huì)根據(jù)方法區(qū)中該方法的字節(jié)碼組建一個(gè)棧幀。并將該棧幀壓入Java棧中,方法執(zhí)行完畢時(shí),JVM會(huì)彈出該棧幀并釋放掉。
注意:Java棧中的數(shù)據(jù)是線(xiàn)程私有的,一個(gè)線(xiàn)程是無(wú)法訪(fǎng)問(wèn)另一個(gè)線(xiàn)程的Java棧的數(shù)據(jù)。這也就是為什么多線(xiàn)程編程時(shí),兩個(gè)相同線(xiàn)程執(zhí)行同一方法時(shí),對(duì)方法內(nèi)的局部變量是不需要數(shù)據(jù)同步的原因。
成員變量有默認(rèn)值(被final修飾且沒(méi)有static的必須顯式賦值),局部變量不會(huì)自動(dòng)賦值。
執(zhí)行引擎
運(yùn)行Java的每一個(gè)線(xiàn)程都是一個(gè)獨(dú)立的虛擬機(jī)執(zhí)行引擎的實(shí)例。從線(xiàn)程生命周期的開(kāi)始到結(jié)束,他要么在執(zhí)行字節(jié)碼,要么在執(zhí)行本地方法。一個(gè)線(xiàn)程可能通過(guò)解釋或者使用芯片級(jí)指令直接執(zhí)行字節(jié)碼,或者間接通過(guò)JIT(即時(shí)編譯器)執(zhí)行編譯過(guò)的本地代碼
注意:JVM是進(jìn)程級(jí)別,執(zhí)行引擎是線(xiàn)程級(jí)別。
指令集
實(shí)際上,class文件中方法的字節(jié)碼流就是有JVM的指令序列構(gòu)成的。每一條指令包含一個(gè)單字節(jié)的操作碼,后面跟隨0個(gè)或多個(gè)操作數(shù)。
指令由一個(gè)操作碼和零個(gè)或多個(gè)操作數(shù)組成。
iload_0 // 把存儲(chǔ)在局部變量區(qū)中索引為0的整數(shù)壓入操作數(shù)棧。iload_1 // 把存儲(chǔ)在局部變量區(qū)中索引為1的整數(shù)壓入操作數(shù)棧。iadd // 從操作數(shù)棧中彈出兩個(gè)整數(shù)相加,在將結(jié)果壓入操作數(shù)棧。 istore_2 // 從操作數(shù)棧中彈出結(jié)果
很顯然,上面的指令反復(fù)用到了Java棧中的某一個(gè)方法棧幀。實(shí)際上執(zhí)行引擎運(yùn)行Java字節(jié)碼指令很多時(shí)候都是在不停的操作Java棧,也有的時(shí)候需要在堆中開(kāi)辟對(duì)象以及運(yùn)行系統(tǒng)的本地指令等。但是Java棧的操作要比堆中的操作要快的多,因此反復(fù)開(kāi)辟對(duì)象是非常耗時(shí)的。這也是為什么Java程序優(yōu)化的時(shí)候,盡量減少new對(duì)象。
示例分析:
//源代碼 Test.java package edu.hr.jvm; import edu.hr.jvm.bean; publicclassTest{ publicstaticvoidmain(String[] args) { Act act=new Act(); act.doMathForever(); } } //源代碼 Act.java package edu.hr.jvm.bean; publicclassAct{ publicvoiddoMathForever() { int i=0; for(;;){ i+=1; i*=2; } } }
首先OS會(huì)創(chuàng)建一個(gè)JVM實(shí)例(進(jìn)行必要的初始化工作,比如:初始啟動(dòng)類(lèi)裝載器,初始運(yùn)行時(shí)內(nèi)存數(shù)據(jù)區(qū)等。
然后通過(guò)自定義類(lèi)裝載器加載Test.class。并提取Test.class字節(jié)碼中的信息存放在方法區(qū) 中(具體的信息在上面已經(jīng)講過(guò))。上圖展示了方法區(qū)中的Test類(lèi)信息,其中在常量池中有一個(gè)符號(hào)引用“Act”(類(lèi)的全限定名,注意:這個(gè)引用目前還沒(méi)有真正的類(lèi)信息的內(nèi)存地址)。
接著JVM開(kāi)始從Test類(lèi)的main字節(jié)碼處開(kāi)始解釋執(zhí)行。在運(yùn)行之前,會(huì)在Java棧中組建一個(gè)main方法的棧幀 ,如上圖Java棧所示。JVM需要運(yùn)行任何方法前,通過(guò)在Java棧中壓入一個(gè)幀棧。在這個(gè)幀棧的內(nèi)存區(qū)域中進(jìn)行計(jì)算。
現(xiàn)在可以開(kāi)始執(zhí)行main方法的第一條指令 —— JVM需要為常量池的第一項(xiàng)的類(lèi)(符號(hào)引用Act)分配內(nèi)存空間。但是Act類(lèi)此時(shí)還沒(méi)有加載進(jìn)JVM(因?yàn)槌A砍啬壳爸挥幸粋€(gè)“Act”的符號(hào)引用)。
JVM加載進(jìn)Act.class,并提取Act類(lèi)信息放入方法區(qū)中。然后以一個(gè)直接指向方法區(qū)Act類(lèi)信息的直接引用(在棧中)換開(kāi)始在常量池中的符號(hào)引用“Act”,這個(gè)過(guò)程就是常量池解析 。以后就可以直接訪(fǎng)問(wèn)Act的類(lèi)信息了。
此時(shí)JVM可以根據(jù)方法區(qū)中的Act類(lèi)信息,在堆中開(kāi)辟一個(gè)Act類(lèi)對(duì)象act。
接著開(kāi)始執(zhí)行main方法中的第二條指令調(diào)用doMathForever方法。這個(gè)可以通過(guò)堆中act對(duì)象所指的方法表中查找,然后定位到方法區(qū)中的Act類(lèi)信息中的doMathForever方法字節(jié)碼。在運(yùn)行之前,仍然要組建一個(gè)doMathForever棧幀壓入Java棧。(注意:JVM會(huì)根據(jù)方法區(qū)中doMathForever的字節(jié)碼來(lái)創(chuàng)建棧幀的局部變量區(qū)和操作數(shù)棧的大?。?/span>
接下來(lái)JVM開(kāi)始解釋運(yùn)行Act.doMathForever字節(jié)碼的內(nèi)容了。
編譯和運(yùn)行過(guò)程
編譯:源碼要運(yùn)行,必須先轉(zhuǎn)成二進(jìn)制的機(jī)器碼。這是編譯器的任務(wù)。
源文件由編譯器編譯成字節(jié)碼。 創(chuàng)建完源文件之后,程序會(huì)先被編譯為.class文件。Java編譯一個(gè)類(lèi)時(shí),如果這個(gè)類(lèi)所依賴(lài)的類(lèi)還沒(méi)有被編譯,編譯器就會(huì)先編譯這個(gè)被依賴(lài)的類(lèi),然后引用,否則直接引用。如果java編譯器在指定目錄下找不到該類(lèi)所其依賴(lài)的類(lèi)的.class文件或者.java源文件的話(huà),編譯器話(huà)報(bào)“cant find symbol”的錯(cuò)誤。編譯后的字節(jié)碼文件格式主要分為兩部分:常量池和方法字節(jié)碼。常量池記錄的是代碼出現(xiàn)過(guò)的所有token(類(lèi)名,成員變量名等等)以及符號(hào)引用(方法引用,成員變量引用等等);方法字節(jié)碼放的是類(lèi)中各個(gè)方法的字節(jié)碼。運(yùn)行:java類(lèi)運(yùn)行的過(guò)程大概可分為兩個(gè)過(guò)程:類(lèi)的加載,類(lèi)的執(zhí)行。需要說(shuō)明的是:JVM主要在程序第一次主動(dòng)使用類(lèi)的時(shí)候,才會(huì)去加載該類(lèi)。也就是說(shuō),JVM并不是在一開(kāi)始就把一個(gè)程序就所有的類(lèi)都加載到內(nèi)存中,而是到不得不用的時(shí)候才把它加載進(jìn)來(lái),而且只加載一次。
下面是程序運(yùn)行的詳細(xì)步驟:
//MainApp.java publicclassMainApp { publicstaticvoidmain(String[] args) { Animal animal = new Animal("Puppy"); animal.printName(); } } //Animal.java publicclassAnimal {public String name; publicAnimal(String name) { this.name = name; } publicvoidprintName(){ System.out.println("Animal ["+name+"]"); } }
在編譯好java程序得到MainApp.class文件后,在命令行上敲java AppMain。系統(tǒng)就會(huì)啟動(dòng)一個(gè)jvm進(jìn)程,jvm進(jìn)程從classpath路徑中找到一個(gè)名為AppMain.class的二進(jìn)制文件,將MainApp的類(lèi)信息加載到運(yùn)行時(shí)數(shù)據(jù)區(qū)的方法區(qū)內(nèi),這個(gè)過(guò)程叫做MainApp類(lèi)的加載。然后JVM找到AppMain的主函數(shù)入口,開(kāi)始執(zhí)行main函數(shù)。main函數(shù)的第一條命令是Animal animal = new Animal("Puppy");就是讓JVM創(chuàng)建一個(gè)Animal對(duì)象,但是這時(shí)候方法區(qū)中沒(méi)有Animal類(lèi)的信息,所以JVM馬上加載Animal類(lèi),把Animal類(lèi)的類(lèi)型信息放到方法區(qū)中。加載完Animal類(lèi)之后,Java虛擬機(jī)做的第一件事情就是在堆區(qū)中為一個(gè)新的Animal實(shí)例分配內(nèi)存,然后調(diào)用構(gòu)造函數(shù)初始化Animal實(shí)例,這個(gè)Animal實(shí)例持有著指向方法區(qū)的Animal類(lèi)的類(lèi)型信息(其中包含有方法表,java動(dòng)態(tài)綁定的底層實(shí)現(xiàn))的引用。當(dāng)使用animal.printName()的時(shí)候,JVM根據(jù)animal引用找到Animal對(duì)象,然后根據(jù)Animal對(duì)象持有的引用定位到方法區(qū)中Animal類(lèi)的類(lèi)型信息的方法表,獲得printName()函數(shù)的字節(jié)碼的地址。開(kāi)始運(yùn)行printName()函數(shù)的字節(jié)碼(可以把字節(jié)碼理解為一條條的指令)。
特別說(shuō)明:java類(lèi)中所有public和protected的實(shí)例方法都采用動(dòng)態(tài)綁定機(jī)制,所有私有方法、靜態(tài)方法、構(gòu)造器及初始化方法<clinit>都是采用靜態(tài)綁定機(jī)制。而使用動(dòng)態(tài)綁定機(jī)制的時(shí)候會(huì)用到方法表,靜態(tài)綁定時(shí)并不會(huì)用到。
通過(guò)前面的兩個(gè)例子的分析,應(yīng)該理解了不少了吧。
類(lèi)加載機(jī)制
JVM主要包含三大核心部分:類(lèi)加載器,運(yùn)行時(shí)數(shù)據(jù)區(qū)和執(zhí)行引擎。
虛擬機(jī)將描述類(lèi)的數(shù)據(jù)從class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn),準(zhǔn)備,解析和初始化,最終就會(huì)形成可以被虛擬機(jī)使用的java類(lèi)型,這就是一個(gè)虛擬機(jī)的類(lèi)加載機(jī)制。java在類(lèi)中的類(lèi)是動(dòng)態(tài)加載的,只有在運(yùn)行期間使用到該類(lèi)的時(shí)候,才會(huì)將該類(lèi)加載到內(nèi)存中,java依賴(lài)于運(yùn)行期動(dòng)態(tài)加載和動(dòng)態(tài)鏈接來(lái)實(shí)現(xiàn)類(lèi)的動(dòng)態(tài)使用。
一個(gè)類(lèi)的生命周期:
加載,驗(yàn)證,準(zhǔn)備,初始化和卸載在開(kāi)始的順序上是固定的,但是可以交叉進(jìn)行。在Java中,對(duì)于類(lèi)有且僅有四種情況會(huì)對(duì)類(lèi)進(jìn)行“初始化”。
使用new關(guān)鍵字實(shí)例化對(duì)象的時(shí)候,讀取或設(shè)置一個(gè)類(lèi)的靜態(tài)字段時(shí)候(除final修飾的static外),調(diào)用類(lèi)的靜態(tài)方法時(shí)候,都只會(huì)初始化該靜態(tài)字段或者靜態(tài)方法所定義的類(lèi)。使用reflect包對(duì)類(lèi)進(jìn)行反射調(diào)用的時(shí)候,如果類(lèi)沒(méi)有進(jìn)行初始化,則先要初始化該類(lèi)。當(dāng)初始化一個(gè)類(lèi)的時(shí)候,如果其父類(lèi)沒(méi)有初始化過(guò),則先要觸發(fā)其父類(lèi)初始化。虛擬機(jī)啟動(dòng)的時(shí)候,會(huì)初始化一個(gè)有main方法的主類(lèi)。
注意:
子類(lèi)引用父類(lèi)靜態(tài)字段,只會(huì)初始化父類(lèi)不會(huì)初始化子類(lèi)
通過(guò)數(shù)組定義來(lái)引用類(lèi),也不會(huì)觸發(fā)該類(lèi)的初始化
常量在編譯階段會(huì)存入調(diào)用類(lèi)的常量池中,本質(zhì)上沒(méi)有直接引用到######定義常量的類(lèi),因此也不會(huì)觸發(fā)定義常量的類(lèi)的初始化
類(lèi)加載過(guò)程
加載
加載階段主要完成三件事,即通過(guò)一個(gè)類(lèi)的全限定名來(lái)獲取定義此類(lèi)的二進(jìn)制字節(jié)流,將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu),在Java堆中生成一個(gè)代表此類(lèi)的Class對(duì)象,作為訪(fǎng)問(wèn)方法區(qū)這些數(shù)據(jù)的入口。這個(gè)加載過(guò)程主要就是靠類(lèi)加載器實(shí)現(xiàn)的,這個(gè)過(guò)程可以由用戶(hù)自定義類(lèi)的加載過(guò)程。
驗(yàn)證
這個(gè)階段目的在于確保才class文件的字節(jié)流中包含信息符合當(dāng)前虛擬機(jī)要求,不會(huì)危害虛擬機(jī)自身安全。主要包括四種驗(yàn)證:
文件格式驗(yàn)證:基于字節(jié)流驗(yàn)證,驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前虛擬機(jī)處理。元數(shù)據(jù)驗(yàn)證:基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)驗(yàn)證,對(duì)字節(jié)碼描述信息進(jìn)行語(yǔ)義驗(yàn)證。字節(jié)碼驗(yàn)證:基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)驗(yàn)證,進(jìn)行數(shù)據(jù)流和控制流的驗(yàn)證。符號(hào)引用驗(yàn)證:基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)驗(yàn)證,發(fā)生在解析中,是否可以將符號(hào)引用成功解析為直接引用。
準(zhǔn)備
僅僅為類(lèi)變量(即static修飾的字段變量)分配內(nèi)存并且設(shè)置該類(lèi)變量的初始值即零值,這里不包含用final修飾的static,因?yàn)閒inal在編譯的時(shí)候就會(huì)分配了(編譯器的優(yōu)化),同時(shí)這里也不會(huì)為實(shí)例變量分配初始化。類(lèi)變量會(huì)分配在方法區(qū)中,而實(shí)例變量是會(huì)隨著對(duì)象一起分配到Java堆中。
解析
解析主要就是將常量池中的符號(hào)引用替換為直接引用的過(guò)程。符號(hào)引用就是一組符號(hào)來(lái)描述目標(biāo),可以是任何字面量,而直接引用就是直接指向目標(biāo)的指針、相對(duì)偏移量或一個(gè)間接定位到目標(biāo)的句柄。有類(lèi)或接口的解析,字段解析,類(lèi)方法解析,接口方法解析。
初始化
初始化階段依舊是初始化類(lèi)變量和其他資源,這里將執(zhí)行用戶(hù)的static字段和靜態(tài)語(yǔ)句塊的賦值操作。這個(gè)過(guò)程就是執(zhí)行類(lèi)構(gòu)造器< clinit >方法的過(guò)程。< clinit >方法是由編譯器收集類(lèi)中所有類(lèi)變量的賦值動(dòng)作和靜態(tài)語(yǔ)句塊的語(yǔ)句生成的,類(lèi)構(gòu)造器< clinit >方法與實(shí)例構(gòu)造器< init >方法不同,這里面不用顯示的調(diào)用父類(lèi)的< clinit >方法,父類(lèi)的< clinit >方法會(huì)自動(dòng)先執(zhí)行于子類(lèi)的< clinit >方法。即父類(lèi)定義的靜態(tài)語(yǔ)句塊和靜態(tài)字段都要優(yōu)先子類(lèi)的變量賦值操作。
類(lèi)加載器
類(lèi)加載器的分類(lèi)
啟動(dòng)類(lèi)加載器(Bootstrap ClassLoader):
主要負(fù)責(zé)加載<JAVA_HOME>\lib目錄中的'.'或是-Xbootclasspath參數(shù)指定的路徑中的,并且可以被虛擬機(jī)識(shí)別(僅僅按照文件名識(shí)別的)的類(lèi)庫(kù)到虛擬機(jī)內(nèi)存中。它加載的是System.getProperty("sun.boot.class.path")所指定的路徑或jar。擴(kuò)展類(lèi)加載器(Extension ClassLoader):主要負(fù)責(zé)加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類(lèi)庫(kù)。它加載的是System.getProperty("java.ext.dirs")所指定的路徑或jar。應(yīng)用程序類(lèi)加載器(Application ClassLoader):也叫系統(tǒng)類(lèi)加載器,主要負(fù)責(zé)加載ClassPath路徑上的類(lèi)庫(kù),如果應(yīng)用程序沒(méi)有自定義自己類(lèi)加載器,則這個(gè)就是默認(rèn)的類(lèi)加載器。它加載的是System.getProperty("java.class.path")所指定的路徑或jar。類(lèi)加載器的特點(diǎn)
運(yùn)行一個(gè)程序時(shí),總是由Application Loader(系統(tǒng)類(lèi)加載器)開(kāi)始加載指定的類(lèi)。在加載類(lèi)時(shí),每個(gè)類(lèi)加載器會(huì)將加載任務(wù)上交給其父,如果其父找不到,再由自己去加載。Bootstrap Loader(啟動(dòng)類(lèi)加載器)是最頂級(jí)的類(lèi)加載器了,其父加載器為null。類(lèi)加載器的雙親委派模型
類(lèi)加載器雙親委派模型的工作過(guò)程是:
如果一個(gè)類(lèi)加載器收到一個(gè)類(lèi)加載的請(qǐng)求,它首先將這個(gè)請(qǐng)求委派給父類(lèi)加載器去完成,每一個(gè)層次類(lèi)加載器都是如此,則所有的類(lèi)加載請(qǐng)求都會(huì)傳送到頂層的啟動(dòng)類(lèi)加載器,只有父加載器無(wú)法完成這個(gè)加載請(qǐng)求(即它的搜索范圍中沒(méi)有找到所要的類(lèi)),子類(lèi)才嘗試加載。
使用雙親委派模型主要是兩個(gè)原因:
可以避免重復(fù)加載,當(dāng)父類(lèi)已經(jīng)加載了,則就子類(lèi)不需再次加載;安全因素,如果不用這種,則用戶(hù)可以隨意的自定義加載器來(lái)替代Java核心API,則就會(huì)帶來(lái)安全隱患。下面是一個(gè)類(lèi)加載器雙親委派模型,這里各個(gè)類(lèi)加載器并不是繼承關(guān)系,它們利用組合實(shí)現(xiàn)的父類(lèi)與子類(lèi)關(guān)系。
類(lèi)加載的幾種方式
命令行啟動(dòng)應(yīng)用時(shí)候由JVM初始化加載,加載含有main的主類(lèi)。
通過(guò)Class.forName("Hello")方法動(dòng)態(tài)加載類(lèi),默認(rèn)會(huì)執(zhí)行初始化塊,這是因?yàn)镃lass.forName("Hello")其實(shí)就是Class.forName("Hello",true,CALLCLASS.getClassLoader()),第二個(gè)參數(shù)就是類(lèi)加載過(guò)程中的連接操作。如果指定了ClassLoader,則不會(huì)執(zhí)行初始化塊。通過(guò)ClassLoader.loadClass("Hello")方法動(dòng)態(tài)加載類(lèi),不會(huì)執(zhí)行初始化塊,因?yàn)閘oadClass方法有兩個(gè)參數(shù),用戶(hù)只是用第一個(gè)參數(shù),第二個(gè)參數(shù)默認(rèn)為false,即不對(duì)該類(lèi)進(jìn)行解析則就不會(huì)初始化。類(lèi)加載實(shí)例
當(dāng)在命令行下執(zhí)行:java HelloWorld
HelloWorld是含有main方法的類(lèi)的Class文件,JVM會(huì)將HelloWorld.class加載到內(nèi)存中,并在堆中形成一個(gè)Class的對(duì)象HelloWorld.class。
基本的加載流程如下:
尋找jre目錄,尋找jvm.dll,并初始化JVM;產(chǎn)生一個(gè)Bootstrap Loader(啟動(dòng)類(lèi)加載器);Bootstrap Loader,該加載器會(huì)加載它指定路徑下的Java核心API,并且再自動(dòng)加載Extended Loader(標(biāo)準(zhǔn)擴(kuò)展類(lèi)加載器),Extended Loader會(huì)加載指定路徑下的擴(kuò)展JavaAPI,并將其父Loader設(shè)為BootstrapLoader。Bootstrap Loader也會(huì)同時(shí)自動(dòng)加載AppClass Loader(系統(tǒng)類(lèi)加載器),并將其父Loader設(shè)為ExtendedLoader。最后由AppClass Loader加載CLASSPATH目錄下定義的類(lèi),HelloWorld類(lèi)。創(chuàng)建自己的類(lèi)加載器在Java應(yīng)用開(kāi)發(fā)過(guò)程中,可能會(huì)需要?jiǎng)?chuàng)建應(yīng)用自己的類(lèi)加載器。典型的場(chǎng)景包括實(shí)現(xiàn)特定的Java字節(jié)代碼查找方式、對(duì)字節(jié)代碼進(jìn)行加密/解密以及實(shí)現(xiàn)同名Java類(lèi)的隔離等。創(chuàng)建自己的類(lèi)加載器并不是一件復(fù)雜的事情,只需要繼承自java.lang.ClassLoader類(lèi)并覆寫(xiě)對(duì)應(yīng)的方法即可。 java.lang.ClassLoader中提供的方法有不少,下面介紹幾個(gè)創(chuàng)建類(lèi)加載器時(shí)需要考慮的:
defineClass():這個(gè)方法用來(lái)完成從Java字節(jié)碼的字節(jié)數(shù)組到j(luò)ava.lang.Class的轉(zhuǎn)換。這個(gè)方法是不能被覆寫(xiě)的,一般是用原生代碼來(lái)實(shí)現(xiàn)的。findLoadedClass():這個(gè)方法用來(lái)根據(jù)名稱(chēng)查找已經(jīng)加載過(guò)的Java類(lèi)。一個(gè)類(lèi)加載器不會(huì)重復(fù)加載同一名稱(chēng)的類(lèi)。findClass():這個(gè)方法用來(lái)根據(jù)名稱(chēng)查找并加載Java類(lèi)。loadClass():這個(gè)方法用來(lái)根據(jù)名稱(chēng)加載Java類(lèi)。resolveClass():這個(gè)方法用來(lái)鏈接一個(gè)Java類(lèi)。這里比較 容易混淆的是findClass()方法和loadClass()方法的作用。前面提到過(guò),在Java類(lèi)的鏈接過(guò)程中,會(huì)需要對(duì)Java類(lèi)進(jìn)行解析,而解析可能會(huì)導(dǎo)致當(dāng)前Java類(lèi)所引用的其它Java類(lèi)被加載。在這個(gè)時(shí)候,JVM就是通過(guò)調(diào)用當(dāng)前類(lèi)的定義類(lèi)加載器的loadClass()方法來(lái)加載其它類(lèi)的。findClass()方法則是應(yīng)用創(chuàng)建的類(lèi)加載器的擴(kuò)展點(diǎn)。應(yīng)用自己的類(lèi)加載器應(yīng)該覆寫(xiě)findClass()方法來(lái)添加自定義的類(lèi)加載邏輯。 loadClass()方法的默認(rèn)實(shí)現(xiàn)會(huì)負(fù)責(zé)調(diào)用findClass()方法。前面提到,類(lèi)加載器的代理模式默認(rèn)使用的是父類(lèi)優(yōu)先的策略。這個(gè)策略的實(shí)現(xiàn)是封裝在loadClass()方法中的。如果希望修改此策略,就需要覆寫(xiě)loadClass()方法。
下面的代碼給出了自定義的類(lèi)加載的常見(jiàn)實(shí)現(xiàn)模式:
publicclassMyClassLoaderextendsClassLoader{ protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] b = null; //查找或生成Java類(lèi)的字節(jié)代碼 return defineClass(name, b, 0, b.length); }}
Java垃圾回收機(jī)制Java堆內(nèi)存分代收集
新生代(Young Generation)
Eden空間(Eden space,任何實(shí)例都通過(guò)Eden空間進(jìn)入運(yùn)行時(shí)內(nèi)存區(qū)域)S0 Survivor空間(S0 Survivor space,存在時(shí)間長(zhǎng)的實(shí)例將會(huì)從Eden空間移動(dòng)到S0 Survivor空間)S1 Survivor空間 (存在時(shí)間更長(zhǎng)的實(shí)例將會(huì)從S0 Survivor空間移動(dòng)到S1 Survivor空間)
老年代(Old Generation)實(shí)例將從S1提升到Tenured(終身代)
永久代(Permanent Generation)包含類(lèi)、方法等細(xì)節(jié)的元信息
永久代空間在Java SE8特性中已經(jīng)被移除。
垃圾回收過(guò)程
年輕代:使用標(biāo)記復(fù)制清理算法,解決內(nèi)存碎片問(wèn)題。因?yàn)樵谀贻p代會(huì)有大量的內(nèi)存需要回收,GC比較頻繁。通過(guò)這種方式來(lái)處理內(nèi)存碎片化,然后在老年代中通過(guò)標(biāo)記清理算法來(lái)回收內(nèi)存,因?yàn)樵诶夏甏枰换厥盏膬?nèi)存比較少,提高效率。
Eden 區(qū):
當(dāng)一個(gè)實(shí)例被創(chuàng)建了,首先會(huì)被存儲(chǔ)在堆內(nèi)存年輕代的 Eden 區(qū)中。
Survivor 區(qū)(S0 和 S1):
作為年輕代 GC(Minor GC)周期的一部分,存活的對(duì)象(仍然被引用的)從 Eden區(qū)被移動(dòng)到 Survivor 區(qū)的 S0 中。類(lèi)似的,垃圾回收器會(huì)掃描 S0 然后將存活的實(shí)例移動(dòng)到 S1 中??倳?huì)有一個(gè)空的survivor區(qū)。
老年代:
老年代(Old or tenured generation)是堆內(nèi)存中的第二塊邏輯區(qū)。當(dāng)垃圾回收器執(zhí)行 Minor GC 周期時(shí)(對(duì)象年齡計(jì)數(shù)器),在 S1 Survivor 區(qū)中的存活實(shí)例將會(huì)被晉升到老年代,而未被引用的對(duì)象被標(biāo)記為回收。老年代是實(shí)例生命周期的最后階段。Major GC 掃描老年代的垃圾回收過(guò)程。如果實(shí)例不再被引用,那么它們會(huì)被標(biāo)記為回收,否則它們會(huì)繼續(xù)留在老年代中。
內(nèi)存碎片:
一旦實(shí)例從堆內(nèi)存中被刪除,其位置就會(huì)變空并且可用于未來(lái)實(shí)例的分配。這些空出的空間將會(huì)使整個(gè)內(nèi)存區(qū)域碎片化。為了實(shí)例的快速分配,需要進(jìn)行碎片整理?;诶厥掌鞯牟煌x擇,回收的內(nèi)存區(qū)域要么被不停地被整理,要么在一個(gè)單獨(dú)的GC進(jìn)程中完成。
根可達(dá)性算法
Java語(yǔ)言規(guī)范沒(méi)有明確地說(shuō)明JVM使用哪種垃圾回收算法,但是任何一種垃圾收集算法一般要做2件基本的事情:
發(fā)現(xiàn)無(wú)用信息對(duì)象回收被無(wú)用對(duì)象占用的內(nèi)存空間,使該空間可被程序再次使用。
GC Roots根集就是正在執(zhí)行的Java程序可以訪(fǎng)問(wèn)的引用變量的集合(包括局部變量、參數(shù)、類(lèi)變量)
GC Roots的對(duì)象包括
虛擬機(jī)棧中所引用的對(duì)象(本地變量表)
方法區(qū)中類(lèi)靜態(tài)屬性引用的對(duì)象
方法區(qū)中常量引用的對(duì)象
本地方法棧中JNI引用的對(duì)象(Native對(duì)象)
**可達(dá)性算法分析 **
通過(guò)一系列稱(chēng)為”GC Roots”的對(duì)象作為起點(diǎn),從這些節(jié)點(diǎn)開(kāi)始向下搜索,搜索所有走過(guò)的路徑稱(chēng)為引用鏈,當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連時(shí)(從GC Roots到此對(duì)象不可達(dá)),則證明此對(duì)象是不可用的,應(yīng)該被回收。
根搜索算法:計(jì)算可達(dá)性,如圖:
垃圾回收算法
引用計(jì)數(shù)法
引用計(jì)數(shù)法是唯一沒(méi)有使用根集(GC Roots)的垃圾回收的法,該算法使用引用計(jì)數(shù)器來(lái)區(qū)分存活對(duì)象和不再使用的對(duì)象。堆中的每個(gè)對(duì)象對(duì)應(yīng)一個(gè)引用計(jì)數(shù)器。當(dāng)每一次創(chuàng)建一個(gè)對(duì)象并賦給一個(gè)變量時(shí),引用計(jì)數(shù)器置為1。當(dāng)對(duì)象被賦給任意變量時(shí),引用計(jì)數(shù)器每次加1,當(dāng)對(duì)象出了作用域后(該對(duì)象丟棄不再使用),引用計(jì)數(shù)器減1,一旦引用計(jì)數(shù)器為0,對(duì)象就滿(mǎn)足了垃圾收集的條件。唯一沒(méi)有使用根可達(dá)性算法的垃圾回收算法。缺陷:不能解決循環(huán)引用的回收。
tracing算法(tracing collector)
tracing算法是為了解決引用計(jì)數(shù)法的問(wèn)題而提出,它使用了根集(GC Roots)概念。垃圾收集器從根集開(kāi)始掃描,識(shí)別出哪些對(duì)象可達(dá),哪些對(duì)象不可達(dá),并用某種方式標(biāo)記可達(dá)對(duì)象,例如對(duì)每個(gè)可達(dá)對(duì)象設(shè)置一個(gè)或多個(gè)位。在掃描識(shí)別過(guò)程中,基于tracing算法的垃圾收集也稱(chēng)為標(biāo)記和清除(mark-and-sweep)垃圾收集器。
compacting算法(Compacting Collector)
為了解決堆碎片問(wèn)題,在清除的過(guò)程中,算法將所有的對(duì)象移到堆的一端,堆的另一端就變成了一個(gè)相鄰的空閑內(nèi)存區(qū),收集器會(huì)對(duì)它移動(dòng)的所有對(duì)象的所有引用進(jìn)行更新,使得這些引用在新的位置能識(shí)別原來(lái)的對(duì)象。在基于Compacting算法的收集器的實(shí)現(xiàn)中,一般增加句柄和句柄表。
copying算法(Coping Collector)
該算法的提出是為了克服句柄的開(kāi)銷(xiāo)和解決堆碎片的垃圾回收。它開(kāi)始時(shí)把堆分成 一個(gè)對(duì)象面和多個(gè)空閑面,程序從對(duì)象面為對(duì)象分配空間,當(dāng)對(duì)象滿(mǎn)了,基于coping算法的垃圾收集就從根集中掃描活動(dòng)對(duì)象,并將每個(gè)活動(dòng)對(duì)象復(fù)制到空閑面(使得活動(dòng)對(duì)象所占的內(nèi)存之間沒(méi)有空閑洞),這樣空閑面變成了對(duì)象面,原來(lái)的對(duì)象面變成了空閑面,程序會(huì)在新的對(duì)象面中分配內(nèi)存。
generation算法(Generational Collector
現(xiàn)在的java內(nèi)存分區(qū)stop-and-copy垃圾收集器的一個(gè)缺陷是收集器必須復(fù)制所有的活動(dòng)對(duì)象,這增加了程序等待時(shí)間,這是coping算法低效的原因。在程序設(shè)計(jì)中有這樣的規(guī)律:多數(shù)對(duì)象存在的時(shí)間比較短,少數(shù)的存在時(shí)間比較長(zhǎng)。因此,generation算法將堆分成兩個(gè)或多個(gè),每個(gè)子堆作為對(duì)象的一代 (generation)。由于多數(shù)對(duì)象存在的時(shí)間比較短,隨著程序丟棄不使用的對(duì)象,垃圾收集器將從最年輕的子堆中收集這些對(duì)象。在分代式的垃圾收集器運(yùn)行后,上次運(yùn)行存活下來(lái)的對(duì)象移到下一最高代的子堆中,由于老一代的子堆不會(huì)經(jīng)常被回收,因而節(jié)省了時(shí)間。
adaptive算法(Adaptive Collector)
在特定的情況下,一些垃圾收集算法會(huì)優(yōu)于其它算法?;贏(yíng)daptive算法的垃圾收集器就是監(jiān)控當(dāng)前堆的使用情況,并將選擇適當(dāng)算法的垃圾收集器。
以上就是扣丁學(xué)堂Java培訓(xùn)之類(lèi)加載機(jī)制原理分析與hook技術(shù)實(shí)現(xiàn)底層方法的詳細(xì)介紹,希望對(duì)小伙伴們有所幫助,想要了解更多內(nèi)容的小伙伴可以登錄扣丁學(xué)堂官網(wǎng)查詢(xún)??鄱W(xué)堂是專(zhuān)業(yè)的Java培訓(xùn)機(jī)構(gòu),不僅有專(zhuān)業(yè)的老師和與時(shí)俱進(jìn)的課程體系,還有大量的Java在線(xiàn)教程供學(xué)員掛看學(xué)習(xí)哦。Java技術(shù)交流群:670348138。
【關(guān)注微信公眾號(hào)獲取更多學(xué)習(xí)資料】
查看更多關(guān)于“Java開(kāi)發(fā)資訊”的相關(guān)文章>>