一褪储、類加載的機制的層次結(jié)構(gòu)
每個編寫的”.java”拓展名類文件都存儲著需要執(zhí)行的程序邏輯蛾茉,這些”.java”文件經(jīng)過Java編譯器編譯成拓展名為”.class”的文件,”.class”文件中保存著Java代碼經(jīng)轉(zhuǎn)換后的虛擬機指令帽馋,當需要使用某個類時,虛擬機將會加載它的”.class”文件,并創(chuàng)建對應的class對象绽族,將class文件加載到虛擬機的內(nèi)存姨涡,這個過程稱為類加載,這里我們需要了解一下類加載的過程吧慢,如下:
Jvm執(zhí)行class文件
步驟一涛漂、類加載機制
將class文件字節(jié)碼內(nèi)容加載到內(nèi)存中,并將這些靜態(tài)數(shù)據(jù)轉(zhuǎn)換成方法區(qū)中的運行時數(shù)據(jù)結(jié)構(gòu)检诗,在堆中生成一個代表這個類的java.lang.Class對象匈仗,作為方法區(qū)類數(shù)據(jù)的訪問入口,這個過程需要類加載器參與逢慌。
當系統(tǒng)運行時悠轩,類加載器將.class文件的二進制數(shù)據(jù)從外部存儲器(如光盤,硬盤)調(diào)入內(nèi)存中攻泼,CPU再從內(nèi)存中讀取指令和數(shù)據(jù)進行運算火架,并將運算結(jié)果存入內(nèi)存中。內(nèi)存在該過程中充當著"二傳手"的作用忙菠,通俗的講何鸡,如果沒有內(nèi)存,類加載器從外部存儲設備調(diào)入.class文件二進制數(shù)據(jù)直接給CPU處理只搁,而由于CPU的處理速度遠遠大于調(diào)入數(shù)據(jù)的速度音比,容易造成數(shù)據(jù)的脫節(jié),所以需要內(nèi)存起緩沖作用氢惋。
類將.class文件加載至運行時的方法區(qū)后洞翩,會在堆中創(chuàng)建一個Java.lang.Class對象,用來封裝類位于方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)焰望,該Class對象是在加載類的過程中創(chuàng)建的骚亿,每個類都對應有一個Class類型的對象,Class類的構(gòu)造方法是私有的熊赖,只有JVM能夠創(chuàng)建来屠。因此Class對象是反射的入口,使用該對象就可以獲得目標類所關(guān)聯(lián)的.class文件中具體的數(shù)據(jù)結(jié)構(gòu)震鹉。
類加載的最終產(chǎn)物就是位于堆中的Class對象(注意不是目標類對象)俱笛,該對象封裝了類在方法區(qū)中的數(shù)據(jù)結(jié)構(gòu),并且向用戶提供了訪問方法區(qū)數(shù)據(jù)結(jié)構(gòu)的接口传趾,即Java反射的接口迎膜。
步驟二、連接過程
將java類的二進制代碼合并到JVM的運行狀態(tài)之中的過程
驗證:確保加載的類信息符合JVM規(guī)范浆兰,沒有安全方面的問題
準備:正式為類變量(static變量)分配內(nèi)存并設置類變量初始值的階段磕仅,這些內(nèi)存都將在方法區(qū)中進行分配
解析:虛擬機常量池的符號引用替換為字節(jié)引用過程
步驟三珊豹、初始化
初始化階段是執(zhí)行類構(gòu)造器<clinit>()
方法的過程。類構(gòu)造器<clinit>()
方法是由編譯器自動收藏類中的所有類變量的賦值動作和靜態(tài)語句塊(static塊)中的語句合并產(chǎn)生榕订,代碼從上往下執(zhí)行店茶。
當初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進行過初始化劫恒,則需要先觸發(fā)其父類的初始化
虛擬機會保證一個類的<clinit>()
方法在多線程環(huán)境中被正確加鎖和同步
當范圍一個Java類的靜態(tài)域時贩幻,只有真正聲名這個域的類才會被初始化
二、類加載器的層次結(jié)構(gòu)
啟動(Bootstrap)類加載器
擴展(Extension)類加載器
系統(tǒng)(-)類加載器
1兼贸、啟動(Bootstrap)類加載器
啟動類加載器主要加載的是JVM自身需要的類段直,這個類加載使用C++語言實現(xiàn)的,是虛擬機自身的一部分溶诞,它負責將<JAVA_HOME>/lib
路徑下的核心類庫或-Xbootclasspath參數(shù)指定的路徑下的jar包加載到內(nèi)存中鸯檬,注意必由于虛擬機是按照文件名識別加載jar包的,如rt.jar螺垢,如果文件名不被虛擬機識別喧务,即使把jar包丟到lib目錄下也是沒有作用的(出于安全考慮,Bootstrap啟動類加載器只加載包名為java枉圃、javax功茴、sun等開頭的類)。
2孽亲、擴展(Extension)類加載器
擴展類加載器是指Sun公司(已被Oracle收購)實現(xiàn)的sun.misc.Launcher$ExtClassLoader類坎穿,由Java語言實現(xiàn)的,是Launcher的靜態(tài)內(nèi)部類返劲,它負責加載<JAVA_HOME>/lib/ext
目錄下或者由系統(tǒng)變量-Djava.ext.dir指定位路徑中的類庫玲昧,開發(fā)者可以直接使用標準擴展類加載器。
3篮绿、系統(tǒng)(System)類加載器
也稱應用程序加載器是指 Sun公司實現(xiàn)的sun.misc.Launcher$AppClassLoader孵延。它負責加載系統(tǒng)類路徑java -classpath或-D java.class.path 指定路徑下的類庫,也就是我們經(jīng)常用到的classpath路徑亲配,開發(fā)者可以直接使用系統(tǒng)類加載器尘应,一般情況下該類加載是程序中默認的類加載器,通過ClassLoader#getSystemClassLoader()方法可以獲取到該類加載器吼虎。
在Java的日常應用程序開發(fā)中犬钢,類的加載幾乎是由上述3種類加載器相互配合執(zhí)行的,在必要時玷犹,我們還可以自定義類加載器,需要注意的是箱舞,Java虛擬機對class文件采用的是按需加載的方式拳亿,也就是說當需要使用該類時才會將它的class文件加載到內(nèi)存生成class對象,而且加載某個類的class文件時电湘,Java虛擬機采用的是雙親委派模式即把請求交由父類處理鹅经,它一種任務委派模式,下面我們進一步了解它贷痪。
3.1蹦误、理解雙親委派模式
下面我們從代碼層面了解幾個Java中定義的類加載器及其雙親委派模式的實現(xiàn),它們類圖關(guān)系如下
雙親委派模式是在Java 1.2后引入的舱沧,其工作原理的是偶洋,如果一個類加載器收到了類加載請求,它并不會自己先去加載玄窝,而是把這個請求委托給父類的加載器去執(zhí)行哆料,如果父類加載器還存在其父類加載器缸剪,則進一步向上委托杏节,依次遞歸典阵,請求最終將到達頂層的啟動類加載器,如果父類加載器可以完成類加載任務壮啊,就成功返回歹啼,倘若父類加載器無法完成此加載任務座菠,子加載器才會嘗試自己去加載藤树,這就是雙親委派模式,即每個兒子都很懶升略,每次有活就丟給父親去干屡限,直到父親說這件事我也干不了時,兒子自己想辦法去完成翰撑,這不就是傳說中的實力坑爹鞍⊙搿?那么采用這種模式有啥用呢?
3.1册养、雙親委派模式優(yōu)勢
采用雙親委派模式的是好處是Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系压固,通過這種層級關(guān)可以避免類的重復加載,當父親已經(jīng)加載了該類時坎炼,就沒有必要子ClassLoader再加載一次拦键。其次是考慮到安全因素,java核心api中定義類型不會被隨意替換萄金,假設通過網(wǎng)絡傳遞一個名為java.lang.Integer的類媚朦,通過雙親委托模式傳遞到啟動類加載器,而啟動類加載器在核心Java API發(fā)現(xiàn)這個名字的類孙乖,發(fā)現(xiàn)該類已被加載,并不會重新加載網(wǎng)絡傳遞的過來的java.lang.Integer唯袄,而直接返回已加載過的Integer.class恋拷,這樣便可以防止核心API庫被隨意篡改∶仿樱可能你會想阎抒,如果我們在classpath路徑下自定義一個名為java.lang.SingleInterge類(該類是胡編的)呢消痛?該類并不存在java.lang中且叁,經(jīng)過雙親委托模式逞带,傳遞到啟動類加載器中纱新,由于父類加載器路徑下并沒有該類,所以不會加載遇汞,將反向委托給子類加載器加載簿废,最終會通過系統(tǒng)類加載器加載該類。但是這樣做是不允許族檬,因為java.lang是核心API包单料,需要訪問權(quán)限,強制加載將會報出如下異常
java.lang.SecurityException: Prohibited package name: java.lang
所以無論如何都無法加載成功的递鹉。
三藏斩、類加載器間的關(guān)系
我們進一步了解類加載器間的關(guān)系(并非指繼承關(guān)系),主要可以分為以下4點
- 啟動類加載器媳拴,由C++實現(xiàn),沒有父類塞关。
- 拓展類加載器(ExtClassLoader)子巾,由Java語言實現(xiàn)线梗,父類加載器為null
- 系統(tǒng)類加載器(AppClassLoader),由Java語言實現(xiàn)瘾婿,父類加載器為ExtClassLoader
- 自定義類加載器,父類加載器肯定為AppClassLoader偏陪。
1笛谦、類加載器常用方法
loadClass(String)
該方法加載指定名稱(包括包名)的二進制類型昌阿,該方法在JDK1.2之后不再建議用戶重寫但用戶可以直接調(diào)用該方法宝泵,loadClass()方法是ClassLoader類自己實現(xiàn)的,該方法中的邏輯就是雙親委派模式的實現(xiàn)儿奶,其源碼如下闯捎,loadClass(String name, boolean resolve)是一個重載方法,resolve參數(shù)代表是否生成class對象的同時進行解析相關(guān)操作秉版。
正如loadClass方法所展示的清焕,當類加載請求到來時,先從緩存中查找該類對象秸妥,如果存在直接返回粥惧,如果不存在則交給該類加載去的父加載器去加載,倘若沒有父加載則交給頂級啟動類加載器去加載起惕,最后倘若仍沒有找到惹想,則使用findClass()方法去加載(關(guān)于findClass()稍后會進一步介紹)饵婆。從loadClass實現(xiàn)也可以知道如果不想重新定義加載類的規(guī)則侨核,也沒有復雜的邏輯搓译,只想在運行時加載自己指定的類锋喜,那么我們可以直接使用this.getClass().getClassLoder.loadClass("className")嘿般,這樣就可以直接調(diào)用ClassLoader的loadClass方法獲取到class對象炉奴。
findClass(String)
在JDK1.2之前,在自定義類加載時赛糟,總會去繼承ClassLoader類并重寫loadClass方法璧南,從而實現(xiàn)自定義的類加載類司倚,但是在JDK1.2之后已不再建議用戶去覆蓋loadClass()方法对湃,而是建議把自定義的類加載邏輯寫在findClass()方法中拍柒,從前面的分析可知,findClass()方法是在loadClass()方法中被調(diào)用的拆讯,當loadClass()方法中父加載器加載失敗后种呐,則會調(diào)用自己的findClass()方法來完成類加載爽室,這樣就可以保證自定義的類加載器也符合雙親委托模式阔墩。需要注意的是ClassLoader類中并沒有實現(xiàn)findClass()方法的具體代碼邏輯,取而代之的是拋出ClassNotFoundException異常,同時應該知道的是findClass方法通常是和defineClass方法一起使用的(稍后會分析)
defineClass(byte[] b, int off, int len)
defineClass()方法是用來將byte字節(jié)流解析成JVM能夠識別的Class對象(ClassLoader中已實現(xiàn)該方法邏輯)蝉娜,通過這個方法不僅能夠通過class文件實例化class對象召川,也可以通過其他方式實例化class對象扮宠,如通過網(wǎng)絡接收一個類的字節(jié)碼坛增,然后轉(zhuǎn)換為byte字節(jié)流創(chuàng)建對應的Class對象,defineClass()方法通常與findClass()方法一起使用罢艾,一般情況下,在自定義類加載器時,會直接覆蓋ClassLoader的findClass()方法并編寫加載規(guī)則矫膨,取得要加載類的字節(jié)碼后轉(zhuǎn)換成流,然后調(diào)用defineClass()方法生成類的Class對象
resolveClass(Class<?> c)
使用該方法可以使用類的Class對象創(chuàng)建完成也同時被解析馁痴。前面我們說鏈接階段主要是對字節(jié)碼進行驗證,為類變量分配內(nèi)存并設置初始值同時將字節(jié)碼文件中的符號引用轉(zhuǎn)換為直接引用赠堵。
四小渊、熱部署
對于Java應用程序來說,熱部署就是在運行時更新Java類文件顾腊。
1粤铭、熱部署的原理是什么
想要知道熱部署的原理挖胃,必須要了解java類的加載過程杂靶。一個java類文件到虛擬機里的對象,要經(jīng)過如下過程酱鸭。
首先通過java編譯器吗垮,將java文件編譯成class字節(jié)碼,類加載器讀取class字節(jié)碼凹髓,再將類轉(zhuǎn)化為實例烁登,對實例newInstance就可以生成對象。
類加載器ClassLoader功能蔚舀,也就是將class字節(jié)碼轉(zhuǎn)換到類的實例饵沧。
在java應用中,所有的實例都是由類加載器赌躺,加載而來狼牺。
一般在系統(tǒng)中,類的加載都是由系統(tǒng)自帶的類加載器完成礼患,而且對于同一個全限定名的java類(如com.csiar.soc.HelloWorld)是钥,只能被加載一次掠归,而且無法被卸載。
這個時候問題就來了悄泥,如果我們希望將java類卸載虏冻,并且替換更新版本的java類,該怎么做呢弹囚?
既然在類加載器中厨相,java類只能被加載一次,并且無法卸載余寥。那是不是可以直接把類加載器給換了领铐?答案是可以的,我們可以自定義類加載器宋舷,并重寫ClassLoader的findClass方法绪撵。想要實現(xiàn)熱部署可以分以下三個步驟:
- 銷毀該自定義ClassLoader
- 更新class類文件
- 創(chuàng)建新的ClassLoader去加載更新后的class類文件。
2祝蝠、熱部署與熱加載
2.1音诈、Java熱部署與Java熱加載的聯(lián)系和區(qū)別
Java熱部署與熱加載的聯(lián)系
- 不重啟服務器編譯/部署項目
- 基于Java的類加載器實現(xiàn)
Java熱部署與熱加載的區(qū)別
- 部署方式
- 熱部署在服務器運行時重新部署項目
- 熱加載在運行時重新加載class
- 實現(xiàn)原理
- 熱部署直接重新加載整個應用
- 熱加載在運行時重新加載class
- 使用場景
- 熱部署更多的是在生產(chǎn)環(huán)境使用
- 熱加載則更多的實在開發(fā)環(huán)境使用
3、相關(guān)代碼
User沒有被修改類
public class User {
public void add() {
System.out.println("addV1,沒有修改過...");
}
}
User更新類
public class User {
public void add() {
System.out.println("我把之前的user add方法修改啦!");
}
}
自定義類加載器
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 文件名稱
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
// 獲取文件輸入流
InputStream is = this.getClass().getResourceAsStream(fileName);
// 讀取字節(jié)
byte[] b = new byte[is.available()];
is.read(b);
// 將byte字節(jié)流解析成jvm能夠識別的Class對象
return defineClass(name, b, 0, b.length);
} catch (Exception e) {
throw new ClassNotFoundException();
}
}
}
更新代碼
public class Hotswap {
public static void main(String[] args)
throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException,
SecurityException, IllegalArgumentException, InvocationTargetException, InterruptedException {
loadUser();
System.gc();
Thread.sleep(1000);// 等待資源回收
// 需要被熱部署的class文件
File file1 = new File("F:\\test\\User.class");
// 之前編譯好的class文件
File file2 = new File(
"F:\\test\\test\\target\\classes\\com\\itmayiedu\\User.class");
boolean isDelete = file2.delete();// 刪除舊版本的class文件
if (!isDelete) {
System.out.println("熱部署失敗.");
return;
}
file1.renameTo(file2);
System.out.println("update success!");
loadUser();
}
public static void loadUser() throws ClassNotFoundException, InstantiationException, IllegalAccessException,
NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {
MyClassLoader myLoader = new MyClassLoader();
Class<?> class1 = myLoader.findClass("com.test.User");
Object obj1 = class1.newInstance();
Method method = class1.getMethod("add");
method.invoke(obj1);
System.out.println(obj1.getClass());
System.out.println(obj1.getClass().getClassLoader());
}
}
個人博客 蝸牛