我曾覺得我瞭解雙親委派|8月更文挑戰

最近面試被問到雙親委派,對於雙親委派和破壞雙親委派的機制以前本身在《深刻理解Java虛擬機》中瞭解過,當時以爲挺簡單的一個概念,可是面試官仔細追問下去發現本身這塊的只是仍是存在一些誤區,當時這一個問題可能聊了有20分鐘,固然面試後天然就沒有下文了😂。java

故事要從這張雙親委派模型的類UML圖提及: image.png面試

  • 誤區1:當時看了類加載器的模型圖,覺得啓動類加載器、擴展類加載器、應用程序類加載器與自定義累加器之間是繼承關係,其實「父加載器」與「子加載器」是組合關係,這點其實和父子加載器的命名給人帶來的主觀想法相悖;
  • 誤區2:沒有理清findClass()loadClass()之間的關係和這兩個方法具體的做用,破壞雙親委派機制與遵循雙親委派機制與這兩個方法的相關性;
  • 誤區3:由於平時工做中沒怎麼使用過ClassLoader,沒有理清Java中ClassLoader類與Bootstrap Class LoderExtension Class LoaderApplication Class Loader之間的關係;錯誤的按照類加載器模型圖,認爲本身實現類加載器須要繼承應用程序類加載器以實現自定義類加載器。

誤區造成的最主要緣由是當時本身看《深刻理解Java虛擬機》的時候,其實仍是處於知識儲備、知識之間的關聯性還不夠到位的狀態,所以今天從新進行梳理一下~數據庫

雙親委派模型

什麼是雙親委派模型? 雙親委派模型定義了加載器加載類的方式,即當一個加載器收到加載一個類的請求時,首先會委託給父加載器進行處理加載,只有當其父加載器沒法加載時,當前加載器纔會進行加載;緩存

那麼何時會觸發類加載器進行類的加載呢? 當在程序中出現對於一個類的主動引用時,若是當前類還沒有被加載到方法區中時,會觸發對引用類的加載;安全

觸發類的加載後類加載器須要作些什麼? 虛擬機經過類加載器在Java類加載階段須要完成如下三件事:markdown

  • 經過一個類的全限定名來獲取定義此類的二進制字節流
  • 將這個字節流所表明的的靜態存儲結構轉化爲方法區的運行時數據結構
  • 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口。

那麼何時父加載器沒法加載一個類呢? 由於每一個加載器加載負責加載的類都是不一樣的:數據結構

  • 啓動類加載器: 主要加載<JAVA_HOME>\lib目錄下的類,並經過文件名匹配,如rt.jartools.jar這類Java核心類庫;這個類經過C++語言進行實現,是虛擬機的一部分;
  • 擴展類加載器: 主要加載<JAVA_HOME>\lib\ext目錄下的擴展類;經過Java語言進行實現,對應的類爲sun.misc.Launcher$ExtClassLoader
  • 應用程序類加載器: 主要加載用戶類路徑(classpath)下的全部類庫;經過Java語言進行實現,對應的類爲sun.misc.Launcher$AppClassLoader,能夠經過ClassLoader類中的getSystemClassLoader()方法獲取應用程序類加載器的引用,而且應用程序中若是沒有自定義類加載器,則會使用這個默認的類加載器;

雙親委派模型實現

JDK1.2以後,經過ClassLoader::loadClass()實現雙親委派模型,而且咱們使用ClassLoader時通常也會經過loadClass()方法進行類的加載:app

public abstract class ClassLoader {
    // 當前ClassLoader依賴的父加載器,注意父加載器與當前加載器是組合的關係!
    private final ClassLoader parent;
    
    // 在構建classLoader時,默認會將系統類加載器AppClassLoader做爲當前classLoader的父加載器
    // getSystemClassLoader()默認返回AppClassLoader
    // 而AppClassLoader的父加載器也是在初始化時設置爲 PlatformClassLoader
    protected ClassLoader() {
        this(checkCreateClassLoader(), null, getSystemClassLoader());
    }

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 1. 首先檢查是否已經加載過當前類
            Class<?> c = findLoadedClass(name);
            // 2. 未加載過當前類則進行加載
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        // 2.1 父加載器不爲空,則先使用父加載器進行加載
                        c = parent.loadClass(name, false);
                    } else {
                        // 2.2 父加載器爲空,則使用啓動類加載器進行加載
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ignore未找到當前類的異常,說明父加載器加載失敗
                }
                
                if (c == null) {
                    long t1 = System.nanoTime();
                    // 2.3 經過當前類加載器進行加載,注意此處是調用了findClass()方法
                    // 而不是像父類加載器經過loadClass()進行加載
                    // 所以若是要遵循雙親委派機制,子加載器須要經過覆蓋findClass()方法實現本身的加載邏輯
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
}
複製代碼

爲何要遵循雙親委派模型?

主要緣由在於經過雙親委派模型組織類加載器之間的關係,使得在Java中的類隨着它的類加載器也具有了一種帶優先級的層次關係,而這種關係保證了類加載的安全性maven

如用戶本身定義了一個Object類,那麼按照雙親委派模型,首先會經過啓動類加載器進行Object類的加載,則天然會加載在<JAVA_HOME>\lib目錄下的Obejct.Class,而不會加載用戶在自身classpath下定義的Object類,從而保證了Java核心類庫的類不會被錯誤的加載。ide

破壞雙親委派模型

遺憾的是,雙親委派模型在面對複雜的世界時,並不能完美的解決全部問題:

上層組件依賴下層組件的問題:

比較常見的是JDBC,由於咱們項目使用的數據庫多是OracleMySQLSQL Server等,而不一樣的數據庫由於使用方式的不一樣提供了不一樣的JDBC Driver,從而使得應用代碼能夠經過調用JDBC Driver Interface對不一樣數據進行增刪改查操做;

Connection connection = DriverManager.getConnection(jdbcUrl, username, password);
複製代碼

可是用於獲取數據庫鏈接的DriverManager類是位於rt.jar下的,按照雙親委派模型理應經過啓動類加載器進行加載,可是這個類又會由於項目使用的數據庫類型依賴不一樣的第三方實現的Driver類,而第三方的是實現類通常是放在用戶類路徑(classpath)下的,啓動類加載器天然沒法加載;

JDK1.6經過引入基於ThreadContextClassLoader(線程上下文類加載器)實現的ServiceLoader這個特殊的類加載器用來解決這個問題:

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 線程上下文類加載器默認爲AppClassLoader
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    // 經過AppClassLoader加載上層組件中依賴的用戶classpath下的包
    return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}
複製代碼

可是線程上下文使用不當可能會致使內存泄漏的問題,具體能夠參考這篇文章:線程上下文類加載器ContextClassLoader內存泄漏隱患

缺少隔離性問題:

對於在一個JVM上會運行多個Web容器的Tomcat,類可見性若是處理不夠穩當,會致使Web容器間缺少合理的隔離性從而帶來安全性和容器間依賴組件版本衝突以及沒法支持類熱更的問題;

  • 安全性問題: 按照雙親委派機制,對於同一個JVM下CLASSPATH指定路徑下的全部類和庫,都會經過AppClassLoader進行加載,所以CLASSPATH路徑下的類能夠被全部運行在JVM上的Web容器經過AppClassLoader類加載器進行加載、使用;可是對於運行在JVM上的每一個Web應用(servlet容器),容器中的servlet應該只能夠訪問自身WEB-INF/classesWEB-INF/lib目錄下的類;而若是經過AppClassLoader類加載器加載servlet,那麼servlet即可以經過AppClassLoader訪問到CLASSPATH下的其餘非本容器的類,而這顯然違反了servlet容器須要的類隔離性;

  • 版本衝突: 項目的多個模塊間可能存在對於同一個第三方組件不一樣版本的依賴,而按照雙親委派模型,若是使用AppClassLoader做爲類加載器加載各個模塊的依賴類,會致使在CLASSPATH路徑下只有一個版本的第三方組件會被加載;PS:相似的還有經過maven直接或間接引入了一個包的多個版本致使版本衝突的問題,也是經過自定義類加載器模型進行解決;

  • 沒法支持熱更: 由於按照雙親委派機制,不一樣模塊之間共同依賴了同一個AppClassLoader,使得模塊之間對於所加載的類存在耦合關係,即咱們不能夠隨便卸載並替換一個類,由於並不知道是否還有其餘模塊依賴這個版本的第三方組件;

解決方法:

爲了解決容器間缺少隔離性帶來的一系列問題,Tomcat自定義了自身的類加載器模型:

image.png

Tomcat容器經過複寫ClassLoader::loadClass()方法自定義了破壞雙親委派模型的WebappClassLoader類加載器,併爲每一個Web應用都關聯了一個WebAppClassLoader,從而實現了Web應用間特殊依賴類的隔離性即每一個應用首先會經過自身該類加載器加載自身WEB-INF/classesWEB-INF/lib目錄下依賴的類庫, 只有當加載不到所須要的類時纔會交由上層類加載器進行加載(破壞了雙親委派首先交由父類進行加載的規則);

可是Tomcat的每一個Web應用間可能也會存在共性的一些依賴,如Servlet規範相關包以及一些工具類包,所以Tomcat的類加載器模型經過父加載器SharedClassLoader進行加載在Web應用間共享的jar包。

而按照該類加載器模型,實現熱更只須要動態替換掉Web容器的WebAppClassLoader便可,避免了重啓整個Java項目帶來的損耗,並不須要擔憂被替換掉的類加載器所加載的類會不會被運行在同一個JVM上的Web應用引用的問題。

具體代碼實現

主要在複寫的loadClass()方法中進行自身加載模型的定義,Tomcat自定義類加載器除了上述中定義類加載器模型的目的以外,還有實現對於已加載類的緩存、類的預載入這兩個方面;

public abstract class WebappClassLoaderBase extends URLClassLoader {
    // ClassLoader對於已加載類的內存緩存
    protected final Map<String, ResourceEntry> resourceEntries = new ConcurrentHashMap<>();

    @Override
    // @param: name,須要加載的類的全限定名
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

        synchronized (getClassLoadingLock(name)) {
            Class<?> clazz = null;

            // 1. 緩存查詢
            // 1.1 WebAppClassLoader自身Map緩存
            clazz = findLoadedClass0(name);
            if (clazz != null) {
                return clazz;
            }

            // 1.2 查詢JVM緩存
            clazz = findLoadedClass(name);
            if (clazz != null) {
                return clazz;
            }

            // 2. 開始類加載
            String resourceName = binaryNameToPath(name, false);
            ClassLoader javaseLoader = getJavaseClassLoader();
            boolean tryLoadingFromJavaseLoader;
            try {
                // 2.1 首先嚐試使用AppClassLoader的父加載器進行加載
                // 主要是爲了不Tomcat類目下自定義類覆蓋JavaSE的核心類
                URL url = javaseLoader.getResource(resourceName);
                tryLoadingFromJavaseLoader = (url != null);
            } catch (Throwable t) {
                tryLoadingFromJavaseLoader = true;
            }

            if (tryLoadingFromJavaseLoader) {
                try {
                    clazz = javaseLoader.loadClass(name);
                    if (clazz != null) {
                        return clazz;
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }
            // 將加載類的全限定名傳入filter()方法判斷是否須要委託給父加載器進行加載
            boolean delegateLoad = delegate || filter(name, true);

            // 2.2 若是須要進行委託加載,則交由AppClassLoader進行類的加載
            // 而AppClassLoader是遵循雙親委派模型的
            if (delegateLoad) {
                try {
                    // 交由父類AppClassLoader進行加載
                    clazz = Class.forName(name, false, parent);
                    if (clazz != null) { 
                        return clazz;
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }

            // 2.2 不須要進行委託,則破壞雙親委派機制,首先經過WebAppClassLoader加載器加載
            // 具體的類加載方法由自身的findClass()方法進行實現
            // 這裏能夠看到,loadClass()方法通常用於實現類加載器模型,
            // 而由findClass()方法負責實現具體的類加載手段
            try {
                clazz = findClass(name);
                if (clazz != null) {
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }

            // 2.3 若是當前Web應用Class目錄下不存在當前須要加載的類
            // 則從新按照雙親委派模型使用父加載器進行最終的加載嘗試
            if (!delegateLoad) {
                try {
                    clazz = Class.forName(name, false, parent);
                    if (clazz != null) {
                        return clazz;
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }
        }
        
        // 2.4 未能成功加載所須要的類,拋出ClassNotFound異常
        throw new ClassNotFoundException(name);
    }
}
複製代碼

總結與參考(答誤區)

  1. 類加載器的雙親委派模型經過ClassLoader::loadClass()方法經過組合父加載器實現優先父加載器加載的模型;
  2. loadClass()通常用於實現類加載器模型,諸如雙親委派模型,Tomcat類加載器模型等;findClass()則用於在定義當前類加載器模型下當前類加載器具體的加載類手段
  3. 本身實現類加載器,若是按照雙親委派模型,則只須要複寫findClass()方法便可;若是須要破壞雙親委派模型,則須要複寫loadClass()findClass()

主要參考:

《深刻剖析Tomcat》 《深刻理解Java虛擬機 Edition3》

你肯定你真的理解"雙親委派"了嗎?!

如何本身手寫一個熱加載

Java類加載器 — classloader 的原理及應用

相關文章
相關標籤/搜索