1.什么是類加載?
類的加載指的是將類的.class文件中的二進(jìn)制數(shù)據(jù)讀入到內(nèi)存(JVM)中,將其放在運(yùn)行時(shí)數(shù)據(jù)放入方法區(qū)內(nèi)(這里方法區(qū)也稱永久代,但是在Jdk1.8后取消這塊改名叫元空間),然后在堆內(nèi)(heap)創(chuàng)建一個java.lang.Class對象,用來封裝類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)。類的加載的最終產(chǎn)品是位于堆區(qū)中的Class對象,Class對象封裝了類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu),并且向Java程序員提供了訪問方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)的接口。
2.類的生命周期
一個類的生命周期包括:加載,驗(yàn)證,準(zhǔn)備,解析,初始化的五個過程,這個五個過程。其中加載的過程對于開發(fā)人員來說是可控制的,至于原因就在加載的過程中包括三個階段:通過一個類的全限定類名來獲取該類的二進(jìn)制字節(jié)流,將二進(jìn)制字節(jié)流所表示的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)到方法區(qū)所運(yùn)行時(shí)的數(shù)據(jù)結(jié)構(gòu),最后在堆中生成代表該類的java.lang.Class的對象,作為方法區(qū)數(shù)據(jù)訪問的入口,而由于JVM并沒有規(guī)定而我們?nèi)绻@取該類的二進(jìn)制字節(jié)流,所以我們可以使用默認(rèn)的類加載和自定義的類加載。
3.類加載的層次圖
在上面我們提到了開發(fā)人員可以使用默認(rèn)的類加載或是自定義類加載器,這樣我們就會想到如果保證這些類加載不會產(chǎn)生沖突呢?首先我們需要先了解一下JDK默認(rèn)的幾種類加載器:
(1) Bootstrap ClassLoader:
啟動類加載器(Bootstrap ClassLoader):由C++語言實(shí)現(xiàn)(針對HotSpot),負(fù)責(zé)將存放在JAVA_HOME下jre\lib目錄或-Xbootclasspath參數(shù)指定的路徑中的虛擬機(jī)識別的類庫加載到內(nèi)存中(譬如 rt.jar)。
(2) Extension ClassLoader :
負(fù)責(zé)加載jre\lib\ext目錄或java.ext.dirs系統(tǒng)變量指定的路徑中的所有類庫。

(3) Application ClassLoader :
是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實(shí)現(xiàn)的。它負(fù)責(zé)將系統(tǒng)類路徑(CLASSPATH)中指定的類庫加載到內(nèi)存中。開發(fā)者可以直接使用系統(tǒng)類加載器。由于這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,因此一般稱為SystemClassLoader
以上就是jdk默認(rèn)的三種類加載器,下圖我們可以看到JDK的類加載層次,圖中的箭頭就是一種雙親委派的加載模式。什么是雙親委派模型呢?
雙親委派模型:某一個特定的類加載器接受一個類加載的請求時(shí)候,首先先把這個類的請求委托給上級的類加載器,即父類的類加載器完成,而不是自己優(yōu)先嘗試加載該類,只有當(dāng)父類無法加載該類的時(shí)候,自己再嘗試加載該類,如果自己也無法加載即拋出:ClassNotFoundException 和 NoClassDefFoundError。
至于為什么要使用這種類加載模型呢?舉一個網(wǎng)上都用的例子:類java.lang.Object,它存在在rt.jar中,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的Bootstrap ClassLoader進(jìn)行加載,因此Object類在程序的各種類加載器環(huán)境中都是同一個類。相反,如果沒有雙親委派模型而是由各個類加載器自行加載的話,即便是同一個.class文件,由不同的類加載器加載即不是同一個類,如果用戶編寫了一個java.lang.Object的同名類并放在ClassPath中,那系統(tǒng)中將會出現(xiàn)多個不同的Object類,程序?qū)⒒靵y。因此,如果開發(fā)者嘗試編寫一個與rt.jar類庫中重名的Java類,可以正常編譯,但是永遠(yuǎn)無法被加載運(yùn)行。


我們可以看到j(luò)dk里面的classloader方法的實(shí)現(xiàn),首先會嘗試看本地有沒有加載過該類,如果加載過,即直接返回,否則獲取該類加載器的父類,如果父類不等于空,則優(yōu)先委托給父類來加載,如果父類為空,則直接交給頂級的類加載器完成加載,如果頂級類加載也無法加載,則才調(diào)用自己的類加載來findClass來加載該類。
4.Tomcat類加載
現(xiàn)在終于走到正題,想必一定是tomcat并沒有完全遵循雙親委派的累加機(jī)制,否則不會單獨(dú)拿出來講。首先我們先思考幾個問題:
1.如果在一個Tomcat內(nèi)部署多個應(yīng)用,甚至多個應(yīng)用內(nèi)使用了某個類似的幾個不同版本,但它們之間卻互不影響。這是如何做到的。
2.如果多個應(yīng)用都用到了某類似的相同版本,是否可以統(tǒng)一提供,不在各個應(yīng)用內(nèi)分別提供,占用內(nèi)存呢。
至于第一個問題其實(shí)上面的講解已經(jīng)解答了,就是因?yàn)閠omcat部署了多個應(yīng)用,而多個應(yīng)用都采用自定義的類加載器,所以即便是同一個類使用不同的類加載機(jī)制最終也是不一樣的類。
至于第二問題,我首先看看Tomcat的類加載層次:

我們看到webappClassLoader上面有一個common的類加載器,它是所有webappClassLoader的父加載器,多個應(yīng)用匯存在公有的類庫,而公有的類庫都會使用commonclassloader來實(shí)現(xiàn)。這樣也就回答了第二個問題;
由此我們也引出了如果不是公有的類呢,這些類就會使用webappClassLoader加載,而webappClassLoader的實(shí)現(xiàn)并沒有走雙親委派的模式,這有是為何呢?
原因有兩個:
1)加載本類的classloader未知時(shí),為了隔離不同的調(diào)用者,即類的隔離,采用了上下文類加載的模式加載類;
2)當(dāng)前高層的接口在低層去實(shí)現(xiàn),而高層的類有需要低層的類加載的時(shí)候,這個時(shí)候,需要使用上下文類加載器去實(shí)現(xiàn)(后面會通過JDBC的加載來講解)
JDCB的類加載(經(jīng)典的線程上下文加載器)
private static Connection getConnection(
String url,java.util.Properties info,Class caller)throwsSQLException {
//由于DriverManger.class是由于jdk里的rt.jar包里面加載的,而實(shí)際調(diào)用的是com.mysql.jdbc.Driver的driver該類,而是調(diào)用getConnection的方法時(shí)候,下圖中的這個方法的到DriverManger這個類是頂級類加載器加載的,這個時(shí)候又要啟動該類的子類,所以雙親委派是無法加載該類的,即圖二中,caller.getClassLoader是null,這個時(shí)候就會調(diào)用 if 里面的線程上下文的加載器,通過上下文加載的方式完成加載,最好驗(yàn)證該是否可用,完成獲取JDBC的連接。


有點(diǎn)跑題,現(xiàn)在回到Tomcat類加載中,我們需要了解到底是采用了雙親委派還是上線文加載模式。首先我們需要明確的一點(diǎn)就是基礎(chǔ)類肯,common類,還是有servlet-api一定用雙親委派模式,因?yàn)檫@些都是公有的類庫,且對于Servlet-api是不允許被重寫,也就是說如果你用自己的類加載的話,會影響到應(yīng)用內(nèi)部得到正常運(yùn)行了,也就是說只有加載app應(yīng)用的類時(shí)候才會引用上下文加載。下面我們看看上線文加載的類:webappLoader;
在Tomcat啟動時(shí),會創(chuàng)建一系列的類加載器,在其主類Bootstrap的初始化過程中,會先初始化classloader,然后將其綁定到Thread中。

其中initClassLoaders方法,會根據(jù)catalina.properties的配置,創(chuàng)建相應(yīng)的classloader。由于默認(rèn)只配置了common.loader屬性,所以其中只會創(chuàng)建一個出來commonClassLoader,然后,當(dāng)一個應(yīng)用啟動的時(shí)候,會為其創(chuàng)建對應(yīng)的WebappClassLoader。此時(shí)會將commonClassLoader設(shè)置為其parent。下面的代碼是StandardContext類在啟動時(shí)創(chuàng)建WebappLoader的代碼

這里的getParentClassLoader會從當(dāng)前組件的classLoader一直向上,找parent classLoader設(shè)置。之后注意下一行代碼
webappLoader.setDelegate
這就是在設(shè)置后面Web應(yīng)用的類查找時(shí)是父優(yōu)先還是子優(yōu)先。這個配置可以在server.xml里,對Context組件進(jìn)行配置。
即在Context元素下可以嵌套一個Loader元素,配置Loader的delegate即可,其默認(rèn)為false,即子優(yōu)先。類似于這樣
delegate="true"/>
注意Loader還有一個屬性是reloadable,用于表明對于/WEB-INF/classes/ 和 /WEB-INF/lib 下資源發(fā)生變化時(shí),是否重新加載應(yīng)用。這個特性在開發(fā)的時(shí)候,還是很有用的。
如果你的應(yīng)用并沒有配置這個屬性,想要重新加載一個應(yīng)用,只需要使用manager里的reload功能就可以。
有點(diǎn)跑題,回到我們說的delgate上面來,配置之后,可以指定Web應(yīng)用類加載時(shí),到底是使用父優(yōu)先還是子優(yōu)先。
這里的WebappLoader,就開始了正式的創(chuàng)建WebappClassLoader,而在WebbappClassLoader里具體邏輯如下:判斷已加載的類里是否已經(jīng)包含,然后避免Java SE的classes被覆蓋,packageAccess的檢查。之后,開始了我們的父優(yōu)先子優(yōu)先的流程。這里判斷是否使用delegate時(shí),對于一些容器提供的class,也會跳過。
boolean delegateLoad = delegate ||filter(name);


而由于上面提到通常delegateLoad這個字段是false,所以普通我們的Tomcat在web應(yīng)用類加載的時(shí)候,都會走上線文加載。最后補(bǔ)充一點(diǎn),也是在網(wǎng)上查閱資料看我們常用的Class.forName()這個方法,其實(shí)我們往往忽略了該方法還有兩個參數(shù),一個是是否必須初始化,另指定加載該類的類加載器,也就是說,在forName方法中,我也是可以指定獲取該類的類加載器的呀?。ú┲饕膊虐l(fā)現(xiàn))
