1邢隧、類加載過程
查看是否加載命令 VM options:-verbose:class
多個(gè)java文件經(jīng)過編譯打包生成可運(yùn)行jar包,最終由java命令運(yùn)行某個(gè)主類的main函數(shù)啟
動(dòng)程序,這里首先需要通過類加載器把主類加載到JVM。 主類在運(yùn)行過程中如果使用到其它類,會逐步加載這些類票从。 注意,jar包里的類不是一次性全部加載的滨嘱,是使用到時(shí)才加載峰鄙。
類加載到使用整個(gè)過程有如下幾步
加載 >> 驗(yàn)證 >> 準(zhǔn)備 >> 解析 >> 初始化 >> 使用 >> 卸載
- 加載:在硬盤上查找并通過IO讀入字節(jié)碼文件,使用到類時(shí)才會加載太雨,例如調(diào)用 類的main()方法吟榴,new對象等等
- 驗(yàn)證:校驗(yàn)字節(jié)碼文件的正確性
-類的靜態(tài)變量分配內(nèi)存,并賦予默認(rèn)值 - 解析:將符號引用替換為直接引用囊扳,該階段會把一些靜態(tài)方法(符號引用吩翻,比如
main()方法)替換為指向數(shù)據(jù)所存內(nèi)存的指針或句柄等(直接引用),這是所謂的靜態(tài)鏈 接過程(類加載期間完成)宪拥,動(dòng)態(tài)鏈接是在程序運(yùn)行期間完成的將符號引用替換為直接 引用仿野,下節(jié)課會講到動(dòng)態(tài)鏈接 -
對類的靜態(tài)變量初始化為指定的值,執(zhí)行靜態(tài)代碼塊
image.png
2她君、類加載器和雙親委派機(jī)制
上面的類加載過程主要是通過類加載器來實(shí)現(xiàn)的,Java里有如下幾種類加載器
- 啟動(dòng)類加載器:負(fù)責(zé)加載支撐JVM運(yùn)行的位于JRE的lib目錄下的核心類庫葫哗,比如 rt.jar缔刹、charsets.jar等
- 擴(kuò)展類加載器:負(fù)責(zé)加載支撐JVM運(yùn)行的位于JRE的lib目錄下的ext擴(kuò)展目錄中 的JAR類包
- 應(yīng)用程序類加載器:負(fù)責(zé)加載ClassPath路徑下的類包,主要就是加載你自己寫 的那些類
- 自定義加載器:負(fù)責(zé)加載用戶自定義路徑下的類包
看一個(gè)類加載器示例:
1 publicclassTestJDKClassLoader{
2 public static void main(String[] args){
3 System.out.println(String.class.getClassLoader());
4 System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassL oader().getClass().getName());
5
System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().get Name());
6
System.out.println(ClassLoader.getSystemClassLoader().getClass().getName()) 7}
8}
9
10 運(yùn)行結(jié)果:
11 null//啟動(dòng)類加載器是C++語言實(shí)現(xiàn)劣针,所以打印不出來 12 sun.misc.Launcher$ExtClassLoader
13 sun.misc.Launcher$AppClassLoader
14 sun.misc.Launcher$AppClassLoader
自定義一個(gè)類加載器示例:
自定義類加載器只需要繼承 java.lang.ClassLoader 類校镐,該類有兩個(gè)核心方法,一個(gè)是 loadClass(String, boolean)捺典,實(shí)現(xiàn)了雙親委派機(jī)制鸟廓,大體邏輯
- 首先,檢查一下指定名稱的類是否已經(jīng)加載過襟己,如果加載過了引谜,就不需要再加載, 直接返回擎浴。
- 如果此類沒有加載過员咽,那么,再判斷一下是否有父加載器;如果有父加載器贮预,則由 父加載器加載(即調(diào)用parent.loadClass(name, false);).或者是調(diào)用bootstrap類加 載器來加載贝室。
- 如果父加載器及bootstrap類加載器都沒有找到指定的類契讲,那么調(diào)用當(dāng)前類加載器 的findClass方法來完成類加載。
還有一個(gè)方法是findClass滑频,默認(rèn)實(shí)現(xiàn)是拋出異常捡偏,所以我們自定義類加載器主要是重寫findClass方法。
2
3
4
5
6 7} 8
9
10
11
12
13
14
15
16
17
18
19
20
{
21
22
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available(); byte[] data = new byte[len]; fis.read(data); fis.close();
return data;
}
protected Class<?> findClass(String name) throws ClassNotFoundException
try{
byte[] data = loadByte(name);
publicclassMyClassLoaderTest{
static class MyClassLoader extends ClassLoader { private String classPath;
public MyClassLoader(String classPath) { this.classPath = classPath;
//defineClass將一個(gè)字節(jié)數(shù)組轉(zhuǎn)為Class對象峡迷,這個(gè)字節(jié)數(shù)組是class文件讀取后最終 的字節(jié)數(shù)組银伟。
24 return defineClass(name, data, 0, data.length);
25 } catch (Exception e) {
26 e.printStackTrace();
27 throw new ClassNotFoundException();
28 }
29 }
30
31 } 32
33 public static void main(String args[]) throws Exception {
34 MyClassLoader classLoader = new MyClassLoader("D:/test");
35 Class clazz = classLoader.loadClass("com.tuling.jvm.User1");
36 Object obj = clazz.newInstance();
37 Method method= clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null); System.out.println(clazz.getClassLoader().getClass().getName()); }
}
運(yùn)行結(jié)果: =======自己的加載器加載類調(diào)用方法======= com.tuling.jvm.MyClassLoaderTest$MyClassLoader
雙親委派機(jī)制 JVM類加載器是有親子層級結(jié)構(gòu)的,如下圖
這里類加載其實(shí)就有一個(gè)雙親委派機(jī)制凉当,加載某個(gè)類時(shí)會先委托父加載器尋找目標(biāo)類枣申,找不 到再委托上層父加載器加載,如果所有父加載器在自己的加載類路徑下都找不到目標(biāo)類看杭,則 在自己的類加載路徑中查找并載入目標(biāo)類忠藤。 比如我們的Math類,最先會找應(yīng)用程序類加載器加載楼雹,應(yīng)用程序類加載器會先委托擴(kuò)展類 加載器加載模孩,擴(kuò)展類加載器再委托啟動(dòng)類加載器,頂層啟動(dòng)類加載器在自己的類加載路徑里 找了半天沒找到Math類贮缅,則向下退回加載Math類的請求榨咐,擴(kuò)展類加載器收到回復(fù)就自己加 載,在自己的類加載路徑里找了半天也沒找到Math類谴供,又向下退回Math類的加載請求給應(yīng) 用程序類加載器块茁,應(yīng)用程序類加載器于是在自己的類加載路徑里找Math類,結(jié)果找到了就 自己加載了桂肌。数焊。
雙親委派機(jī)制說簡單點(diǎn)就是,先找父親加載崎场,不行再由兒子自己加載
- 沙箱安全機(jī)制:自己寫的java.lang.String.class類不會被加載佩耳,這樣便可以防止
核心API庫被隨意篡改 - 避免類的重復(fù)加載:當(dāng)父親已經(jīng)加載了該類時(shí),就沒有必要子ClassLoader再加
載一次谭跨,保證被加載類的唯一性
看一個(gè)類加載示例:
packagejava.lang;
publicclassString{
public static void main(String[] args) { System.out.println("**************My String Class**************");
9 運(yùn)行結(jié)果:
10 11 12
錯(cuò)誤: 在類 java.lang.String 中找不到 main 方法, 請將 main 方法定義為: public static void main(String[] args)
否則 JavaFX 應(yīng)用程序類必須擴(kuò)展javafx.application.Application
再來一個(gè)沙箱安全機(jī)制示例干厚,嘗試打破雙親委派機(jī)制,用自定義類加載器加載我們自己實(shí)現(xiàn) 的 java.lang.String.class
publicclassMyClassLoaderTest{
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) { this.classPath = classPath;
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
20
21 protected Class<?> findClass(String name) throws ClassNotFoundException {
22 try{
23 byte[] data = loadByte(name);
24 return defineClass(name, data, 0, data.length);
25 } catch (Exception e) {
26 e.printStackTrace();
27 throw new ClassNotFoundException();
28 }
29 }
30
31 /**
32 * 重寫類加載方法螃宙,實(shí)現(xiàn)自己的加載邏輯蛮瞄,不委派給雙親加載
33 * @param name
34 * @param resolve
35 * @return
36 * @throws ClassNotFoundException
37 */
38 protected Class<?> loadClass(String name, boolean resolve)
39 throws ClassNotFoundException {
40 synchronized (getClassLoadingLock(name)) {
41 // First, check if the class has already been loaded
42 Class<?> c = findLoadedClass(name);
43
44 if(c==null){
45 // If still not found, then invoke findClass in order
46 // to find the class.
47 long t1 = System.nanoTime();
48 c = findClass(name);
49
50 // this is the defining class loader; record the stats
51 sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
52 sun.misc.PerfCounter.getFindClasses().increment();
53 }
54 if (resolve) {
55 resolveClass(c);
56 }
57 return c;
}
59 }
60 }
61
62 public static void main(String args[]) throws Exception {
63 MyClassLoader classLoader = new MyClassLoader("D:/test");
64 //嘗試用自己改寫類加載機(jī)制去加載自己寫的java.lang.String.class
65 Class clazz = classLoader.loadClass("java.lang.String");
66 Object obj = clazz.newInstance();
67 Method method= clazz.getDeclaredMethod("sout", null);
68 method.invoke(obj, null);
69 System.out.println(clazz.getClassLoader().getClass().getName());
70 }
71 }
72
73 運(yùn)行結(jié)果:
74 java.lang.SecurityException:Prohibitedpackagename:java.lang
75 at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659)
76 at java.lang.ClassLoader.defineClass(ClassLoader.java:758)
打破雙親委派
以Tomcat類加載為例,Tomcat 如果使用默認(rèn)的雙親委派類加載機(jī)制行不行? 我們思考一下:Tomcat是個(gè)web容器污呼, 那么它要解決什么問題:
- 一個(gè)web容器可能需要部署兩個(gè)應(yīng)用程序裕坊,不同的應(yīng)用程序可能會依賴同一個(gè)第三方類 庫的不同版本,不能要求同一個(gè)類庫在同一個(gè)服務(wù)器只有一份燕酷,因此要保證每個(gè)應(yīng)用程序的 類庫都是獨(dú)立的籍凝,保證相互隔離周瞎。
- 部署在同一個(gè)web容器中相同的類庫相同的版本可以共享。否則饵蒂,如果服務(wù)器有10個(gè)應(yīng) 用程序声诸,那么要有10份相同的類庫加載進(jìn)虛擬機(jī)。
- web容器也有自己依賴的類庫退盯,不能與應(yīng)用程序的類庫混淆彼乌。基于安全考慮渊迁,應(yīng)該讓容 器的類庫和程序的類庫隔離開來慰照。
- web容器要支持jsp的修改,我們知道琉朽,jsp 文件最終也是要編譯成class文件才能在虛擬 機(jī)中運(yùn)行毒租,但程序運(yùn)行后修改jsp已經(jīng)是司空見慣的事情, web容器需要支持 jsp 修改后不 用重啟箱叁。
再看看我們的問題:Tomcat 如果使用默認(rèn)的雙親委派類加載機(jī)制行不行? 答案是不行的墅垮。為什么?
第一個(gè)問題,如果使用默認(rèn)的類加載器機(jī)制耕漱,那么是無法加載兩個(gè)相同類庫的不同版本的算色, 默認(rèn)的類加器是不管你是什么版本的,只在乎你的全限定類名螟够,并且只有一份灾梦。第二個(gè)問 題,默認(rèn)的類加載器是能夠?qū)崿F(xiàn)的妓笙,因?yàn)樗穆氊?zé)就是保證唯一性斥废。 第三個(gè)問題和第一個(gè)問題一樣。 我們再看第四個(gè)問題给郊,我們想我們要怎么實(shí)現(xiàn)jsp文件的熱加載,jsp 文件其實(shí)也就是class 文件捧灰,那么如果修改了淆九,但類名還是一樣,類加載器會直接取方法區(qū)中已經(jīng)存在的毛俏,修改后 的jsp是不會重新加載的炭庙。那么怎么辦呢?我們可以直接卸載掉這jsp文件的類加載器,所以 你應(yīng)該想到了煌寇,每個(gè)jsp文件對應(yīng)一個(gè)唯一的類加載器焕蹄,當(dāng)一個(gè)jsp文件修改了,就直接卸載 這個(gè)jsp類加載器阀溶。重新創(chuàng)建類加載器腻脏,重新加載jsp文件鸦泳。
Tomcat自定義加載器詳解
tomcat的幾個(gè)主要類加載器:
- commonLoader:Tomcat最基本的類加載器,加載路徑中的class可以被
Tomcat容器本身以及各個(gè)Webapp訪問; - catalinaLoader:Tomcat容器私有的類加載器永品,加載路徑中的class對于
Webapp不可見; - sharedLoader:各個(gè)Webapp共享的類加載器做鹰,加載路徑中的class對于所有
Webapp可見,但是對于Tomcat容器不可見; - WebappClassLoader:各個(gè)Webapp私有的類加載器鼎姐,加載路徑中的class只對
當(dāng)前Webapp可見
從圖中的委派關(guān)系中可以看出: CommonClassLoader能加載的類都可以被CatalinaClassLoader和SharedClassLoader使 用钾麸,從而實(shí)現(xiàn)了公有類庫的共用,而CatalinaClassLoader和SharedClassLoader自己能加 載的類則與對方相互隔離炕桨。 WebAppClassLoader可以使用SharedClassLoader加載到的類饭尝,但各個(gè) WebAppClassLoader實(shí)例之間相互隔離。 而JasperLoader的加載范圍僅僅是這個(gè)JSP文件所編譯出來的那一個(gè).Class文件献宫,它出現(xiàn)的 目的就是為了被丟棄:當(dāng)Web容器檢測到JSP文件被修改時(shí)钥平,會替換掉目前的 JasperLoader的實(shí)例,并通過再建立一個(gè)新的Jsp類加載器來實(shí)現(xiàn)JSP文件的熱加載功能遵蚜。
tomcat 這種類加載機(jī)制違背了java 推薦的雙親委派模型了嗎?答案是:違背了帖池。 我們前面說過,雙親委派機(jī)制要求除了頂層的啟動(dòng)類加載器之外吭净,其余的類加載器都應(yīng)當(dāng)由 自己的父類加載器加載睡汹。
很顯然,tomcat 不是這樣實(shí)現(xiàn)寂殉,tomcat 為了實(shí)現(xiàn)隔離性囚巴,沒有遵守這個(gè)約定
每個(gè) webappClassLoader加載自己的目錄下的class文件,不會傳遞給父類加載器友扰,打破了雙 親委派機(jī)制彤叉。