在研究tomcat 類加載之前,我們復習一下或者說鞏固一下java 默認的類加載器。樓主以前對類加載也是懵懵懂懂,借此機會,也好好復習一下。
樓主翻開了神書《深入理解Java虛擬機》第二版,p227, 關於類加載器的部分。請看:
什么是類加載機制?
Java虛擬機把描述類的數據從Class文件加載進內存,並對數據進行校驗,轉換解析和初始化,最終形成可以唄虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。
虛擬機設計團隊把類加載階段中的“通過一個類的全限定名來獲取描述此類的二進制字節流”這個動作放到Java虛擬機外部去實現,以便讓應用程序自己決定如何去獲取所需要的類。實現這動作的代碼模塊成為“類加載器”。
類與類加載器的關系
類加載器雖然只用於實現類的加載動作,但它在Java程序中起到的作用卻遠遠不限於類加載階段。對於任意一個類,都需要由加載他的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類命名空間。這句話可以表達的更通俗一些:比較兩個類是否“相等”,
只有在這兩個類是由同一個類加載器加載的前提下才有意義
,否則,即使這兩個類來自同一個Class文件,被同一個虛擬機加載,只要加載他們的類加載器不同,那這個兩個類就必定不相等。
什么是雙親委任模型?
-
從Java虛擬機的角度來說,只存在兩種不同類加載器:一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現(只限HotSpot),是虛擬機自身的一部分;另一種就是所有其他的類加載器,這些類加載器都由Java語言實現,獨立於虛擬機外部,並且全都繼承自抽象類
java.lang.ClassLoader
. -
從Java開發人員的角度來看,類加載還可以划分的更細致一些,絕大部分Java程序員都會使用以下3種系統提供的類加載器:
- 啟動類加載器(Bootstrap ClassLoader):這個類加載器復雜將存放在 JAVA_HOME/lib 目錄中的,或者被-Xbootclasspath 參數所指定的路徑種的,並且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即使放在lib目錄下也不會重載)。
- 擴展類加載器(Extension ClassLoader):這個類加載器由sun.misc.Launcher$ExtClassLoader實現,它負責夾雜JAVA_HOME/lib/ext 目錄下的,或者被java.ext.dirs 系統變量所指定的路徑種的所有類庫。開發者可以直接使用擴展類加載器。
- 應用程序類加載器(Application ClassLoader):這個類加載器由sun.misc.Launcher$AppClassLoader 實現。由於這個類加載器是ClassLoader 種的getSystemClassLoader方法的返回值,所以也成為系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫。開發者可以直接使用這個類加載器,如果應用中沒有定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
這些類加載器之間的關系一般如下圖所示:
類加載器的雙親委派模型在JDK1.2 期間被引入並被廣泛應用於之后的所有Java程序中,但他並不是個強制性的約束模型,而是Java設計者推薦給開發者的一種類加載器實現方式。
雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求,他首先不會自己去嘗試加載這個類,而是把這個請求委派父類加載器去完成。每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個請求(他的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。
為什么要這么做呢?
如果沒有使用雙親委派模型,由各個類加載器自行加載的話,如果用戶自己編寫了一個稱為java.lang.Object的類,並放在程序的ClassPath中,那系統將會出現多個不同的Object類, Java類型體系中最基礎的行為就無法保證。應用程序也將會變得一片混亂。
雙親委任模型時如何實現的?
非常簡單:所有的代碼都在java.lang.ClassLoader中的loadClass方法之中,代碼如下:
先檢查是否已經被加載過,若沒有加載則調用父加載器的loadClass方法, 如父加載器為空則默認使用啟動類加載器作為父加載器。如果父類加載失敗,拋出ClassNotFoundException 異常后,再調用自己的findClass方法進行加載。
如何破壞雙親委任模型?
雙親委任模型不是一個強制性的約束模型,而是一個建議型的類加載器實現方式。在Java的世界中大部分的類加載器都遵循者模型,但也有例外,到目前為止,雙親委派模型有過3次大規模的“被破壞”的情況。
第一次:在雙親委派模型出現之前-----即JDK1.2發布之前。
第二次:是這個模型自身的缺陷導致的。我們說,雙親委派模型很好的解決了各個類加載器的基礎類的統一問題(越基礎的類由越上層的加載器進行加載),基礎類之所以稱為“基礎”,是因為它們總是作為被用戶代碼調用的API, 但沒有絕對,如果基礎類調用會用戶的代碼怎么辦呢?
這不是沒有可能的。一個典型的例子就是JNDI服務,JNDI現在已經是Java的標准服務,它的代碼由啟動類加載器去加載(在JDK1.3時就放進去的rt.jar),但它需要調用由獨立廠商實現並部署在應用程序的ClassPath下的JNDI接口提供者(SPI, Service Provider Interface)的代碼,但啟動類加載器不可能“認識“這些代碼啊。因為這些類不在rt.jar中,但是啟動類加載器又需要加載。怎么辦呢?
為了解決這個問題,Java設計團隊只好引入了一個不太優雅的設計:線程上下文類加載器(Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContextClassLoader方法進行設置。如果創建線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局范圍內都沒有設置過多的話,那這個類加載器默認即使應用程序類加載器。
嘿嘿,有了線程上下文加載器,JNDI服務使用這個線程上下文加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載的動作,這種行為實際上就是打通了雙親委派模型的層次結構來逆向使用類加載器,實際上已經違背了雙親委派模型的一般性原則。但這無可奈何,Java中所有涉及SPI的加載動作基本勝都采用這種方式。例如JNDI,JDBC,JCE,JAXB,JBI等。
第三次:為了實現熱插拔,熱部署,模塊化,意思是添加一個功能或減去一個功能不用重啟,只需要把這模塊連同類加載器一起換掉就實現了代碼的熱替換。
Tomcat 的類加載器是怎么設計的?
首先,我們來問個問題:
Tomcat 如果使用默認的類加載機制行不行?
我們思考一下:Tomcat是個web容器, 那么它要解決什么問題:
- 一個web容器可能需要部署兩個應用程序,不同的應用程序可能會依賴同一個第三方類庫的不同版本,不能要求同一個類庫在同一個服務器只有一份,因此要保證每個應用程序的類庫都是獨立的,保證相互隔離。
- 部署在同一個web容器中相同的類庫相同的版本可以共享。否則,如果服務器有10個應用程序,那么要有10份相同的類庫加載進虛擬機,這是扯淡的。
- web容器也有自己依賴的類庫,不能於應用程序的類庫混淆。基於安全考慮,應該讓容器的類庫和程序的類庫隔離開來。
- web容器要支持jsp的修改,我們知道,jsp 文件最終也是要編譯成class文件才能在虛擬機中運行,但程序運行后修改jsp已經是司空見慣的事情,否則要你何用? 所以,web容器需要支持 jsp 修改后不用重啟。
答案是不行的。為什么?我們看,第一個問題,如果使用默認的類加載器機制,那么是無法加載兩個相同類庫的不同版本的,默認的類加器是不管你是什么版本的,只在乎你的全限定類名,並且只有一份。第二個問題,默認的類加載器是能夠實現的,因為他的職責就是保證唯一性。第三個問題和第一個問題一樣。我們再看第四個問題,我們想我們要怎么實現jsp文件的熱修改(樓主起的名字),jsp 文件其實也就是class文件,那么如果修改了,但類名還是一樣,類加載器會直接取方法區中已經存在的,修改后的jsp是不會重新加載的。那么怎么辦呢?我們可以直接卸載掉這jsp文件的類加載器,所以你應該想到了,每個jsp文件對應一個唯一的類加載器,當一個jsp文件修改了,就直接卸載這個jsp類加載器。重新創建類加載器,重新加載jsp文件。
Tomcat 如何實現自己獨特的類加載機制?
我們看看他們的設計圖:

我們在這張圖中看到很多類加載器,除了Jdk自帶的類加載器,我們尤其關心Tomcat自身持有的類加載器。仔細一點我們很容易發現:Catalina類加載器和Shared類加載器,他們並不是父子關系,而是兄弟關系。為啥這樣設計,我們得分析一下每個類加載器的用途,才能知曉。
- Common類加載器,負責加載Tomcat和Web應用都復用的類
- Catalina類加載器,負責加載Tomcat專用的類,而這些被加載的類在Web應用中將不可見
- Shared類加載器,負責加載Tomcat下所有的Web應用程序都復用的類,而這些被加載的類在Tomcat中將不可見
- WebApp類加載器,負責加載具體的某個Web應用程序所使用到的類,而這些被加載的類在Tomcat和其他的Web應用程序都將不可見
- Jsp類加載器,每個jsp頁面一個類加載器,不同的jsp頁面有不同的類加載器,方便實現jsp頁面的熱插拔
源碼閱讀
Tomcat啟動的入口在Bootstrap的main()方法
。main()
方法執行前,必然先執行其static{}
塊。所以我們首先分析static{}
塊,然后分析main()
方法
Bootstrap.static{}
static { // 獲取用戶目錄 // Will always be non-null String userDir = System.getProperty("user.dir"); // 第一步,從環境變量中獲取catalina.home,在沒有獲取到的時候將執行后面的獲取操作 // Home first String home = System.getProperty(Globals.CATALINA_HOME_PROP); File homeFile = null; if (home != null) { File f = new File(home); try { homeFile = f.getCanonicalFile(); } catch (IOException ioe) { homeFile = f.getAbsoluteFile(); } } // 第二步,在第一步沒獲取的時候,從bootstrap.jar所在目錄的上一級目錄獲取 if (homeFile == null) { // First fall-back. See if current directory is a bin directory // in a normal Tomcat install File bootstrapJar = new File(userDir, "bootstrap.jar"); if (bootstrapJar.exists()) { File f = new File(userDir, ".."); try { homeFile = f.getCanonicalFile(); } catch (IOException ioe) { homeFile = f.getAbsoluteFile(); } } } // 第三步,第二步中的bootstrap.jar可能不存在,這時我們直接把user.dir作為我們的home目錄 if (homeFile == null) { // Second fall-back. Use current directory File f = new File(userDir); try { homeFile = f.getCanonicalFile(); } catch (IOException ioe) { homeFile = f.getAbsoluteFile(); } } // 重新設置catalinaHome屬性 catalinaHomeFile = homeFile; System.setProperty( Globals.CATALINA_HOME_PROP, catalinaHomeFile.getPath()); // 接下來獲取CATALINA_BASE(從系統變量中獲取),若不存在,則將CATALINA_BASE保持和CATALINA_HOME相同 // Then base String base = System.getProperty(Globals.CATALINA_BASE_PROP); if (base == null) { catalinaBaseFile = catalinaHomeFile; } else { File baseFile = new File(base); try { baseFile = baseFile.getCanonicalFile(); } catch (IOException ioe) { baseFile = baseFile.getAbsoluteFile(); } catalinaBaseFile = baseFile; } // 重新設置catalinaBase屬性 System.setProperty( Globals.CATALINA_BASE_PROP, catalinaBaseFile.getPath()); }
我們把代碼中的注釋搬下來總結一下:
- 獲取用戶目錄
- 第一步,從環境變量中獲取catalina.home,在沒有獲取到的時候將執行后面的獲取操作
- 第二步,在第一步沒獲取的時候,從bootstrap.jar所在目錄的上一級目錄獲取
- 第三步,第二步中的bootstrap.jar可能不存在,這時我們直接把user.dir作為我們的home目錄
- 重新設置catalinaHome屬性
- 接下來獲取CATALINA_BASE(從系統變量中獲取),若不存在,則將CATALINA_BASE保持和CATALINA_HOME相同
- 重新設置catalinaBase屬性
簡單總結一下,就是加載並設置catalinaHome和catalinaBase相關的信息,以備后續使用。
main()
main方法大體分成兩塊,一塊為init,另一塊為load+start。
public static void main(String args[]) { // 第一塊,main方法第一次執行的時候,daemon肯定為null,所以直接new了一個Bootstrap對象,然后執行其init()方法 if (daemon == null) { // Don't set daemon until init() has completed Bootstrap bootstrap = new Bootstrap(); try { bootstrap.init(); } catch (Throwable t) { handleThrowable(t); t.printStackTrace(); return; } // daemon守護對象設置為bootstrap daemon = bootstrap; } else { // When running as a service the call to stop will be on a new // thread so make sure the correct class loader is used to prevent // a range of class not found exceptions. Thread.currentThread().setContextClassLoader(daemon.catalinaLoader); } // 第二塊,執行守護對象的load方法和start方法 try { String command = "start"; if (args.length > 0) { command = args[args.length - 1]; } if (command.equals("startd")) { args[args.length - 1] = "start"; daemon.load(args); daemon.start(); } else if (command.equals("stopd")) { args[args.length - 1] = "stop"; daemon.stop(); } else if (command.equals("start")) { daemon.setAwait(true); daemon.load(args); daemon.start(); if (null == daemon.getServer()) { System.exit(1); } } else if (command.equals("stop")) { daemon.stopServer(args); } else if (command.equals("configtest")) { daemon.load(args); if (null == daemon.getServer()) { System.exit(1); } System.exit(0); } else { log.warn("Bootstrap: command \"" + command + "\" does not exist."); } } catch (Throwable t) { // Unwrap the Exception for clearer error reporting if (t instanceof InvocationTargetException && t.getCause() != null) { t = t.getCause(); } handleThrowable(t); t.printStackTrace(); System.exit(1); } }
我們點到init()
里面去看看~
public void init() throws Exception { // 非常關鍵的地方,初始化類加載器s,后面我們會詳細具體地分析這個方法 initClassLoaders(); // 設置上下文類加載器為catalinaLoader,這個類加載器負責加載Tomcat專用的類 Thread.currentThread().setContextClassLoader(catalinaLoader); // 暫時略過,后面會講 SecurityClassLoad.securityClassLoad(catalinaLoader); // 使用catalinaLoader加載我們的Catalina類 // Load our startup class and call its process() method if (log.isDebugEnabled()) log.debug("Loading startup class"); Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina"); Object startupInstance = startupClass.getConstructor().newInstance(); // 設置Catalina類的parentClassLoader屬性為sharedLoader // Set the shared extensions class loader if (log.isDebugEnabled()) log.debug("Setting startup class properties"); String methodName = "setParentClassLoader"; Class<?> paramTypes[] = new Class[1]; paramTypes[0] = Class.forName("java.lang.ClassLoader"); Object paramValues[] = new Object[1]; paramValues[0] = sharedLoader; Method method = startupInstance.getClass().getMethod(methodName, paramTypes); method.invoke(startupInstance, paramValues); // catalina守護對象為剛才使用catalinaLoader加載類、並初始化出來的Catalina對象 catalinaDaemon = startupInstance; }
關鍵的方法initClassLoaders
,這個方法負責初始化Tomcat的類加載器。通過這個方法,我們很容易驗證我們上一小節提到的Tomcat類加載圖。
private void initClassLoaders() { try { // 創建commonLoader,如果未創建成果的話,則使用應用程序類加載器作為commonLoader 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,父類加載器為commonLoader catalinaLoader = createClassLoader("server", commonLoader); // 創建sharedLoader,父類加載器為commonLoader sharedLoader = createClassLoader("shared", commonLoader); } catch (Throwable t) { // 如果創建的過程中出現異常了,日志記錄完成之后直接系統退出 handleThrowable(t); log.error("Class loader creation threw exception", t); System.exit(1); } }
createClassLoader
,所以,我們進一步分析一下這個方法。
createClassLoader
用到了CatalinaProperties.getProperty("xxx")方法,這個方法用於從
conf/catalina.properties
文件獲取屬性值。
private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception { // 獲取類加載器待加載的位置,如果為空,則不需要加載特定的位置,使用父類加載返回回去。 String value = CatalinaProperties.getProperty(name + ".loader"); if ((value == null) || (value.equals(""))) return parent; // 替換屬性變量,比如:${catalina.base}、${catalina.home} value = replace(value); List<Repository> repositories = new ArrayList<>(); // 解析屬性路徑變量為倉庫路徑數組 String[] repositoryPaths = getPaths(value); // 對每個倉庫路徑進行repositories設置。我們可以把repositories看成一個個待加載的位置對象,可以是一個classes目錄,一個jar文件目錄等等 for (String repository : repositoryPaths) { // Check for a JAR URL repository 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); }
我們來分析一下ClassLoaderFactory.createClassLoader
--類加載器工廠創建類加載器。
public static ClassLoader createClassLoader(List<Repository> repositories, final ClassLoader parent) throws Exception { if (log.isDebugEnabled()) log.debug("Creating new class loader"); // Construct the "class path" for this class loader Set<URL> set = new LinkedHashSet<>(); // 遍歷repositories,對每個repository進行類型判斷,並生成URL,每個URL我們都要校驗其有效性,有效的URL我們會放到URL集合中 if (repositories != null) { for (Repository repository : repositories) { if (repository.getType() == RepositoryType.URL) { URL url = buildClassLoaderUrl(repository.getLocation()); if (log.isDebugEnabled()) log.debug(" Including URL " + url); set.add(url); } else if (repository.getType() == RepositoryType.DIR) { File directory = new File(repository.getLocation()); directory = directory.getCanonicalFile(); if (!validateFile(directory, RepositoryType.DIR)) { continue; } URL url = buildClassLoaderUrl(directory); if (log.isDebugEnabled()) log.debug(" Including directory " + url); set.add(url); } else if (repository.getType() == RepositoryType.JAR) { File file=new File(repository.getLocation()); file = file.getCanonicalFile(); if (!validateFile(file, RepositoryType.JAR)) { continue; } URL url = buildClassLoaderUrl(file); if (log.isDebugEnabled()) log.debug(" Including jar file " + url); set.add(url); } else if (repository.getType() == RepositoryType.GLOB) { File directory=new File(repository.getLocation()); directory = directory.getCanonicalFile(); if (!validateFile(directory, RepositoryType.GLOB)) { continue; } if (log.isDebugEnabled()) log.debug(" Including directory glob " + directory.getAbsolutePath()); String filenames[] = directory.list(); if (filenames == null) { continue; } for (int j = 0; j < filenames.length; j++) { String filename = filenames[j].toLowerCase(Locale.ENGLISH); if (!filename.endsWith(".jar")) continue; File file = new File(directory, filenames[j]); file = file.getCanonicalFile(); if (!validateFile(file, RepositoryType.JAR)) { continue; } if (log.isDebugEnabled()) log.debug(" Including glob jar file " + file.getAbsolutePath()); URL url = buildClassLoaderUrl(file); set.add(url); } } } } // Construct the class loader itself final URL[] array = set.toArray(new URL[set.size()]); if (log.isDebugEnabled()) for (int i = 0; i < array.length; i++) { log.debug(" location " + i + " is " + array[i]); } // 從這兒看,最終所有的類加載器都是URLClassLoader的對象~~ return AccessController.doPrivileged( new PrivilegedAction<URLClassLoader>() { @Override public URLClassLoader run() { if (parent == null) return new URLClassLoader(array); else return new URLClassLoader(array, parent); } }); }
我們已經對initClassLoaders分析完了,接下來分析SecurityClassLoad.securityClassLoad
,我們看看里面做了什么事情
public static void securityClassLoad(ClassLoader loader) throws Exception { securityClassLoad(loader, true); } static void securityClassLoad(ClassLoader loader, boolean requireSecurityManager) throws Exception { if (requireSecurityManager && System.getSecurityManager() == null) { return; } loadCorePackage(loader); loadCoyotePackage(loader); loadLoaderPackage(loader); loadRealmPackage(loader); loadServletsPackage(loader); loadSessionPackage(loader); loadUtilPackage(loader); loadValvesPackage(loader); loadJavaxPackage(loader); loadConnectorPackage(loader); loadTomcatPackage(loader); } private static final void loadCorePackage(ClassLoader loader) throws Exception { final String basePackage = "org.apache.catalina.core."; loader.loadClass(basePackage + "AccessLogAdapter"); loader.loadClass(basePackage + "ApplicationContextFacade$PrivilegedExecuteMethod"); loader.loadClass(basePackage + "ApplicationDispatcher$PrivilegedForward"); loader.loadClass(basePackage + "ApplicationDispatcher$PrivilegedInclude"); loader.loadClass(basePackage + "ApplicationPushBuilder"); loader.loadClass(basePackage + "AsyncContextImpl"); loader.loadClass(basePackage + "AsyncContextImpl$AsyncRunnable"); loader.loadClass(basePackage + "AsyncContextImpl$DebugException"); loader.loadClass(basePackage + "AsyncListenerWrapper"); loader.loadClass(basePackage + "ContainerBase$PrivilegedAddChild"); loadAnonymousInnerClasses(loader, basePackage + "DefaultInstanceManager"); loader.loadClass(basePackage + "DefaultInstanceManager$AnnotationCacheEntry"); loader.loadClass(basePackage + "DefaultInstanceManager$AnnotationCacheEntryType"); loader.loadClass(basePackage + "ApplicationHttpRequest$AttributeNamesEnumerator"); }
這兒其實就是使用catalinaLoader加載tomcat源代碼里面的各個專用類。我們大致羅列一下待加載的類所在的package:
- org.apache.catalina.core.*
- org.apache.coyote.*
- org.apache.catalina.loader.*
- org.apache.catalina.realm.*
- org.apache.catalina.servlets.*
- org.apache.catalina.session.*
- org.apache.catalina.util.*
- org.apache.catalina.valves.*
- javax.servlet.http.Cookie
- org.apache.catalina.connector.*
- org.apache.tomcat.*
好了,至此我們已經分析完了init里面涉及到的幾個關鍵方法
WebApp類加載器
到這兒,我們隱隱感覺到少分析了點什么!沒錯,就是WebApp類加載器。整個啟動過程分析下來,我們仍然沒有看到這個類加載器。它又是在哪兒出現的呢?
我們知道WebApp類加載器是Web應用私有的,而每個Web應用其實算是一個Context,那么我們通過Context的實現類應該可以發現。在Tomcat中,Context的默認實現為StandardContext
,我們看看這個類的startInternal()方法
,在這兒我們發現了我們感興趣的WebApp類加載器。
protected synchronized void startInternal() throws LifecycleException { if (getLoader() == null) { WebappLoader webappLoader = new WebappLoader(getParentClassLoader()); webappLoader.setDelegate(getDelegate()); setLoader(webappLoader); } }
入口代碼非常簡單,就是webappLoader不存在的時候創建一個,並調用setLoader
方法。
總結
我們終於完整地分析完了Tomcat的整個啟動過程+類加載過程。也了解並學習了Tomcat不同的類加載機制是為什么要這樣設計,帶來的附加作用又是怎樣的。