Tomcat 內部定義了多個 ClassLoader,以便應用和容器訪問不一樣存儲庫中的類和資源,同時達到應用間類隔離的目的。本文首發於公衆號:頓悟源碼。java
類加載就是把編譯生成的 class 文件,加載到 JVM 內存中(永久代/元空間)。web
類加載器之因此能實現類隔離,是由於兩個類相等的前提是它們由同一個類加載器加載,不然一定不相等。緩存
JVM 在加載時,採用的是一種雙親委託機制,當類加載器要加載一個類時,加載順序是:tomcat
這個機制的好處就是可以保證核心類庫不被覆蓋。app
而按照 Servlet 規範的建議,Webapp 加載器略有不一樣,它首先會在本身的資源庫中搜索,而不是向上委託,打破了標準的委託機制,來看下 Tomcat 的設計和實現。優化
Tomcat 總體類加載器結構以下:this
其中 JDK 內部提供的類加載器分別是:線程
Tomcat 自定義實現的類加載器分別是:debug
在實現時,上圖不是繼承關係,而是經過組合體現父子關係。Tomcat 類加載器的源碼類圖:設計
Common、Catalina 、Shared 它們都是 StandardClassLoader 的實例,在默認狀況下,它們引用的是同一個對象。其中 StandardClassLoader 與 URLClassLoader 沒有區別;WebappClassLoader 則按規範實現如下順序的查找並加載:
接下來看下源碼實現。
common 類加載器是在 Bootstrap 的 initClassLoaders 初始化的,源碼以下:
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(); } // 指定倉庫路徑配置文件前綴和父加載器,建立 ClassLoader 實例 catalinaLoader = createClassLoader("server", commonLoader); sharedLoader = createClassLoader("shared", commonLoader); } catch (Throwable t) { log.error("Class loader creation threw exception", t); System.exit(1); } }
能夠看到分別建立了三個類加載器,createClassLoader 就是根據配置獲取資源倉庫地址,最後返回一個 StandardClassLoader 實例,核心代碼以下:
private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception { String value = CatalinaProperties.getProperty(name + ".loader"); if ((value == null) || (value.equals(""))) return parent; // 若是沒有配置,則返回傳入的父加載器 ArrayList repositoryLocations = new ArrayList(); ArrayList repositoryTypes = new ArrayList(); ... // 獲取資源倉庫路徑 String[] locations = (String[]) repositoryLocations.toArray(new String[0]); Integer[] types = (Integer[]) repositoryTypes.toArray(new Integer[0]); // 建立一個 StandardClassLoader 對象 ClassLoader classLoader = ClassLoaderFactory.createClassLoader (locations, types, parent); ... return classLoader; }
類加載器初始化完畢後,會建立一個 Catalina 對象,最終會調用它的 load 方法,解析 server.xml 初始化容器內部組件。那麼容器,好比 Engine,又是怎麼關聯到這個設置的父加載器的呢?
Catalina 對象有一個 parentClassLoader 成員變量,它是全部組件的父加載器,默認是 AppClassLoader,在此對象建立完畢時,會反射調用它的 setParentClassLoader 方法,將父加載器設爲 sharedLoader。
而 Tomcat 內部頂級容器 Engine 在初始化時,Digester 有一個 SetParentClassLoaderRule 規則,會將 Catalina 的 parentClassLoader 經過 Engine.setParentClassLoader 方法關聯起來。
答案是使用 Thread.getContextClassLoader() - 當前線程的上下文加載器,該加載器可經過 Thread.setContextClassLoader() 在代碼運行時動態設置。
默認狀況下,Thread 上下文加載器繼承自父線程,也就是說全部線程默認上下文加載器都與第一個啓動的線程相同,也就是 main 線程,它的上下文加載器是 AppClassLoader。
Tomcat 就是在 StandardContext 啓動時首先初始化一個 WebappClassLoader 而後設置爲當前線程的上下文加載器,最後將其封裝爲 Loader 對象,藉助容器之間的父子關係,在加載 Servlet 類時使用。
Web 應用的類加載是由 WebappClassLoader 的方法 loadClass(String, boolean) 完成,核心代碼以下:
public synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { ... Class clazz = null; // (0) 檢查自身內部緩存中是否已經加載 clazz = findLoadedClass0(name); if (clazz != null) { if (log.isDebugEnabled()) log.debug(" Returning class from cache"); if (resolve) resolveClass(clazz); return (clazz); } // (0.1) 檢查 JVM 的緩存中是否已經加載 clazz = findLoadedClass(name); if (clazz != null) { if (log.isDebugEnabled()) log.debug(" Returning class from cache"); if (resolve) resolveClass(clazz); return (clazz); } // (0.2) 嘗試使用系統類加載加載,防止覆蓋 J2SE 類 try { clazz = system.loadClass(name); if (clazz != null) { if (resolve) resolveClass(clazz); return (clazz); } } catch (ClassNotFoundException e) {// Ignore} // (0.5) 使用 SecurityManager 檢查是否有此類的訪問權限 if (securityManager != null) { int i = name.lastIndexOf('.'); if (i >= 0) { try { securityManager.checkPackageAccess(name.substring(0,i)); } catch (SecurityException se) { String error = "Security Violation, attempt to use " + "Restricted Class: " + name; log.info(error, se); throw new ClassNotFoundException(error, se); } } } boolean delegateLoad = delegate || filter(name); // (1) 是否委託給父類,這裏默認爲 false if (delegateLoad) { ... } // (2) 嘗試查找本身的存儲庫並加載 try { clazz = findClass(name); if (clazz != null) { if (log.isDebugEnabled()) log.debug(" Loading class from local repository"); if (resolve) resolveClass(clazz); return (clazz); } } catch (ClassNotFoundException e) {} // (3) 若是此時還加載失敗,那麼將加載請求委託給父加載器 if (!delegateLoad) { if (log.isDebugEnabled()) log.debug(" Delegating to parent classloader at end: " + parent); ClassLoader loader = parent; if (loader == null) loader = system; try { clazz = loader.loadClass(name); if (clazz != null) { if (log.isDebugEnabled()) log.debug(" Loading class from parent"); if (resolve) resolveClass(clazz); return (clazz); } } catch (ClassNotFoundException e) {} } // 最後加載失敗,拋出異常 throw new ClassNotFoundException(name); }
在防止覆蓋 J2SE 類的時候,版本 Tomcat 6,使用的是 AppClassLoader,rt.jar 核心類庫是由 Bootstrap Classloader 加載的,可是在 Java 代碼是獲取不了這個加載器的,在高版本作了如下優化:
ClassLoader j = String.class.getClassLoader(); if (j == null) { j = getSystemClassLoader(); while (j.getParent() != null) { j = j.getParent(); } } this.javaseClassLoader = j;
也就是使用盡量接近 Bootstrap 加載器的類加載器。
相信大部分人都遇到過 ClassNotFoundException 這個異常,這背後就涉及到了類加載器,對加載的原理有必定的瞭解,有助於排查問題。