轉載:https://blog.csdn.net/qq_38182963/article/details/78660779
http://www.cnblogs.com/aspirant/p/8991830.html
http://www.cnblogs.com/xing901022/p/4574961.html
雙親委派模式的破壞
第一次破壞:向前兼容
雙親委派模型的第一次“被破壞”其實發生在雙親委派模型出現之前–即JDK1.2發布之前。由於雙親委派模型是在JDK1.2之后才被引入的,而類加載器和抽象類java.lang.ClassLoader則是JDK1.0時候就已經存在,面對已經存在 的用戶自定義類加載器的實現代碼,Java設計者引入雙親委派模型時不得不做出一些妥協。為了向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一個新的proceted方法findClass(),在此之前,用戶去繼承java.lang.ClassLoader的唯一目的就是重寫loadClass()方法,因為虛擬在進行類加載的時候會調用加載器的私有方法loadClassInternal(),而這個方法的唯一邏輯就是去調用自己的loadClass()。JDK1.2之后已不再提倡用戶再去覆蓋loadClass()方法,應當把自己的類加載邏輯寫到findClass()方法中,在loadClass()方法的邏輯里,如果父類加載器加載失敗,則會調用自己的findClass()方法來完成加載,這樣就可以保證新寫出來的類加載器是符合雙親委派模型的。
第二次破壞:加載SPI接口實現類
雙親委派模型的第二次“被破壞”是這個模型自身的缺陷所導致的,雙親委派模型很好地解決了各個類加載器的基礎類統一問題(越基礎的類由越上層的加載器進行加載),基礎類之所以被稱為“基礎”,是因為它們總是作為被調用代碼調用的API。但是,如果基礎類又要調用用戶的代碼,那該怎么辦呢。
這並非是不可能的事情,一個典型的例子便是JNDI服務,它的代碼由啟動類加載器去加載(在JDK1.3時放進rt.jar),但JNDI的目的就是對資源進行集中管理和查找,它需要調用獨立廠商實現部部署在應用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代碼,但啟動類加載器不可能“認識”之些代碼,該怎么辦?
為了解決這個困境,Java設計團隊只好引入了一個不太優雅的設計:線程上下文件類加載器(Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設置,如果創建線程時還未設置,它將會從父線程中繼承一個;如果在應用程序的全局范圍內都沒有設置過,那么這個類加載器默認就是應用程序類加載器。了有線程上下文類加載器,JNDI服務使用這個線程上下文類加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載動作,這種行為實際上就是打通了雙親委派模型的層次結構來逆向使用類加載器,已經違背了雙親委派模型,但這也是無可奈何的事情。Java中所有涉及SPI的加載動作基本上都采用這種方式,例如JNDI,JDBC,JCE,JAXB和JBI等。
第三次破壞:熱部署
雙親委派模型的第三次“被破壞”是由於用戶對程序的動態性的追求導致的。為了實現熱插拔,熱部署,模塊化,意思是添加一個功能或減去一個功能不用重啟,只需要把這模塊連同類加載器一起換掉就實現了代碼的熱替換。例如OSGi的出現。在OSGi環境下,類加載器不再是雙親委派模型中的樹狀結構,而是進一步發展為網狀結構。
Java 程序中基本有一個共識:OSGI對類加載器的使用時值得學習的,弄懂了OSGI的實現,就可以算是掌握了類加載器的精髓。
Tomcat類加載器
拋出問題
1、既然 Tomcat 不遵循雙親委派機制,那么如果我自己定義一個惡意的HashMap,會不會有風險呢?(阿里的面試官問)
答: 顯然不會有風險,如果有,Tomcat都運行這么多年了,那群Tomcat大神能不改進嗎? tomcat不遵循雙親委派機制,只是自定義的classLoader順序不同,但頂層還是相同的,
還是要去頂層請求classloader.
2、我們思考一下:Tomcat是個web容器, 那么它要解決什么問題:
1. 一個web容器可能需要部署兩個應用程序,不同的應用程序可能會依賴同一個第三方類庫的不同版本,不能要求同一個類庫在同一個服務器只有一份,因此要保證每個應用程序的類庫都是獨立的,保證相互隔離。
2. 部署在同一個web容器中相同的類庫相同的版本可以共享。否則,如果服務器有10個應用程序,那么要有10份相同的類庫加載進虛擬機,這是扯淡的。
3. web容器也有自己依賴的類庫,不能於應用程序的類庫混淆。基於安全考慮,應該讓容器的類庫和程序的類庫隔離開來。
4. web容器要支持jsp的修改,我們知道,jsp 文件最終也是要編譯成class文件才能在虛擬機中運行,但程序運行后修改jsp已經是司空見慣的事情,否則要你何用? 所以,web容器需要支持 jsp 修改后不用重啟。
再看看我們的問題:Tomcat 如果使用默認的類加載機制行不行?
答案是不行的。為什么?我們看,第一個問題,如果使用默認的類加載器機制,那么是無法加載兩個相同類庫的不同版本的,默認的類加載器是不管你是什么版本的,只在乎你的全限定類名,並且只有一份。第二個問題,默認的類加載器是能夠實現的,因為他的職責就是保證唯一性。第三個問題和第一個問題一樣。我們再看第四個問題,我們想我們要怎么實現jsp文件的熱修改(樓主起的名字),jsp 文件其實也就是class文件,那么如果修改了,但類名還是一樣,類加載器會直接取方法區中已經存在的,修改后的jsp是不會重新加載的。那么怎么辦呢?我們可以直接卸載掉這jsp文件的類加載器,所以你應該想到了,每個jsp文件對應一個唯一的類加載器,當一個jsp文件修改了,就直接卸載這個jsp類加載器。重新創建類加載器,重新加載jsp文件。
Tomcat 如何實現自己獨特的類加載機制?
所以,Tomcat 是怎么實現的呢?牛逼的Tomcat團隊已經設計好了。我們看看他們的設計圖:
我們看到,前面3個類加載和默認的一致,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader則是Tomcat自己定義的類加載器,它們分別加載/common/*
、/server/*
、/shared/*
(在tomcat 6之后已經合並到根目錄下的lib目錄下)和/WebApp/WEB-INF/*
中的Java類庫。其中WebApp類加載器和Jsp類加載器通常會存在多個實例,每一個Web應用程序對應一個WebApp類加載器,每一個JSP文件對應一個Jsp類加載器。
- commonLoader:Tomcat最基本的類加載器,加載路徑中的class可以被Tomcat容器本身以及各個Webapp(web應用)訪問;
- catalinaLoader:Tomcat容器私有的類加載器,加載路徑中的class對於Webapp不可見;
- sharedLoader:各個Webapp共享的類加載器,加載路徑中的class對於所有Webapp可見,但是對於Tomcat容器不可見;
- WebappClassLoader:各個Webapp私有的類加載器,加載路徑中的class只對當前Webapp可見;
從圖中的委派關系中可以看出:
CommonClassLoader能加載的類都可以被Catalina ClassLoader和SharedClassLoader使用,從而實現了公有類庫的共用,而CatalinaClassLoader和Shared ClassLoader自己能加載的類則與對方相互隔離。
WebAppClassLoader可以使用SharedClassLoader加載到的類,但各個WebAppClassLoader實例之間相互隔離。
而JasperLoader的加載范圍僅僅是這個JSP文件所編譯出來的那一個.Class文件,它出現的目的就是為了被丟棄:當Web容器檢測到JSP文件被修改時,會替換掉目前的JasperLoader的實例,並通過再建立一個新的Jsp類加載器來實現JSP文件的HotSwap功能。
好了,至此,我們已經知道了tomcat為什么要這么設計,以及是如何設計的,那么,tomcat 違背了java 推薦的雙親委派模型了嗎?答案是:違背了。 我們前面說過:
雙親委派模型要求除了頂層的啟動類加載器之外,其余的類加載器都應當由自己的父類加載器加載。
很顯然,tomcat 不是這樣實現,tomcat 為了實現隔離性,沒有遵守這個約定,每個webappClassLoader加載自己的目錄下的class文件,不會傳遞給父類加載器。
我們擴展出一個問題:如果tomcat 的 Common ClassLoader 想加載 WebApp ClassLoader 中的類,該怎么辦?
看了前面的關於破壞雙親委派模型的內容,我們心里有數了,我們可以使用線程上下文類加載器實現,使用線程上下文加載器,可以讓父類加載器請求子類加載器去完成類加載的動作。
Tomcat類加載過程
tomcat的類加載機制是違反了雙親委托原則的,對於一些未加載的非基礎類(Object,String等),各個web應用自己的類加載器(WebAppClassLoader)會優先加載,加載不到時再交給commonClassLoader走雙親委托。具體的加載邏輯位於WebAppClassLoaderBase.loadClass()
方法中,代碼篇幅長,這里以文字描述加載一個類過程:
- 先在本地緩存中查找是否已經加載過該類(對於一些已經加載了的類,會被緩存在
resourceEntries
這個數據結構中),如果已經加載即返回,否則 繼續下一步。 - 讓系統類加載器(AppClassLoader)嘗試加載該類,主要是為了防止一些基礎類會被web中的類覆蓋,如果加載到即返回,返回繼續。
- 前兩步均沒加載到目標類,那么web應用的類加載器將自行加載,如果加載到則返回,否則繼續下一步。
- 最后還是加載不到的話,則委托父類加載器(Common ClassLoader)去加載。
第3第4兩個步驟的順序已經違反了雙親委托機制,除了tomcat之外,JDBC,JNDI,Thread.currentThread().setContextClassLoader();
等很多地方都一樣是違反了雙親委托。