一、類加載的機制的層次結構
每個編寫的”.java”拓展名類文件都存儲着需要執行的程序邏輯,這些”.java”文件經過Java編譯器編譯成拓展名為”.class”的文件,”.class”文件中保存着Java代碼經轉換后的虛擬機指令,當需要使用某個類時,虛擬機將會加載它的”.class”文件,並創建對應的class對象,將class文件加載到虛擬機的內存,這個過程稱為類加載,這里我們需要了解一下類加載的過程,如下:
Jvm執行class文件
步驟一、類加載機制
將class文件字節碼內容加載到內存中,並將這些靜態數據轉換成方法區中的運行時數據結構,在堆中生成一個代表這個類的java.lang.Class對象,作為方法區類數據的訪問入口,這個過程需要類加載器參與。
當系統運行時,類加載器將.class文件的二進制數據從外部存儲器(如光盤,硬盤)調入內存中,CPU再從內存中讀取指令和數據進行運算,並將運算結果存入內存中。內存在該過程中充當着"二傳手"的作用,通俗的講,如果沒有內存,類加載器從外部存儲設備調入.class文件二進制數據直接給CPU處理,而由於CPU的處理速度遠遠大於調入數據的速度,容易造成數據的脫節,所以需要內存起緩沖作用。
類將.class文件加載至運行時的方法區后,會在堆中創建一個Java.lang.Class對象,用來封裝類位於方法區內的數據結構,該Class對象是在加載類的過程中創建的,每個類都對應有一個Class類型的對象,Class類的構造方法是私有的,只有JVM能夠創建。因此Class對象是反射的入口,使用該對象就可以獲得目標類所關聯的.class文件中具體的數據結構。
類加載的最終產物就是位於堆中的Class對象(注意不是目標類對象),該對象封裝了類在方法區中的數據結構,並且向用戶提供了訪問方法區數據結構的接口,即Java反射的接口。
步驟二、連接過程
將java類的二進制代碼合並到JVM的運行狀態之中的過程
驗證:確保加載的類信息符合JVM規范,沒有安全方面的問題
准備:正式為類變量(static變量)分配內存並設置類變量初始值的階段,這些內存都將在方法區中進行分配
解析:虛擬機常量池的符號引用替換為字節引用過程
步驟三、初始化
初始化階段是執行類構造器<clinit>()
方法的過程。類構造器<clinit>()
方法是由編譯器自動收藏類中的所有類變量的賦值動作和靜態語句塊(static塊)中的語句合並產生,代碼從上往下執行。
當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化
虛擬機會保證一個類的<clinit>()
方法在多線程環境中被正確加鎖和同步
當范圍一個Java類的靜態域時,只有真正聲名這個域的類才會被初始化
二、類加載器的層次結構
啟動(Bootstrap)類加載器
擴展(Extension)類加載器
系統(-)類加載器
1、啟動(Bootstrap)類加載器
啟動類加載器主要加載的是JVM自身需要的類,這個類加載使用C++語言實現的,是虛擬機自身的一部分,它負責將 <JAVA_HOME>/lib
路徑下的核心類庫或-Xbootclasspath參數指定的路徑下的jar包加載到內存中,注意必由於虛擬機是按照文件名識別加載jar包的,如rt.jar,如果文件名不被虛擬機識別,即使把jar包丟到lib目錄下也是沒有作用的(出於安全考慮,Bootstrap啟動類加載器只加載包名為java、javax、sun等開頭的類)。
2、擴展(Extension)類加載器
擴展類加載器是指Sun公司(已被Oracle收購)實現的sun.misc.Launcher$ExtClassLoader類,由Java語言實現的,是Launcher的靜態內部類,它負責加載<JAVA_HOME>/lib/ext
目錄下或者由系統變量-Djava.ext.dir指定位路徑中的類庫,開發者可以直接使用標准擴展類加載器。
3、系統(System)類加載器
也稱應用程序加載器是指 Sun公司實現的sun.misc.Launcher$AppClassLoader。它負責加載系統類路徑java -classpath或-D java.class.path 指定路徑下的類庫,也就是我們經常用到的classpath路徑,開發者可以直接使用系統類加載器,一般情況下該類加載是程序中默認的類加載器,通過ClassLoader#getSystemClassLoader()方法可以獲取到該類加載器。
在Java的日常應用程序開發中,類的加載幾乎是由上述3種類加載器相互配合執行的,在必要時,我們還可以自定義類加載器,需要注意的是,Java虛擬機對class文件采用的是按需加載的方式,也就是說當需要使用該類時才會將它的class文件加載到內存生成class對象,而且加載某個類的class文件時,Java虛擬機采用的是雙親委派模式即把請求交由父類處理,它一種任務委派模式,下面我們進一步了解它。
3.1、理解雙親委派模式
下面我們從代碼層面了解幾個Java中定義的類加載器及其雙親委派模式的實現,它們類圖關系如下
雙親委派模式是在Java 1.2后引入的,其工作原理的是,如果一個類加載器收到了類加載請求,它並不會自己先去加載,而是把這個請求委托給父類的加載器去執行,如果父類加載器還存在其父類加載器,則進一步向上委托,依次遞歸,請求最終將到達頂層的啟動類加載器,如果父類加載器可以完成類加載任務,就成功返回,倘若父類加載器無法完成此加載任務,子加載器才會嘗試自己去加載,這就是雙親委派模式,即每個兒子都很懶,每次有活就丟給父親去干,直到父親說這件事我也干不了時,兒子自己想辦法去完成,這不就是傳說中的實力坑爹啊?那么采用這種模式有啥用呢?
3.1、雙親委派模式優勢
采用雙親委派模式的是好處是Java類隨着它的類加載器一起具備了一種帶有優先級的層次關系,通過這種層級關可以避免類的重復加載,當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次。其次是考慮到安全因素,java核心api中定義類型不會被隨意替換,假設通過網絡傳遞一個名為java.lang.Integer的類,通過雙親委托模式傳遞到啟動類加載器,而啟動類加載器在核心Java API發現這個名字的類,發現該類已被加載,並不會重新加載網絡傳遞的過來的java.lang.Integer,而直接返回已加載過的Integer.class,這樣便可以防止核心API庫被隨意篡改。可能你會想,如果我們在classpath路徑下自定義一個名為java.lang.SingleInterge類(該類是胡編的)呢?該類並不存在java.lang中,經過雙親委托模式,傳遞到啟動類加載器中,由於父類加載器路徑下並沒有該類,所以不會加載,將反向委托給子類加載器加載,最終會通過系統類加載器加載該類。但是這樣做是不允許,因為java.lang是核心API包,需要訪問權限,強制加載將會報出如下異常
java.lang.SecurityException: Prohibited package name: java.lang
所以無論如何都無法加載成功的。
三、類加載器間的關系
我們進一步了解類加載器間的關系(並非指繼承關系),主要可以分為以下4點
- 啟動類加載器,由C++實現,沒有父類。
- 拓展類加載器(ExtClassLoader),由Java語言實現,父類加載器為null
- 系統類加載器(AppClassLoader),由Java語言實現,父類加載器為ExtClassLoader
- 自定義類加載器,父類加載器肯定為AppClassLoader。
1、類加載器常用方法
loadClass(String)
該方法加載指定名稱(包括包名)的二進制類型,該方法在JDK1.2之后不再建議用戶重寫但用戶可以直接調用該方法,loadClass()方法是ClassLoader類自己實現的,該方法中的邏輯就是雙親委派模式的實現,其源碼如下,loadClass(String name, boolean resolve)是一個重載方法,resolve參數代表是否生成class對象的同時進行解析相關操作。
正如loadClass方法所展示的,當類加載請求到來時,先從緩存中查找該類對象,如果存在直接返回,如果不存在則交給該類加載去的父加載器去加載,倘若沒有父加載則交給頂級啟動類加載器去加載,最后倘若仍沒有找到,則使用findClass()方法去加載(關於findClass()稍后會進一步介紹)。從loadClass實現也可以知道如果不想重新定義加載類的規則,也沒有復雜的邏輯,只想在運行時加載自己指定的類,那么我們可以直接使用this.getClass().getClassLoder.loadClass("className"),這樣就可以直接調用ClassLoader的loadClass方法獲取到class對象。
findClass(String)
在JDK1.2之前,在自定義類加載時,總會去繼承ClassLoader類並重寫loadClass方法,從而實現自定義的類加載類,但是在JDK1.2之后已不再建議用戶去覆蓋loadClass()方法,而是建議把自定義的類加載邏輯寫在findClass()方法中,從前面的分析可知,findClass()方法是在loadClass()方法中被調用的,當loadClass()方法中父加載器加載失敗后,則會調用自己的findClass()方法來完成類加載,這樣就可以保證自定義的類加載器也符合雙親委托模式。需要注意的是ClassLoader類中並沒有實現findClass()方法的具體代碼邏輯,取而代之的是拋出ClassNotFoundException異常,同時應該知道的是findClass方法通常是和defineClass方法一起使用的(稍后會分析)
defineClass(byte[] b, int off, int len)
defineClass()方法是用來將byte字節流解析成JVM能夠識別的Class對象(ClassLoader中已實現該方法邏輯),通過這個方法不僅能夠通過class文件實例化class對象,也可以通過其他方式實例化class對象,如通過網絡接收一個類的字節碼,然后轉換為byte字節流創建對應的Class對象,defineClass()方法通常與findClass()方法一起使用,一般情況下,在自定義類加載器時,會直接覆蓋ClassLoader的findClass()方法並編寫加載規則,取得要加載類的字節碼后轉換成流,然后調用defineClass()方法生成類的Class對象
resolveClass(Class<?> c)
使用該方法可以使用類的Class對象創建完成也同時被解析。前面我們說鏈接階段主要是對字節碼進行驗證,為類變量分配內存並設置初始值同時將字節碼文件中的符號引用轉換為直接引用。
四、熱部署
對於Java應用程序來說,熱部署就是在運行時更新Java類文件。
1、熱部署的原理是什么
想要知道熱部署的原理,必須要了解java類的加載過程。一個java類文件到虛擬機里的對象,要經過如下過程。
首先通過java編譯器,將java文件編譯成class字節碼,類加載器讀取class字節碼,再將類轉化為實例,對實例newInstance就可以生成對象。
類加載器ClassLoader功能,也就是將class字節碼轉換到類的實例。
在java應用中,所有的實例都是由類加載器,加載而來。
一般在系統中,類的加載都是由系統自帶的類加載器完成,而且對於同一個全限定名的java類(如com.csiar.soc.HelloWorld),只能被加載一次,而且無法被卸載。
這個時候問題就來了,如果我們希望將java類卸載,並且替換更新版本的java類,該怎么做呢?
既然在類加載器中,java類只能被加載一次,並且無法卸載。那是不是可以直接把類加載器給換了?答案是可以的,我們可以自定義類加載器,並重寫ClassLoader的findClass方法。想要實現熱部署可以分以下三個步驟:
- 銷毀該自定義ClassLoader
- 更新class類文件
- 創建新的ClassLoader去加載更新后的class類文件。
2、熱部署與熱加載
2.1、Java熱部署與Java熱加載的聯系和區別
Java熱部署與熱加載的聯系
- 不重啟服務器編譯/部署項目
- 基於Java的類加載器實現
Java熱部署與熱加載的區別
- 部署方式
- 熱部署在服務器運行時重新部署項目
- 熱加載在運行時重新加載class
- 實現原理
- 熱部署直接重新加載整個應用
- 熱加載在運行時重新加載class
- 使用場景
- 熱部署更多的是在生產環境使用
- 熱加載則更多的實在開發環境使用
3、相關代碼
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);
// 讀取字節
byte[] b = new byte[is.available()];
is.read(b);
// 將byte字節流解析成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());
}
}
個人博客 蝸牛