tomcat學習之四:tomcat的類載入機制
tomcat的類載入機制遵循了java類載入機制中經典的雙親委派模型。所以要了解tomcat的類載入機制需要先了解雙親委派模型。
在程式中用到的類需要由類載入器將類的class檔案載入到記憶體中,然後經由JVM驗證、解析、初始化後才能使用,如下段程式碼:
User類需要類載入器將User.class載入到記憶體中,然後經過上述所說的步驟後,程式才能使用,類載入器在Java中使用ClassLoader物件表示。一個類在程式執行期間只會被載入一次,而類載入器會在載入後建立該類的class物件,會將class物件儲存在方法區,也稱永久代(Metaspace)的記憶體區域,並在將該class物件的引用放到Vector中。 ClassLoader中相關程式碼如下:public static void main(String[] args) throws Exception { User user = new User(); user.setUsername("tom"); user.setPhone("110"); user.setSex("man"); System.out.println(user); }
由於類載入機制有一個非常重要的特性,那就是類載入器A載入的類不能被類載入器B載入的類使用,具體可見下面程式碼:// Invoked by the VM to record every loaded class with this loader. void addClass(Class<?> c) { classes.addElement(c); }
所以為了保證一個類只由一個類載入器載入,所以引入了雙親委派模型。什麼是雙親委派模型? 每一個載入器都要通過組合的方式儲存一個載入器的例項作為父載入器。當使用當前類載入器載入類時,當前類載入器不會自己載入,會先交給父載入器載入,父載入器載入時會重複這個過程,直到到達頂端的類載入器後。如果還不能載入就丟擲ClassNotFoundException,子載入器捕獲到異常,就會嘗試自己載入,如果自己載入不了,重複這個過程,直到最頂端的載入器,如果這時候最低端的載入器也不能載入,就會丟擲ClassNotFoundException。而且明確規範了,如果一個類載入器第一次能載入成功,那麼以後的載入必須成功,如果第一次失敗,那麼以後的載入也必須失敗。這就保證了一個類只由一個類載入器載入。同時 通過類載入器的分層,也將java類庫分了層。例如java.lang.Object作為一個基類,只能由Bootstrap類載入器載入(最頂層的類載入器)。//定義自己的類載入器 ClassLoader cl = new ClassLoader() { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { try { //違背雙親委派模型,先自己載入,自己不能載入再交給父載入器載入 String str = name.substring(name.lastIndexOf(".") + 1) + ".class"; InputStream is = getClass().getResourceAsStream(str); if (is == null) { return super.loadClass(name); } byte[] b = new byte[is.available()]; is.read(b); return defineClass(name, b, 0, b.length); } catch (IOException e) { throw new ClassNotFoundException(name); } } }; Object obj = cl.loadClass("cn.yamikaze.java.basic.HelloJava").newInstance(); System.out.println(obj.getClass()); // Class cn.yamikaze.java.basic.HelloJava // 結果為false // 為什麼? 因為obj是自定義的類載入器載入產生的物件,而HelloJava是由系統載入器載入的 System.out.println(obj instanceof cn.yamikaze.java.basic.HelloJava);
至於為什麼java不採用單一載入器的原因,我想可能是那樣做職責不明確吧!但具體什麼原因我也不知,如果知道的小夥伴請在下面留言謝謝!
1、雙親委派模型
java的雙親委派模型如下圖所示(組合方式實現繼承關係):Bootstrap類載入器:負責載入jre/lib下的jar,例如rt.jar是java的核心類庫,Object、String、System等常用類都存在於rt.jar中。這個載入器存在於虛擬機器中,如果是HotSpot虛擬機器,這個類載入器是C++語言編寫,如果虛擬機器本身就是由純java語言實現,那就是由java編寫。如果想要使用這個載入器,可以使用引數-Xbootclasspath指定載入路徑,但boostrap類載入器只會載入虛擬機器識別的類庫,例如rt.jar,否則即使名字不符合,放在lib中也不會被載入。
Extension類載入器:擴充套件類載入器,負責載入jre/lib/ext中的jar,或者java.ext.dirs系統變數指定路徑的類庫。
Application類載入器:負責載入使用者類路徑上指定的類庫,由於是ClassLoader的getSystemClassLoader方法的返回值,所以也叫作系統類載入器。
2、tomcat的類載入器
2.1、為什麼tomcat要實現自己的類載入器?
tomcat作為一個伺服器,在它上面可以部署多個應用。預設情況下是使用1中的應用載入器載入類的,但往往部署的應用都會有多個jar依賴,所以第一點為了解決依賴問題,必須保證每個應用的類庫獨立。 為什麼要保證每個應用的類庫相互獨立呢? 打個比方,我們都知道連線資料庫需要資料庫驅動,而資料庫驅動的版本要與連線的資料庫相對應,否則無法獲取連線。 那假設部署在tomcat上的兩個應用依賴的資料庫驅動版本有很大差異,這樣直接使用java的系統類載入器會導致這兩個應用有一個無法啟動。 a)、要保證部署在tomcat上的每個應用依賴的類庫相互獨立,不受影響。 b)、由於tomcat是採用java語言編寫的,它自身也有類庫依賴,為了安全考慮,tomcat使用的類庫要與部署的應用的類庫相互獨立。 c)、有些類庫tomcat與部署的應用可以共享,比如說servlet-api,使用maven編寫web程式時,servlet-api的範圍是provided,表示打包時不打包這個依賴,因為我們都知道伺服器已經有這個依賴了。 d)、部署的應用之間的類庫可以共享。這聽起來好像與第一點相互矛盾,但其實這很合理,類被類載入器載入到虛擬機器後,會生成代表該類的class物件存放在永久代區域,這時候如果有大量的應用使用spring來管理,如果spring類庫不能共享,那每個應用的spring類庫都會被載入一次,將會是很大的資源浪費。 由於存在上述問題,tomcat實現了自己的類載入器,不僅僅是tomcat,所有的伺服器基本都有或多或少上述所說的問題。2.2、tomcat的類載入器設計
tomcat的類載入器設計如下圖所示:
ps: 其中藍色的類載入器為tomcat自己實現的類載入器。
Common類載入器:負責載入/common目錄的類庫,這兒存放的類庫可被tomcat以及所有的應用使用。
Catalina類載入器:負責載入/server目錄的類庫,只能被tomcat使用。
Shared類載入器:負載載入/shared目錄的類庫,可被所有的web應用使用,但tomcat不可使用。
WebApp類載入器:負載載入單個Web應用下classes目錄以及lib目錄的類庫,只能當前應用使用。
Jsp類載入器:負責載入Jsp,每一個Jsp檔案都對應一個Jsp載入器。
Tomcat執行期間,Webapp類載入器與Jsp類載入器個數為複數。通過上圖的設計,可以解決掉2.1中的問題。
可能看到這兒你會翻出tomcat目錄結構檢視一下,然後你會發現根本沒有common、shared、server目錄。這是因為只有在conf目錄下的catalina.properties指定了server.loader 以及 share.loader兩個屬性tomcat才會建立CatalinaClassLoader和SharedClassLoader例項,而預設情況下都沒指定,所以CatalinaClassLoader以及SharedClassLoader都會使用CommonClassLoader來代替,所以tomcat6.x以上順理成章地把上述三個目錄合併成了一個lib目錄。這個目錄相當於/common目錄的作用。
來看看原始碼中關於這三個類載入器的建立吧,如下程式碼:
private void initClassLoaders() {
try {
commonLoader = createClassLoader("common", null);
if( commonLoader == null ) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader=this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
String value = CatalinaProperties.getProperty(name + ".loader");
//預設情況下為空,直接返回parent,這兒的parent就是CommonLoader
if ((value == null) || (value.equals("")))
return parent;
value = replace(value);
List<Repository> repositories = new ArrayList<Repository>();
StringTokenizer tokenizer = new StringTokenizer(value, ",");
//這兒是對類載入器定義載入的路徑解析
while (tokenizer.hasMoreElements()) {
String repository = tokenizer.nextToken().trim();
if (repository.length() == 0) {
continue;
}
// 不是本地類庫
try {
@SuppressWarnings("unused")
URL url = new URL(repository);
repositories.add(
new Repository(repository, RepositoryType.URL));
continue;
} catch (MalformedURLException e) {
// Ignore
}
// Local repository
if (repository.endsWith("*.jar")) {
repository = repository.substring
(0, repository.length() - "*.jar".length());
repositories.add(
new Repository(repository, RepositoryType.GLOB));
} else if (repository.endsWith(".jar")) {
repositories.add(
new Repository(repository, RepositoryType.JAR));
} else {
repositories.add(
new Repository(repository, RepositoryType.DIR));
}
}
//使用工廠建立
return ClassLoaderFactory.createClassLoader(repositories, parent);
}
可以看到initClassLoader方法中的呼叫,再結合catalina.properties檔案,CommonClassLoader、ServerClassLoader、ShareClassLoader為同一個類載入器CommonClassLoader。上述程式碼存在於org.apache.catalina.startup.Bootstrap類中。
建立完成後會將ShareClassLoader通過反射呼叫Catalina類setParentClassLoader方法設定到parentClassLoader屬性中,然後在解析方法中再設定到Engine容器中去,部分程式碼如下:
// Add RuleSets for nested elements
digester.addRuleSet(new NamingRuleSet("Server/GlobalNamingResources/"));
digester.addRuleSet(new EngineRuleSet("Server/Service/"));
digester.addRuleSet(new HostRuleSet("Server/Service/Engine/"));
digester.addRuleSet(new ContextRuleSet("Server/Service/Engine/Host/"));
addClusterRuleSet(digester, "Server/Service/Engine/Host/Cluster/");
digester.addRuleSet(new NamingRuleSet("Server/Service/Engine/Host/Context/"));
// When the 'engine' is found, set the parentClassLoader.
digester.addRule("Server/Service/Engine",
new SetParentClassLoaderRule(parentClassLoader));
2.3、Tomcat類載入器的實現
java中使用者自定義類載入器需要實現ClassLoader類,ClassLoader類結構如下:在tomcat中,類載入器實現是通過繼承URLClassLoader類來實現的,URLClassLoader是ClassLoader的一個子類。 下面是WebappClassLoaderBase部分原始碼:
protected String[] repositories = new String[0];
protected URL[] repositoryURLs = null;
/**
* Repositories translated as path in the work directory (for Jasper
* originally), but which is used to generate fake URLs should getURLs be
* called.
*/
protected File[] files = new File[0];
/**
* The list of JARs, in the order they should be searched
* for locally loaded classes or resources.
*/
protected JarFile[] jarFiles = new JarFile[0];
/**
* The list of JARs, in the order they should be searched
* for locally loaded classes or resources.
*/
protected File[] jarRealFiles = new File[0];
/**
* The path which will be monitored for added Jar files.
*/
protected String jarPath = null;
/**
* The list of JARs, in the order they should be searched
* for locally loaded classes or resources.
*/
protected String[] jarNames = new String[0];
這個ClassLoader自然就是對應一個Web應用,上面程式碼表示這個ClassLoader的載入範圍。ClassLoader是在StandardContext的startInternal方法中與Context繫結的。以下為StandardWrapper獲取Servlet例項程式碼:
public synchronized Servlet loadServlet() throws ServletException {
if (unloading) {
throw new ServletException(
sm.getString("standardWrapper.unloading", getName()));
}
// Nothing to do if we already have an instance or an instance pool
if (!singleThreadModel && (instance != null))
return instance;
PrintStream out = System.out;
if (swallowOutput) {
SystemLogHandler.startCapture();
}
Servlet servlet;
try {
long t1=System.currentTimeMillis();
// Complain if no servlet class has been specified
if (servletClass == null) {
unavailable(null);
throw new ServletException
(sm.getString("standardWrapper.notClass", getName()));
}
/**
* 交給父容器(Context)載入,父容器再交給InstanceManager載入。
* InstanceManager中的ClassLoader是在StandardContext的startInternal方法設定進去的
*/
InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();
try {
servlet = (Servlet) instanceManager.newInstance(servletClass);
} catch (ClassCastException e) {
unavailable(null);
// Restore the context ClassLoader
throw new ServletException
(sm.getString("standardWrapper.notServlet", servletClass), e);
} catch (Throwable e) {
e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
unavailable(null);
// Added extra log statement for Bugzilla 36630:
// http://bz.apache.org/bugzilla/show_bug.cgi?id=36630
if(log.isDebugEnabled()) {
log.debug(sm.getString("standardWrapper.instantiate", servletClass), e);
}
// Restore the context ClassLoader
throw new ServletException
(sm.getString("standardWrapper.instantiate", servletClass), e);
}
if (multipartConfigElement == null) {
MultipartConfig annotation =
servlet.getClass().getAnnotation(MultipartConfig.class);
if (annotation != null) {
multipartConfigElement =
new MultipartConfigElement(annotation);
}
}
processServletSecurityAnnotation(servlet.getClass());
// Special handling for ContainerServlet instances
if ((servlet instanceof ContainerServlet) &&
(isContainerProvidedServlet(servletClass) ||
((Context) getParent()).getPrivileged() )) {
((ContainerServlet) servlet).setWrapper(this);
}
classLoadTime=(int) (System.currentTimeMillis() -t1);
if (servlet instanceof SingleThreadModel) {
if (instancePool == null) {
instancePool = new Stack<Servlet>();
}
singleThreadModel = true;
}
initServlet(servlet);
fireContainerEvent("load", this);
loadTime=System.currentTimeMillis() -t1;
} finally {
if (swallowOutput) {
String log = SystemLogHandler.stopCapture();
if (log != null && log.length() > 0) {
if (getServletContext() != null) {
getServletContext().log(log);
} else {
out.println(log);
}
}
}
}
return servlet;
}
可以看到註釋部分,載入例項交給了父容器StandardContext完成。
對於jsp類載入器,每一個jsp檔案都對應了一個jsp類載入器。 在tomcat中,jsp也是一個servlet, 也會被StandardWrapper包裝。那jsp檔案修改後可以直接將改變展示在瀏覽器中,無須重啟tomcat,也無需reload容器,那這是怎麼做的呢?
對於容器,在ContainerBase類的startInternal方法中,會呼叫這段程式碼:
protected void threadStart() {
if (thread != null)
return;
if (backgroundProcessorDelay <= 0)
return;
threadDone = false;
String threadName = "ContainerBackgroundProcessor[" + toString() + "]";
thread = new Thread(new ContainerBackgroundProcessor(), threadName);
thread.setDaemon(true);
thread.start();
}
可以很明顯看出,啟動了一個後臺執行緒,後臺執行緒程式碼如下:
public void run() {
Throwable t = null;
String unexpectedDeathMessage = sm.getString(
"containerBase.backgroundProcess.unexpectedThreadDeath",
Thread.currentThread().getName());
try {
while (!threadDone) {
try {
Thread.sleep(backgroundProcessorDelay * 1000L);
} catch (InterruptedException e) {
// Ignore
}
if (!threadDone) {
Container parent = (Container) getMappingObject();
ClassLoader cl =
Thread.currentThread().getContextClassLoader();
if (parent.getLoader() != null) {
cl = parent.getLoader().getClassLoader();
}
processChildren(parent, cl);//關鍵是這句
}
}
} catch (RuntimeException e) {
t = e;
throw e;
} catch (Error e) {
t = e;
throw e;
} finally {
if (!threadDone) {
log.error(unexpectedDeathMessage, t);
}
}
}
protected void processChildren(Container container, ClassLoader cl) {
try {
if (container.getLoader() != null) {
Thread.currentThread().setContextClassLoader
(container.getLoader().getClassLoader());
}
container.backgroundProcess();
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error("Exception invoking periodic operation: ", t);
} finally {
Thread.currentThread().setContextClassLoader(cl);
}
Container[] children = container.findChildren();
for (int i = 0; i < children.length; i++) {
if (children[i].getBackgroundProcessorDelay() <= 0) {
processChildren(children[i], cl);
}
}
}
可以看到processChildren呼叫了容器的backgroundProcess方法並且呼叫了子容器的這個方法,繼續看backgroundProcess裡面幹了些什麼:
ps:這個方法在ContainerBase有預設實現,但只有StandardContext以及StandardWrapper方法,這個預設實現主要是為StandardContext準備的,它檢查java類檔案是否有所改變,然後呼叫reload方法重啟容器,或者為容器設定ClassLoader。
下面是StandardWrapper的backgroundProcess方法:
/**
* Execute a periodic task, such as reloading, etc. This method will be
* invoked inside the classloading context of this container. Unexpected
* throwables will be caught and logged.
*/
@Override
public void backgroundProcess() {
super.backgroundProcess();
if (!getState().isAvailable())
return;
if (getServlet() != null && (getServlet() instanceof PeriodicEventListener)) {
((PeriodicEventListener) getServlet()).periodicEvent();
}
}
這個方法沒什麼特別的,主要是為Servlet型別為JspServlet的StandardWrapper準備的,因為JspServlet實現了PeriodicEventListener。下面是呼叫的該方法:
@Override
public void periodicEvent() {
rctxt.checkUnload();
rctxt.checkCompile();
}
checkCompile方法主要是呼叫JspCompiler重新編譯Jsp檔案(丟掉當前jsp類載入器,重新建立一個),前提是修改時間 + 超時時間 小於當前時間才會重新編譯,當然在tomcat7中,超時時間預設為0,且無法設定。其實這個方法並不是直接由servlet的rctxt屬性來呼叫,中間還會委託給其他物件,由其他物件來完成呼叫,但由於篇幅有限,且這也不是本篇討論的重點。
以上就是我對tomcat類載入機制的學習與理解,如果不足或錯誤,歡迎在下面評論指正。最後,感謝你的閱讀! ( ̄▽ ̄)~*
相關推薦
tomcat學習之四:tomcat的類載入機制
tomcat的類載入機制遵循了java類載入機制中經典的雙親委派模型。所以要了解tomcat的類載入機制需要先了解雙親委派模型。 在程式中用到的類需要由類載入器將類的class檔案載入到記憶體中,然後經由JVM驗證、解析、初始化後才能使用,
Tomcat學習之二:tomcat安裝、配置及目錄檔案說明
一、下載JDK和Tomcat 二、安裝JDK 點選JDK應用程式預設安裝即可,記下JDK的安裝目錄(例如:C:\Program Files\Java\jdk1.7.0_45)。 三、配置JDK和Tomcat 1. 配置JDK
Docker學習之四:使用docker安裝mysql,碰到了一個啟動的坑
Docker學習之四:使用docker安裝mysql,碰到了一個啟動的坑 第一步,從docker中拉取MySQL映象 $ sudo docker pull mysql $ sudo docker images 第二步,建立並啟動一個MySQL容器 $ sudo doc
C++11併發學習之四:執行緒同步(續)
有時候,在第一個執行緒完成前,可能需要等待另一個執行緒執行完成。C++標準庫提供了一些工具可用於這種同步操作,形式上表現為條件變數(condition variable)和期望(future)。 一.條件變數(condition variable) C++標準庫對條件變數有兩套實現:std::c
USB開裝置開發學習之四:USB傳輸之控制傳輸
原文:https://blog.csdn.net/go_str/article/details/80782229 前言 USB控制傳輸分為以下四種: 批量傳輸:批量傳輸一般用於批量的和非實時的資料傳輸,通俗的來說就是用於資料量大但對時間
六天搞懂“深度學習”之四:基於神經網路的分類
分類用於確定資料所歸屬的類別,而回歸是根據已知的資料進行推斷或估計某個未知量,比如根據年齡和教育水平進行收入預測分析。分類的典型應用是垃圾郵件過濾和字元識別。 雖然神經網路適用於分類和迴歸,但卻很少用於迴歸。這不是因為它的效能不好,而是因為大多數迴歸問題可以用更簡單的模型來解決。(迴歸問
VVC程式碼 BMS 幀內預測學習之四:xFillReferenceSamples()
xFillReferenceSamples()函式是參考畫素的獲取過程。 主要步驟: 1、分析臨近的畫素是否可獲取 2、進行參考樣本的填充:若臨近的畫素全部可獲取,則賦值;全部不可獲取,則賦預設值;若部分可獲取,則對可獲取的賦對應的值,不可獲取的用預設值填充。
理解JVM(四):JVM類載入機制
Class檔案 我們寫的Java程式碼,經過編譯器編譯之後,就成為了.class檔案,從本地機器碼變成了位元組碼。Class檔案是一組以8位位元組為基礎單位的二進位制流,各個資料專案嚴格按照順序緊湊地排列在Class檔案之中,中間沒有新增任何分隔符,這使得整個
MyBatis學習之四:MyBatis配置檔案
在定義sqlSessionFactory時需要指定MyBatis主配置檔案: Xml程式碼 <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
NLTK學習之四:文字資訊抽取
1 資訊抽取 從資料庫中抽取資訊是容易的,但對於從自然文字中抽取資訊則不那麼直觀。通常資訊抽取的流程如下圖: 它開始於分句,分詞。接下來進行詞性標註,識別其中的命名實體,最後使用關係識別搜尋相近實體間的可能的關係。 2 分塊 分塊是實體識別(NE
WebRTC學習之四:最簡單的語音聊天
VoiceEngine中與最簡單語音聊天相關的標頭檔案有五個,如下表所示: 標頭檔案 包含的類 說明 voe_base.h VoiceEngineObserver VoiceEngine VoEBase 1.預設使用G.711通過RTP進行全雙工的VoI
深度學習之四:卷積神經網路基礎
計算機視覺在深度學習的幫助下取得了令人驚歎的進展,其中發揮重要作用的是卷積神經網路。本節總結了卷積神經的原理與實現方法。 1 卷積神經網路 1.1 計算機視覺與深度學習 計算機視覺要解決的問題是如何讓機器理解現實世界的現象。目前主要處理的問題如影象
強化學習之四:基於策略的Agents (Policy-based Agents)
本文是對Arthur Juliani在Medium平臺釋出的強化學習系列教程的個人中文翻譯,該翻譯是基於個人分享知識的目的進行的,歡迎交流!(This article is my personal translation for the tutorial wri
Echarts學習之四:series-pie餅圖
mytextStyle={ color:"#333", //文字顏色 fontStyle:"normal", //italic斜體 oblique傾斜 fontWeight:"normal",
seL4微核心學習之四:系統呼叫
seL4系統呼叫主要有以下八個: seL4 Send(): 通過已被命名的cap傳遞訊息,然後允許程式繼續,如果呼叫這個cap的是endpoint,且沒有receiver接收訊息,sender將會
Elastic search 系統學習之四: 文件API
一、Index API 1、插入文件 curl -XPUT 'localhost:9200/twitter/tweet/1?pretty' -H 'Content-Type: application/json' -d' { "user" : "kimchy",
jackson學習之四:WRAP_ROOT_VALUE(root物件)
### 歡迎訪問我的GitHub [https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) 內容:所有原創文章分類彙總及配套原始碼,涉及Java、Docker、Kubernetes、DevOPS等; ### 系
JUnit5學習之四:按條件執行
### 歡迎訪問我的GitHub [https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) 內容:所有原創文章分類彙總及配套原始碼,涉及Java、Docker、Kubernetes、DevOPS等; ### 關
JVM(二):JVM類載入機制
如下圖所示,JVM類載入機制分為五個部分:載入,驗證,準備,解析,初始化,下面我們就分別來看一下這五個過程。 載入 載入是類載入過程中的一個階段,這個階段會在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的入口。注意這裡不一
JVM系列第7講:JVM 類載入機制
當 Java 虛擬機器將 Java 原始碼編譯為位元組碼之後,虛擬機器便可以將位元組碼讀取進記憶體,從而進行解析、執行等整個過程,這個過程我們叫:Java 虛擬機器的類載入機制。JVM 虛擬機器執行 class 位元組碼的過程可以分為七個階段:載入、驗證、準備、解析、初始化、使用、解除安裝。 在開始聊之前,先