日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

深入JVM系列(三)之类加载、类加载器、双亲委派机制与常见问题

發(fā)布時(shí)間:2023/12/3 编程问答 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 深入JVM系列(三)之类加载、类加载器、双亲委派机制与常见问题 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

轉(zhuǎn)載自?深入JVM系列(三)之類加載、類加載器、雙親委派機(jī)制與常見問題

?

一.概述

定義:虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的java類型。類加載和連接的過程都是在運(yùn)行期間完成的。

?

二. 類的加載方式

1):本地編譯好的class中直接加載

2):網(wǎng)絡(luò)加載:java.net.URLClassLoader可以加載url指定的類

3):從jar、zip等等壓縮文件加載類,自動(dòng)解析jar文件找到class文件去加載util類

4):從java源代碼文件動(dòng)態(tài)編譯成為class文件

?

三.類加載的時(shí)機(jī)

1. 類加載的生命周期:加載(Loading)-->驗(yàn)證(Verification)-->準(zhǔn)備(Preparation)-->解析(Resolution)-->初始化(Initialization)-->使用(Using)-->卸載(Unloading)

2.?加載:這有虛擬機(jī)自行決定。

3.?初始化階段:

a)?遇到new、getstatic、putstatic、invokestatic這4個(gè)字節(jié)碼指令時(shí),如果類沒有進(jìn)行過初始化,出發(fā)初始化操作。

b)?使用java.lang.reflect包的方法對(duì)類進(jìn)行反射調(diào)用時(shí)。

c)?當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)其父類還沒有執(zhí)行初始化則進(jìn)行初始化。

d)?虛擬機(jī)啟動(dòng)時(shí)用戶需要指定一個(gè)需要執(zhí)行的主類,虛擬機(jī)首先初始化這個(gè)主類。

注意:接口與類的初始化規(guī)則在第三點(diǎn)不同,接口不要?dú)馑械母附涌诙歼M(jìn)行初始化。

?

四.類加載的過程

4.1.?加載

a)?加載階段的工作

i.?通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流。

ii.?將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。

iii.?在java堆中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象,做為方法區(qū)這些數(shù)據(jù)的訪問入口。

b)?加載階段完成之后二進(jìn)制字節(jié)流就按照虛擬機(jī)所需的格式存儲(chǔ)在方區(qū)去中。

4.2.?驗(yàn)證

這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求。

a)?文件格式驗(yàn)證:驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理。

b)?元數(shù)據(jù)驗(yàn)證:對(duì)字節(jié)碼描述的信息進(jìn)行語義分析,以確保其描述的信息符合java語言規(guī)范的要求。

c)?字節(jié)碼驗(yàn)證:這個(gè)階段的主要工作是進(jìn)行數(shù)據(jù)流和控制流的分析。任務(wù)是確保被驗(yàn)證類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的行為。

d)?符號(hào)引用驗(yàn)證:這一階段發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)換為直接引用的時(shí)候(解析階段),主要是對(duì)類自身以外的信息進(jìn)行匹配性的校驗(yàn)。目的是確保解析動(dòng)作能夠正常執(zhí)行。

4.3.?準(zhǔn)備

準(zhǔn)備階段是正式為變量分配內(nèi)存并設(shè)置初始值,這些內(nèi)存都將在方法區(qū)中進(jìn)行分配,這里的變量?jī)H包括類標(biāo)量不包括實(shí)例變量。

4.4.?解析

解析是虛擬機(jī)將常量池的符號(hào)引用替換為直接引用的過程。

a)?符號(hào)引用:符號(hào)引用以一組符號(hào)來描述所引用的目標(biāo),符號(hào)可以是任意形式的字面量,只要使用時(shí)能無歧義地定位到目標(biāo)即可。符號(hào)引用與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局無關(guān),引用的目標(biāo)并不一定已經(jīng)加載到內(nèi)存中。

b)?直接引用:直接引用可以是直接指向目標(biāo)的指針,相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。直接飲用是與內(nèi)存布局相關(guān)的。

c)?類或接口的解析

d)?字段的解析

e)?類方法解析

f)?接口方法解析

4.5.?初始化

是根據(jù)程序員制定的主觀計(jì)劃區(qū)初始化變量和其他資源,或者可以從另外一個(gè)角度來表達(dá):初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程。

?

五.?JVM三種預(yù)定義類型類加載器

當(dāng)一個(gè) JVM 啟動(dòng)的時(shí)候,Java 缺省開始使用如下三種類型類裝入器:

啟動(dòng)(Bootstrap)類加載器:引導(dǎo)類裝入器是用本地代碼實(shí)現(xiàn)的類裝入器,它負(fù)責(zé)將 <Java_Runtime_Home>/lib 下面的類庫加載到內(nèi)存中。由于引導(dǎo)類加載器涉及到虛擬機(jī)本地實(shí)現(xiàn)細(xì)節(jié),開發(fā)者無法直接獲取到啟動(dòng)類加載器的引用,所以不允許直接通過引用進(jìn)行操作。

標(biāo)準(zhǔn)擴(kuò)展(Extension)類加載器:擴(kuò)展類加載器是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader) 實(shí)現(xiàn)的。它負(fù)責(zé)將?

< Java_Runtime_Home >/lib/ext 或者由系統(tǒng)變量 java.ext.dir 指定位置中的類庫加載到內(nèi)存中。開發(fā)者可以直接使用標(biāo)準(zhǔn)擴(kuò)展類加載器。

系統(tǒng)(System)類加載器:系統(tǒng)類加載器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實(shí)現(xiàn)的。它負(fù)責(zé)將系統(tǒng)類路徑(CLASSPATH)中指定的類庫加載到內(nèi)存中。開發(fā)者可以直接使用系統(tǒng)類加載器。

除了以上列舉的三種類加載器,還有一種比較特殊的類型就是線程上下文類加載器,這個(gè)將在后面單獨(dú)介紹。

a. Bootstrap ClassLoader/啟動(dòng)類加載器

主要負(fù)責(zé)jdk_home/lib目錄下的核心 api 或 -Xbootclasspath 選項(xiàng)指定的jar包裝入工作.

b. Extension ClassLoader/擴(kuò)展類加載器

主要負(fù)責(zé)jdk_home/lib/ext目錄下的jar包或 -Djava.ext.dirs 指定目錄下的jar包裝入工作

c. System ClassLoader/系統(tǒng)類加載器

主要負(fù)責(zé)java -classpath/-Djava.class.path所指的目錄下的類與jar包裝入工作.

d. ?User Custom ClassLoader/用戶自定義類加載器(java.lang.ClassLoader的子類)

在程序運(yùn)行期間, 通過java.lang.ClassLoader的子類動(dòng)態(tài)加載class文件, 體現(xiàn)java動(dòng)態(tài)實(shí)時(shí)類裝入特性.

?

六. 類加載雙親委派機(jī)制介紹和分析

???????在這里,需要著重說明的是,JVM在加載類時(shí)默認(rèn)采用的是雙親委派機(jī)制。通俗的講,就是某個(gè)特定的類加載器在接到加載類的請(qǐng)求時(shí),首先將加載任務(wù)委托給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務(wù),就成功返回;只有父類加載器無法完成此加載任務(wù)時(shí),才自己去加載。關(guān)于虛擬機(jī)默認(rèn)的雙親委派機(jī)制,我們可以從系統(tǒng)類加載器和標(biāo)準(zhǔn)擴(kuò)展類加載器為例作簡(jiǎn)單分析。


? ? ? ?圖一 標(biāo)準(zhǔn)擴(kuò)展類加載器繼承層次圖

????????圖二 系統(tǒng)類加載器繼承層次圖

?

? ? 通過圖一和圖二我們可以看出,類加載器均是繼承自java.lang.ClassLoader抽象類。我們下面我們就看簡(jiǎn)要介紹一下java.lang.ClassLoader中幾個(gè)最重要的方法:

//加載指定名稱(包括包名)的二進(jìn)制類型,供用戶調(diào)用的接口 public Class<?> loadClass(String name) throws ClassNotFoundException{//…} //加載指定名稱(包括包名)的二進(jìn)制類型,同時(shí)指定是否解析(但是,這里的resolve參數(shù)不一定真正能達(dá)到解析的效果~_~),供繼承用 protectedsynchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{//…} //findClass方法一般被loadClass方法調(diào)用去加載指定名稱類,供繼承用 protected Class<?> findClass(String name) throws ClassNotFoundException {//…} //定義類型,一般在findClass方法中讀取到對(duì)應(yīng)字節(jié)碼后調(diào)用,可以看出不可繼承(說明:JVM已經(jīng)實(shí)現(xiàn)了對(duì)應(yīng)的具體功能,解析對(duì)應(yīng)的字節(jié)碼,產(chǎn)生對(duì)應(yīng)的內(nèi)部數(shù)據(jù)結(jié)構(gòu)放置到方法區(qū),所以無需覆寫,直接調(diào)用就可以了) protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{//…}

???????通過進(jìn)一步分析標(biāo)準(zhǔn)擴(kuò)展類加載器(sun.misc.Launcher$ExtClassLoader)和系統(tǒng)類加載器(sun.misc.Launcher$AppClassLoader)的代碼以及其公共父類(java.net.URLClassLoader和java.security.SecureClassLoader)的代碼可以看出,都沒有覆寫java.lang.ClassLoader中默認(rèn)的加載委派規(guī)則---loadClass(…)方法。既然這樣,我們就可以通過分析java.lang.ClassLoader中的loadClass(String name)方法的代碼就可以分析出虛擬機(jī)默認(rèn)采用的雙親委派機(jī)制到底是什么模樣:

public Class<?> loadClass(String name)throws ClassNotFoundException { return loadClass(name,false); } protectedsynchronized Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException { //首先判斷該類型是否已經(jīng)被加載 Class c = findLoadedClass(name); if (c ==null) { //如果沒有被加載,就委托給父類加載或者委派給啟動(dòng)類加載器加載 try { if (parent !=null) { //如果存在父類加載器,就委派給父類加載器加載 c = parent.loadClass(name,false); }else { //如果不存在父類加載器,就檢查是否是由啟動(dòng)類加載器加載的類,通過調(diào)用本地方法native Class findBootstrapClass(String name) c = findBootstrapClass0(name); } }catch (ClassNotFoundException e) { //如果父類加載器和啟動(dòng)類加載器都不能完成加載任務(wù),才調(diào)用自身的加載功能 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; }

???通過上面的代碼分析,我們可以對(duì)JVM采用的雙親委派類加載機(jī)制有了更感性的認(rèn)識(shí),下面我們就接著分析一下啟動(dòng)類加載器、標(biāo)準(zhǔn)擴(kuò)展類加載器和系統(tǒng)類加載器三者之間的關(guān)系。可能大家已經(jīng)從各種資料上面看到了如下類似的一幅圖片:


????????????????????
? ? ? ? ?圖三 類加載器默認(rèn)委派關(guān)系圖

?

上面圖片給人的直觀印象是系統(tǒng)類加載器的父類加載器是標(biāo)準(zhǔn)擴(kuò)展類加載器,標(biāo)準(zhǔn)擴(kuò)展類加載器的父類加載器是啟動(dòng)類加載器,下面我們就用代碼具體測(cè)試一下:

示例代碼:

public static void main(String[] args) { try { System.out.println(ClassLoader.getSystemClassLoader()); System.out.println(ClassLoader.getSystemClassLoader().getParent(); System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent()); } catch (Exception e) { e.printStackTrace(); } }

說明:通過java.lang.ClassLoader.getSystemClassLoader()可以直接獲取到系統(tǒng)類加載器。

?

代碼輸出如下:??
sun.misc.Launcher$AppClassLoader@197d257??
sun.misc.Launcher$ExtClassLoader@7259da??
null?

???通過以上的代碼輸出,我們可以判定系統(tǒng)類加載器的父加載器是標(biāo)準(zhǔn)擴(kuò)展類加載器,但是我們?cè)噲D獲取標(biāo)準(zhǔn)擴(kuò)展類加載器的父類加載器時(shí)確得到了null,就是說標(biāo)準(zhǔn)擴(kuò)展類加載器本身強(qiáng)制設(shè)定父類加載器為null。我們還是借助于代碼分析一下:

?????我們首先看一下java.lang.ClassLoader抽象類中默認(rèn)實(shí)現(xiàn)的兩個(gè)構(gòu)造函數(shù):

protected ClassLoader() { SecurityManager security = System.getSecurityManager(); if (security !=null) { security.checkCreateClassLoader(); } //默認(rèn)將父類加載器設(shè)置為系統(tǒng)類加載器,getSystemClassLoader()獲取系統(tǒng)類加載器 this.parent = getSystemClassLoader(); initialized =true; } protected ClassLoader(ClassLoader parent) { SecurityManager security = System.getSecurityManager(); if (security !=null) { security.checkCreateClassLoader(); } //強(qiáng)制設(shè)置父類加載器 this.parent = parent; initialized =true; }

???我們?cè)倏匆幌翪lassLoader抽象類中parent成員的聲明:

// The parent class loader for delegation e ClassLoaderparent;

聲明為私有變量的同時(shí)并沒有對(duì)外提供可供派生類訪問的public或者protected設(shè)置器接口(對(duì)應(yīng)的setter方法),結(jié)合前面的測(cè)試代碼的輸出,我們可以推斷出

1.系統(tǒng)類加載器(AppClassLoader)調(diào)用ClassLoader(ClassLoader parent)構(gòu)造函數(shù)將父類加載器設(shè)置為標(biāo)準(zhǔn)擴(kuò)展類加載器(ExtClassLoader)。(因?yàn)槿绻粡?qiáng)制設(shè)置,默認(rèn)會(huì)通過調(diào)用getSystemClassLoader()方法獲取并設(shè)置成系統(tǒng)類加載器,這顯然和測(cè)試輸出結(jié)果不符。)

2.擴(kuò)展類加載器(ExtClassLoader)調(diào)用ClassLoader(ClassLoader parent)構(gòu)造函數(shù)將父類加載器設(shè)置為null。(因?yàn)槿绻粡?qiáng)制設(shè)置,默認(rèn)會(huì)通過調(diào)用getSystemClassLoader()方法獲取并設(shè)置成系統(tǒng)類加載器,這顯然和測(cè)試輸出結(jié)果不符。)

?????現(xiàn)在我們可能會(huì)有這樣的疑問:擴(kuò)展類加載器(ExtClassLoader)的父類加載器被強(qiáng)制設(shè)置為null了,那么擴(kuò)展類加載器為什么還能將加載任務(wù)委派給啟動(dòng)類加載器呢?

??

? ? 圖四 標(biāo)準(zhǔn)擴(kuò)展類加載器和系統(tǒng)類加載器成員大綱視圖

?
? ? 圖五擴(kuò)展類加載器和系統(tǒng)類加載器公共父類成員大綱視圖

????通過圖四和圖五可以看出,標(biāo)準(zhǔn)擴(kuò)展類加載器和系統(tǒng)類加載器及其父類(java.net.URLClassLoader和java.security.SecureClassLoader)都沒有覆寫java.lang.ClassLoader中默認(rèn)的加載委派規(guī)則---loadClass(…)方法。有關(guān)java.lang.ClassLoader中默認(rèn)的加載委派規(guī)則前面已經(jīng)分析過,如果父加載器為null,則會(huì)調(diào)用本地方法進(jìn)行啟動(dòng)類加載嘗試。所以,圖三中,啟動(dòng)類加載器、標(biāo)準(zhǔn)擴(kuò)展類加載器和系統(tǒng)類加載器之間的委派關(guān)系事實(shí)上是仍就成立的。(在后面的用戶自定義類加載器部分,還會(huì)做更深入的分析)。

?

七. 類加載雙親委派示例

以上已經(jīng)簡(jiǎn)要介紹了虛擬機(jī)默認(rèn)使用的啟動(dòng)類加載器、標(biāo)準(zhǔn)擴(kuò)展類加載器和系統(tǒng)類加載器,并以三者為例結(jié)合JDK代碼對(duì)JVM默認(rèn)使用的雙親委派類加載機(jī)制做了分析。下面我們就來看一個(gè)綜合的例子。首先在eclipse中建立一個(gè)簡(jiǎn)單的java應(yīng)用工程,然后寫一個(gè)簡(jiǎn)單的JavaBean如下:

package classloader.test.bean; publicclass TestBean { public TestBean() {} }

?

在現(xiàn)有當(dāng)前工程中另外建立一測(cè)試類(ClassLoaderTest.java)內(nèi)容如下:

測(cè)試一:

publicclass ClassLoaderTest { publicstaticvoid main(String[] args) { try { //查看當(dāng)前系統(tǒng)類路徑中包含的路徑條目 System.out.println(System.getProperty("java.class.path")); //調(diào)用加載當(dāng)前類的類加載器(這里即為系統(tǒng)類加載器)加載TestBean Class typeLoaded = Class.forName("classloader.test.bean.TestBean"); //查看被加載的TestBean類型是被那個(gè)類加載器加載的 System.out.println(typeLoaded.getClassLoader()); }catch (Exception e) { e.printStackTrace(); } } }

對(duì)應(yīng)的輸出如下:

D:"DEMO"dev"Study"ClassLoaderTest"bin sun.misc.Launcher$AppClassLoader@197d257

(說明:當(dāng)前類路徑默認(rèn)的含有的一個(gè)條目就是工程的輸出目錄)

測(cè)試二:

將當(dāng)前工程輸出目錄下的…/classloader/test/bean/TestBean.class打包進(jìn)test.jar剪貼到< Java_Runtime_Home >/lib/ext目錄下(現(xiàn)在工程輸出目錄下和JRE擴(kuò)展目錄下都有待加載類型的class文件)。再運(yùn)行測(cè)試測(cè)試代碼,結(jié)果如下:

D:"DEMO"dev"Study"ClassLoaderTest"bin??
sun.misc.Launcher$ExtClassLoader@7259da??

對(duì)比測(cè)試一和測(cè)試二,我們明顯可以驗(yàn)證前面說的雙親委派機(jī)制,系統(tǒng)類加載器在接到加載classloader.test.bean.TestBean類型的請(qǐng)求時(shí),首先將請(qǐng)求委派給父類加載器(標(biāo)準(zhǔn)擴(kuò)展類加載器),標(biāo)準(zhǔn)擴(kuò)展類加載器搶先完成了加載請(qǐng)求。

測(cè)試三:

test.jar拷貝一份到< Java_Runtime_Home >/lib下,運(yùn)行測(cè)試代碼,輸出如下:

D:"DEMO"dev"Study"ClassLoaderTest"bin??
sun.misc.Launcher$ExtClassLoader@7259da??

?

??測(cè)試三和測(cè)試二輸出結(jié)果一致。那就是說,放置到< Java_Runtime_Home >/lib目錄下的TestBean對(duì)應(yīng)的class字節(jié)碼并沒有被加載,這其實(shí)和前面講的雙親委派機(jī)制并不矛盾。虛擬機(jī)出于安全等因素考慮,不會(huì)加載< Java_Runtime_Home >/lib存在的陌生類開發(fā)者通過將要加載的非JDK自身的類放置到此目錄下期待啟動(dòng)類加載器加載是不可能的。做個(gè)進(jìn)一步驗(yàn)證,刪除< Java_Runtime_Home >/lib/ext目錄下和工程輸出目錄下的TestBean對(duì)應(yīng)的class文件,然后再運(yùn)行測(cè)試代碼,則將會(huì)有ClassNotFoundException異常拋出。有關(guān)這個(gè)問題,大家可以在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中設(shè)置相應(yīng)斷點(diǎn)運(yùn)行測(cè)試三進(jìn)行調(diào)試,會(huì)發(fā)現(xiàn)findBootstrapClass0()會(huì)拋出異常,然后在下面的findClass方法中被加載,當(dāng)前運(yùn)行的類加載器正是擴(kuò)展類加載器(sun.misc.Launcher$ExtClassLoader),這一點(diǎn)可以通過JDT中變量視圖查看驗(yàn)證。

?

八. 程序動(dòng)態(tài)擴(kuò)展方式

Java的連接模型允許用戶運(yùn)行時(shí)擴(kuò)展引用程序,既可以通過當(dāng)前虛擬機(jī)中預(yù)定義的加載器加載編譯時(shí)已知的類或者接口,又允許用戶自行定義類裝載器,在運(yùn)行時(shí)動(dòng)態(tài)擴(kuò)展用戶的程序。通過用戶自定義的類裝載器,你的程序可以裝載在編譯時(shí)并不知道或者尚未存在的類或者接口,并動(dòng)態(tài)連接它們并進(jìn)行有選擇的解析。

運(yùn)行時(shí)動(dòng)態(tài)擴(kuò)展java應(yīng)用程序有如下兩個(gè)途徑:

8.1.調(diào)用java.lang.Class.forName(…)

這個(gè)方法其實(shí)在前面已經(jīng)討論過,在后面的問題2解答中說明了該方法調(diào)用會(huì)觸發(fā)那個(gè)類加載器開始加載任務(wù)。這里需要說明的是多參數(shù)版本的forName(…)方法:

public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException

這里的initialize參數(shù)是很重要的,可以覺得被加載同時(shí)是否完成初始化的工作(說明: 單參數(shù)版本的forName方法默認(rèn)是不完成初始化的).有些場(chǎng)景下,需要將initialize設(shè)置為true來強(qiáng)制加載同時(shí)完成初始化,例如典型的就是利用DriverManager進(jìn)行JDBC驅(qū)動(dòng)程序類注冊(cè)的問題,因?yàn)槊恳粋€(gè)JDBC驅(qū)動(dòng)程序類的靜態(tài)初始化方法都用DriverManager注冊(cè)驅(qū)動(dòng)程序,這樣才能被應(yīng)用程序使用,這就要求驅(qū)動(dòng)程序類必須被初始化,而不單單被加載.

8.2.用戶自定義類加載器

通過前面的分析,我們可以看出,除了和本地實(shí)現(xiàn)密切相關(guān)的啟動(dòng)類加載器之外,包括標(biāo)準(zhǔn)擴(kuò)展類加載器和系統(tǒng)類加載器在內(nèi)的所有其他類加載器我們都可以當(dāng)做自定義類加載器來對(duì)待,唯一區(qū)別是是否被虛擬機(jī)默認(rèn)使用。前面的內(nèi)容中已經(jīng)對(duì)java.lang.ClassLoader抽象類中的幾個(gè)重要的方法做了介紹,這里就簡(jiǎn)要敘述一下一般用戶自定義類加載器的工作流程吧(可以結(jié)合后面問題解答一起看):

1、首先檢查請(qǐng)求的類型是否已經(jīng)被這個(gè)類裝載器裝載到命名空間中了,如果已經(jīng)裝載,直接返回;否則轉(zhuǎn)入步驟2

2、委派類加載請(qǐng)求給父類加載器(更準(zhǔn)確的說應(yīng)該是雙親類加載器,真?zhèn)€虛擬機(jī)中各種類加載器最終會(huì)呈現(xiàn)樹狀結(jié)構(gòu)),如果父類加載器能夠完成,則返回父類加載器加載的Class實(shí)例;否則轉(zhuǎn)入步驟3

3、調(diào)用本類加載器的findClass(…)方法,試圖獲取對(duì)應(yīng)的字節(jié)碼,如果獲取的到,則調(diào)用defineClass(…)導(dǎo)入類型到方法區(qū);如果獲取不到對(duì)應(yīng)的字節(jié)碼或者其他原因失敗,返回異常給loadClass(…), loadClass(…)轉(zhuǎn)拋異常,終止加載過程(注意:這里的異常種類不止一種)。

???????(說明:這里說的自定義類加載器是指JDK 1.2以后版本的寫法,即不覆寫改變java.lang.loadClass(…)已有委派邏輯情況下)

?

九. 常見問題分析

9.1.由不同的類加載器加載的指定類型還是相同的類型嗎?

在Java中,一個(gè)類用其完全匹配類名(fully qualified class name)作為標(biāo)識(shí),這里指的完全匹配類名包括包名和類名。但在JVM中一個(gè)類用其全名和一個(gè)加載類ClassLoader的實(shí)例作為唯一標(biāo)識(shí),不同類加載器加載的類將被置于不同的命名空間.我們可以用兩個(gè)自定義類加載器去加載某自定義類型(注意,不要將自定義類型的字節(jié)碼放置到系統(tǒng)路徑或者擴(kuò)展路徑中,否則會(huì)被系統(tǒng)類加載器或擴(kuò)展類加載器搶先加載),然后用獲取到的兩個(gè)Class實(shí)例進(jìn)行java.lang.Object.equals(…)判斷,將會(huì)得到不相等的結(jié)果。這個(gè)大家可以寫兩個(gè)自定義的類加載器去加載相同的自定義類型,然后做個(gè)判斷;同時(shí),可以測(cè)試加載java.*類型,然后再對(duì)比測(cè)試一下測(cè)試結(jié)果。

9.2.在代碼中直接調(diào)用Class.forName(String name)方法,到底會(huì)觸發(fā)那個(gè)類加載器進(jìn)行類加載行為?

Class.forName(String name)默認(rèn)會(huì)使用調(diào)用類的類加載器來進(jìn)行類加載。我們直接來分析一下對(duì)應(yīng)的jdk的代碼:

//java.lang.Class.java publicstatic Class<?>forName(String className)throws ClassNotFoundException { return forName0(className,true, ClassLoader.getCallerClassLoader()); } //java.lang.ClassLoader.java // Returns the invoker's class loader, or null if none. static ClassLoader getCallerClassLoader() { // 獲取調(diào)用類(caller)的類型 Class caller = Reflection.getCallerClass(3); // This can be null if the VM is requesting it if (caller ==null) { returnnull; } //調(diào)用java.lang.Class中本地方法獲取加載該調(diào)用類(caller)的ClassLoader return caller.getClassLoader0(); } //java.lang.Class.java //虛擬機(jī)本地實(shí)現(xiàn),獲取當(dāng)前類的類加載器,前面介紹的Class的getClassLoader()也使用此方法 native ClassLoader getClassLoader0();

9.3.在編寫自定義類加載器時(shí),如果沒有設(shè)定父加載器,那么父加載器是?

前面講過,在不指定父類加載器的情況下,默認(rèn)采用系統(tǒng)類加載器。可能有人覺得不明白,現(xiàn)在我們來看一下JDK對(duì)應(yīng)的代碼實(shí)現(xiàn)。眾所周知,我們編寫自定義的類加載器直接或者間接繼承自java.lang.ClassLoader抽象類,對(duì)應(yīng)的無參默認(rèn)構(gòu)造函數(shù)實(shí)現(xiàn)如下:

//摘自java.lang.ClassLoader.java protected ClassLoader() { SecurityManager security = System.getSecurityManager(); if (security !=null) { security.checkCreateClassLoader(); } this.parent = getSystemClassLoader(); initialized =true; }

我們?cè)賮砜匆幌聦?duì)應(yīng)的getSystemClassLoader()方法的實(shí)現(xiàn):

privatestaticsynchronizedvoid initSystemClassLoader() { //... sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); scl = l.getClassLoader(); //... }

我們可以寫簡(jiǎn)單的測(cè)試代碼來測(cè)試一下:

System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());

本機(jī)對(duì)應(yīng)輸出如下:

sun.misc.Launcher$AppClassLoader@197d257??

所以,我們現(xiàn)在可以相信當(dāng)自定義類加載器沒有指定父類加載器的情況下,默認(rèn)的父類加載器即為系統(tǒng)類加載器。同時(shí),我們可以得出如下結(jié)論:

即時(shí)用戶自定義類加載器不指定父類加載器,那么,同樣可以加載如下三個(gè)地方的類:

1.???<Java_Runtime_Home>/lib下的類

2.???< Java_Runtime_Home >/lib/ext下或者由系統(tǒng)變量java.ext.dir指定位置中的類

3.???當(dāng)前工程類路徑下或者由系統(tǒng)變量java.class.path指定位置中的類

9.4.在編寫自定義類加載器時(shí),如果將父類加載器強(qiáng)制設(shè)置為null,那么會(huì)有什么影響?如果自定義的類加載器不能加載指定類,就肯定會(huì)加載失敗嗎?

JVM規(guī)范中規(guī)定如果用戶自定義的類加載器將父類加載器強(qiáng)制設(shè)置為null,那么會(huì)自動(dòng)將啟動(dòng)類加載器設(shè)置為當(dāng)前用戶自定義類加載器的父類加載器(這個(gè)問題前面已經(jīng)分析過了)。同時(shí),我們可以得出如下結(jié)論:

即時(shí)用戶自定義類加載器不指定父類加載器,那么,同樣可以加載到<Java_Runtime_Home>/lib下的類,但此時(shí)就不能夠加載<Java_Runtime_Home>/lib/ext目錄下的類了。

???說明:問題3和問題4的推斷結(jié)論是基于用戶自定義的類加載器本身延續(xù)了java.lang.ClassLoader.loadClass(…)默認(rèn)委派邏輯,如果用戶對(duì)這一默認(rèn)委派邏輯進(jìn)行了改變,以上推斷結(jié)論就不一定成立了,詳見問題5。

9.5.編寫自定義類加載器時(shí),一般有哪些注意點(diǎn)?

9.5.1.一般盡量不要覆寫已有的loadClass(…)方法中的委派邏輯

一般在JDK 1.2之前的版本才這樣做,而且事實(shí)證明,這樣做極有可能引起系統(tǒng)默認(rèn)的類加載器不能正常工作。在JVM規(guī)范和JDK文檔中(1.2或者以后版本中),都沒有建議用戶覆寫loadClass(…)方法,相比而言,明確提示開發(fā)者在開發(fā)自定義的類加載器時(shí)覆寫findClass(…)邏輯。舉一個(gè)例子來驗(yàn)證該問題:

//用戶自定義類加載器WrongClassLoader.Java(覆寫loadClass邏輯) publicclassWrongClassLoaderextends ClassLoader { public Class<?> loadClass(String name)throws ClassNotFoundException { returnthis.findClass(name); } protected Class<?> findClass(String name)throws ClassNotFoundException { //假設(shè)此處只是到工程以外的特定目錄D:/library下去加載類 具體實(shí)現(xiàn)代碼省略 } }

???通過前面的分析我們已經(jīng)知道,用戶自定義類加載器(WrongClassLoader)的默認(rèn)的類加載器是系統(tǒng)類加載器,但是現(xiàn)在問題4種的結(jié)論就不成立了。大家可以簡(jiǎn)單測(cè)試一下,現(xiàn)在<Java_Runtime_Home>/lib、< Java_Runtime_Home >/lib/ext和工程類路徑上的類都加載不上了。

//問題5測(cè)試代碼一 publicclass WrongClassLoaderTest { publicstaticvoid main(String[] args) { try { WrongClassLoader loader =new WrongClassLoader(); Class classLoaded = loader.loadClass("beans.Account"); System.out.println(classLoaded.getName()); System.out.println(classLoaded.getClassLoader()); }catch (Exception e) { e.printStackTrace(); } } }

(說明:D:"classes"beans"Account.class物理存在的)

輸出結(jié)果:

?java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系統(tǒng)找不到指定的路徑。)??
? ? at java.io.FileInputStream.open(Native Method)??
? ? at java.io.FileInputStream.<init>(FileInputStream.java:106)??
? ? at WrongClassLoader.findClass(WrongClassLoader.java:40)??
? ? at WrongClassLoader.loadClass(WrongClassLoader.java:29)??
? ? at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)??
? ? at java.lang.ClassLoader.defineClass1(Native Method)??
? ? at java.lang.ClassLoader.defineClass(ClassLoader.java:620)??
? ? at java.lang.ClassLoader.defineClass(ClassLoader.java:400)??
? ? at WrongClassLoader.findClass(WrongClassLoader.java:43)??
? ? at WrongClassLoader.loadClass(WrongClassLoader.java:29)??
? ? at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)??
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object??
? ? at java.lang.ClassLoader.defineClass1(Native Method)??
? ? at java.lang.ClassLoader.defineClass(ClassLoader.java:620)??
? ? at java.lang.ClassLoader.defineClass(ClassLoader.java:400)??
? ? at WrongClassLoader.findClass(WrongClassLoader.java:43)??
? ? at WrongClassLoader.loadClass(WrongClassLoader.java:29)??
? ? at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)?

?

這說明,連要加載的類型的超類型java.lang.Object都加載不到了。這里列舉的由于覆寫loadClass(…)引起的邏輯錯(cuò)誤明顯是比較簡(jiǎn)單的,實(shí)際引起的邏輯錯(cuò)誤可能復(fù)雜的多。

//問題5測(cè)試二 //用戶自定義類加載器WrongClassLoader.Java(不覆寫loadClass邏輯) publicclassWrongClassLoaderextends ClassLoader { protected Class<?> findClass(String name)throws ClassNotFoundException { //假設(shè)此處只是到工程以外的特定目錄D:/library下去加載類 具體實(shí)現(xiàn)代碼省略 } }

將自定義類加載器代碼WrongClassLoader.Java做以上修改后,再運(yùn)行測(cè)試代碼,輸出結(jié)果如下:

beans.Account??
WrongClassLoader@1c78e57??

?

這說明,beans.Account加載成功,且是由自定義類加載器WrongClassLoader加載。

這其中的原因分析,我想這里就不必解釋了,大家應(yīng)該可以分析的出來了。

9.5.2.正確設(shè)置父類加載器

通過上面問題4和問題5的分析我們應(yīng)該已經(jīng)理解,個(gè)人覺得這是自定義用戶類加載器時(shí)最重要的一點(diǎn),但常常被忽略或者輕易帶過。有了前面JDK代碼的分析作為基礎(chǔ),我想現(xiàn)在大家都可以隨便舉出例子了。

9.5.3.保證findClass(String)方法的邏輯正確性

事先盡量準(zhǔn)確理解待定義的類加載器要完成的加載任務(wù),確保最大程度上能夠獲取到對(duì)應(yīng)的字節(jié)碼內(nèi)容。

9.6.如何在運(yùn)行時(shí)判斷系統(tǒng)類加載器能加載哪些路徑下的類?

一是可以直接調(diào)用ClassLoader.getSystemClassLoader()或者其他方式獲取到系統(tǒng)類加載器(系統(tǒng)類加載器和擴(kuò)展類加載器本身都派生自URLClassLoader),調(diào)用URLClassLoader中的getURLs()方法可以獲取到;

二是可以直接通過獲取系統(tǒng)屬性java.class.path 來查看當(dāng)前類路徑上的條目信息 , System.getProperty("java.class.path")

9.7.如何在運(yùn)行時(shí)判斷標(biāo)準(zhǔn)擴(kuò)展類加載器能加載哪些路徑下的類?

方法之一:

try { URL[] extURLs = ((URLClassLoader)ClassLoader.getSystemClassLoader().getParent()).getURLs(); for (int i = 0; i < extURLs.length; i++) { System.out.println(extURLs[i]); } } catch (Exception e) {//…}

本機(jī)對(duì)應(yīng)輸出如下:

file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/dnsns.jar??
file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/localedata.jar??
file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/sunjce_provider.jar??
file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/sunpkcs11.jar??

?

十、再分析類加載

10.1.類加載器的特性

1,?每個(gè)ClassLoader都維護(hù)了一份自己的名稱空間,?同一個(gè)名稱空間里不能出現(xiàn)兩個(gè)同名的類。
2,?為了實(shí)現(xiàn)java安全沙箱模型頂層的類加載器安全機(jī)制, java默認(rèn)采用了?”?雙親委派的加載鏈?”?結(jié)構(gòu).

如下圖:

Class Diagram:

類圖中,?BootstrapClassLoader是一個(gè)單獨(dú)的java類,?其實(shí)在這里,?不應(yīng)該叫他是一個(gè)java類。
因?yàn)?#xff0c;?它已經(jīng)完全不用java實(shí)現(xiàn)了。

它是在jvm啟動(dòng)時(shí),?就被構(gòu)造起來的,?負(fù)責(zé)java平臺(tái)核心庫。(具體上面已經(jīng)有介紹)

啟動(dòng)類加載實(shí)現(xiàn)?(其實(shí)我們不用關(guān)心這塊,?但是有興趣的,?可以研究一下?):
bootstrap classLoader?類加載原理探索

10.2.自定義類加載器加載一個(gè)類的步驟

?

ClassLoader?類加載邏輯分析,?以下邏輯是除?BootstrapClassLoader?外的類加載器加載流程:

// 檢查類是否已被裝載過 Class c = findLoadedClass(name); if (c == null ) { // 指定類未被裝載過 try { if (parent != null ) { // 如果父類加載器不為空, 則委派給父類加載 c = parent.loadClass(name, false ); } else { // 如果父類加載器為空, 則委派給啟動(dòng)類加載加載 c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { // 啟動(dòng)類加載器或父類加載器拋出異常后, 當(dāng)前類加載器將其 // 捕獲, 并通過findClass方法, 由自身加載 c = findClass(name); } }

10.3.用Class.forName加載類

Class.forName使用的是被調(diào)用者的類加載器來加載類的.
這種特性,?證明了java類加載器中的名稱空間是唯一的,?不會(huì)相互干擾.

即在一般情況下,?保證同一個(gè)類中所關(guān)聯(lián)的其他類都是由當(dāng)前類的類加載器所加載的.

public static Class forName(String className) throws ClassNotFoundException { return forName0(className, true , ClassLoader.getCallerClassLoader()); } /** Called after security checks have been made. */ private static native Class forName0(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException;

上圖中?ClassLoader.getCallerClassLoader?就是得到調(diào)用當(dāng)前forName方法的類的類加載器

10.4.線程上下文類加載器

java默認(rèn)的線程上下文類加載器是?系統(tǒng)類加載器(AppClassLoader).

// Now create the class loader to use to launch the application try { loader = AppClassLoader.getAppClassLoader(extcl); } catch (IOException e) { throw new InternalError( "Could not create application class loader" ); } // Also set the context class loader for the primordial thread. Thread.currentThread().setContextClassLoader(loader);

以上代碼摘自sun.misc.Launch的無參構(gòu)造函數(shù)Launch()。

使用線程上下文類加載器,?可以在執(zhí)行線程中,?拋棄雙親委派加載鏈模式,?使用線程上下文里的類加載器加載類.


典型的例子有,?通過線程上下文來加載第三方庫jndi實(shí)現(xiàn),?而不依賴于雙親委派.

大部分java app服務(wù)器(jboss, tomcat..)也是采用contextClassLoader來處理web服務(wù)。


還有一些采用?hotswap?特性的框架,?也使用了線程上下文類加載器,?比如?seasar (full stack framework in japenese).

線程上下文從根本解決了一般應(yīng)用不能違背雙親委派模式的問題.

使java類加載體系顯得更靈活.

隨著多核時(shí)代的來臨,?相信多線程開發(fā)將會(huì)越來越多地進(jìn)入程序員的實(shí)際編碼過程中.?因此,
在編寫基礎(chǔ)設(shè)施時(shí),?通過使用線程上下文來加載類,?應(yīng)該是一個(gè)很好的選擇.

當(dāng)然,?好東西都有利弊.?使用線程上下文加載類,?也要注意,?保證多根需要通信的線程間的類加載器應(yīng)該是同一個(gè),
防止因?yàn)椴煌念惣虞d器,?導(dǎo)致類型轉(zhuǎn)換異常(ClassCastException).

10.5.自定義的類加載器實(shí)現(xiàn)

defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain)
是java.lang.Classloader提供給開發(fā)人員,?用來自定義加載class的接口.

使用該接口,?可以動(dòng)態(tài)的加載class文件.

例如,
在jdk中, URLClassLoader是配合findClass方法來使用defineClass,?可以從網(wǎng)絡(luò)或硬盤上加載class.

而使用類加載接口,?并加上自己的實(shí)現(xiàn)邏輯,?還可以定制出更多的高級(jí)特性.

比如,

一個(gè)簡(jiǎn)單的hot swap?類加載器實(shí)現(xiàn):

import java.io.File; import java.io.FileInputStream; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; /** * 可以重新載入同名類的類加載器實(shí)現(xiàn) * * 放棄了雙親委派的加載鏈模式. * 需要外部維護(hù)重載后的類的成員變量狀態(tài). * * @author ken.wu * @mail ken.wug@gmail.com * 2007-9-28 下午01:37:43 */ public class HotSwapClassLoader extends URLClassLoader { public HotSwapClassLoader(URL[] urls) { super (urls); } public HotSwapClassLoader(URL[] urls, ClassLoader parent) { super (urls, parent); } public Class load(String name) throws ClassNotFoundException { return load(name, false ); } public Class load(String name, boolean resolve) throws ClassNotFoundException { if ( null != super .findLoadedClass(name)) return reload(name, resolve); Class clazz = super .findClass(name); if (resolve) super .resolveClass(clazz); return clazz; } public Class reload(String name, boolean resolve) throws ClassNotFoundException { return new HotSwapClassLoader( super .getURLs(), super .getParent()).load( name, resolve); } } public class A { private B b; public void setB(B b) { this .b = b; } public B getB() { return b; } } public class B {}

這個(gè)類的作用是可以重新載入同名的類,?但是,?為了實(shí)現(xiàn)hotswap,?老的對(duì)象狀態(tài)
需要通過其他方式拷貝到重載過的類生成的全新實(shí)例中來。(A類中的b實(shí)例)

而新實(shí)例所依賴的B類如果與老對(duì)象不是同一個(gè)類加載器加載的,?將會(huì)拋出類型轉(zhuǎn)換異常(ClassCastException).

為了解決這種問題,?HotSwapClassLoader自定義了load方法.?即當(dāng)前類是由自身classLoader加載的,?而內(nèi)部依賴的類

還是老對(duì)象的classLoader加載的.

public class TestHotSwap { public static void main(String args[]) { A a = new A(); B b = new B(); a.setB(b); System.out.printf("A classLoader is %s n" , a.getClass().getClassLoader()); System.out.printf("B classLoader is %s n" , b.getClass().getClassLoader()); System.out.printf("A.b classLoader is %s n" , a.getB().getClass().getClassLoader()); HotSwapClassLoader c1 = new HotSwapClassLoader( new URL[]{ new URL( "file:\e:\test\")} , a.getClass().getClassLoader()); Class clazz = c1.load(" test.hotswap.A "); Object aInstance = clazz.newInstance(); Method method1 = clazz.getMethod(" setB ", B.class); method1.invoke(aInstance, b); Method method2 = clazz.getMethod(" getB ", null); Object bInstance = method2.invoke(aInstance, null); System.out.printf(" reloaded A.b classLoader is %s n", bInstance.getClass().getClassLoader()); } }

輸出

A classLoader is sun.misc.Launcher$AppClassLoader@19821f??
B classLoader is sun.misc.Launcher$AppClassLoader@19821f??
A.b classLoader is sun.misc.Launcher$AppClassLoader@19821f??
reloaded A.b classLoader is sun.misc.Launcher$AppClassLoader@19821f?

創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)

總結(jié)

以上是生活随笔為你收集整理的深入JVM系列(三)之类加载、类加载器、双亲委派机制与常见问题的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。