天天看點

我竟然被“雙親委派”給虐了【轉】

作者 l Hollis來源 l Hollis(ID:hollischuang)

文章來源于微信公衆号: Hollis

  最近一段時間,我在面試的過程中,很喜歡問雙親委派的一些問題,因為我發現這個問題真的可以幫助我全方位的了解一個候選人。

  記得前幾天一次面試過程中,我和一位候選人聊到了JVM的類加載機制的問題,他談到了雙親委派,并且很自信的給我講了一下他對于雙親委派的了解。因為難得碰到一個對着塊知識了解的比較多的候選人,于是我們展開了"300回合"的交鋒,當問完這些問題的之後,大概半個小時已經過去了。最後,這個後續人和我說:"我萬萬沒想到,我一個工作7年的技術經理,竟然被雙親委派給虐了!!!"

  先來回顧下我都問了他哪些問題,看看你能回答上來多少個:

  1、什麼是雙親委派? 

  2、為什麼需要雙親委派,不委派有什麼問題?

   3、"父加載器"和"子加載器"之間的關系是繼承的嗎? 

  4、雙親委派是怎麼實作的? 

  5、我能不能主動破壞這種雙親委派機制?怎麼破壞? 

  6、為什麼重寫loadClass方法可以破壞雙親委派,這個方法和findClass()、defineClass()差別是什麼? 

  7、說一說你知道的雙親委派被破壞的例子吧 

  8、為什麼JNDI、JDBC等需要破壞雙親委派? 

  9、為什麼TOMCAT要破壞雙親委派? 

  10、談談你對子產品化技術的了解吧!

以上,10個問題,從頭開始答,你大概可以堅持到第幾題?

  • 什麼是雙親委派機制?

  首先,我們知道,虛拟機在加載類的過程中需要使用類加載器進行加載,而在Java中,類加載器有很多,那麼當JVM想要加載一個.class檔案的時候,到底應該由哪個類加載器加載呢?這就不得不提到"雙親委派機制"。首先,我們需要知道的是,Java語言系統中支援以下4種類加載器:

  Bootstrap ClassLoader 啟動類加載器

  Extention ClassLoader 标準擴充類加載器

  Application ClassLoader 應用類加載器

  User ClassLoader 使用者自定義類加載器

  這四種類加載器之間,是存在着一種層次關系的,如下圖

我竟然被“雙親委派”給虐了【轉】

  一般認為上一層加載器是下一層加載器的父加載器,那麼,除了BootstrapClassLoader之外,所有的加載器都是有父加載器的。

  那麼,所謂的雙親委派機制,指的就是:當一個類加載器收到了類加載的請求的時候,他不會直接去加載指定的類,而是把這個請求委托給自己的父加載器去加載。隻有父加載器無法加載這個類的時候,才會由目前這個加載器來負責類的加載。那麼,什麼情況下父加載器會無法加載某一個類呢?其實,Java中提供的這四種類型的加載器,是有各自的職責的:

  • Bootstrap ClassLoader ,主要負責加載Java核心類庫(JAVA_HOME/jre/lib/rt.jar、resources.jar等),以及加載提供JVM自身需要的類和加載System.getProperty("sun.boot.class.path")所指定的路徑下的類。
  • Extention ClassLoader,主要負責加載目錄%JRE_HOME%\lib\ext目錄下的jar包和class檔案。
  • Application ClassLoader ,主要負責加載目前應用的classpath下的所有類
  • User ClassLoader , 使用者自定義的類加載器,可加載指定路徑的class檔案

那麼也就是說,一個使用者自定義的類,如com.hollis.ClassHollis 是無論如何也不會被Bootstrap和Extention加載器加載的。

  • 為什麼需要雙親委派?

  如上面我們提到的,因為類加載器之間有嚴格的層次關系,那麼也就使得Java類也随之具備了層次關系。或者說這種層次關系是優先級。比如一個定義在java.lang包下的類,因為它被存放在rt.jar之中,是以在被加載過程彙總,會被一直委托到Bootstrap ClassLoader,最終由Bootstrap ClassLoader所加載。而一個使用者自定義的com.hollis.ClassHollis類,他也會被一直委托到Bootstrap ClassLoader,但是因為Bootstrap ClassLoader不負責加載該類,那麼會在由Extention ClassLoader嘗試加載,而Extention ClassLoader也不負責這個類的加載,最終才會被Application ClassLoader加載。

  這種機制有幾個好處:

  首先,通過委派的方式,可以避免類的重複加載,當父加載器已經加載過某一個類時,子加載器就不會再重新加載這個類。

  另外,通過雙親委派的方式,還保證了安全性。因為Bootstrap ClassLoader在加載的時候,隻會加載JAVA_HOME中的jar包裡面的類,如java.lang.Integer,那麼這個類是不會被随意替換的,除非有人跑到你的機器上, 破壞你的JDK。那麼,就可以避免有人自定義一個有破壞功能的java.lang.Integer被加載。這樣可以有效的防止核心Java API被篡改。

  • "父子加載器"之間的關系是繼承嗎?

  很多人看到父加載器、子加載器這樣的名字,就會認為Java中的類加載器之間存在着繼承關系。甚至網上很多文章也會有類似的錯誤觀點。這裡需要明确一下,雙親委派模型中,類加載器之間的父子關系一般不會以繼承(Inheritance)的關系來實作,而是都使用組合(Composition)關系來複用父加載器的代碼的。如下為ClassLoader中父加載器的定義:

public abstract class ClassLoader {

    // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;
    // ...... 
}      
  • 雙親委派是怎麼實作的?

雙親委派模型對于保證Java程式的穩定運作很重要,但它的實作并不複雜。實作雙親委派的代碼都集中在java.lang.ClassLoader的loadClass()方法之中:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
          // 如果經過父類加載器後還是為空,表明父類加載器沒有加載,則調用findClass()使用自己的加載器進行加載
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }      

findClass()的源碼在抽象ClassLoader類中并沒有實作  

protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
      

因為父類中findClass方法的實作是抛出異常,是以子類必須實作該方法

如:URLClassLoader中的findClass()源碼:

protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        String path = name.replace('.', '/').concat(".class");
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }      

代碼不難了解,主要就是以下幾個步驟:

  1、先檢查類是否已經被加載過

  2、若沒有加載且父類加載器存在則調用父加載器的loadClass()方法進行加載 

  3、若父加載器為空則預設使用啟動類加載器作為父加載器。

  4、如果父類加載失敗,抛出ClassNotFoundException異常後,再調用自己的findClass()方法進行加載。

  • 如何主動破壞雙親委派機制?

  知道了雙親委派模型的實作,那麼想要破壞雙親委派機制就很簡單了。因為他的雙親委派過程都是在loadClass方法中實作的,那麼想要破壞這種機制,那麼就自定義一個類加載器,重寫其中的loadClass方法,使其不進行雙親委派即可。

  • loadClass()、findClass()、defineClass()差別

  ClassLoader中和類加載有關的方法有很多,前面提到了loadClass,除此之外,還有findClass和defineClass等,那麼這幾個方法有什麼差別呢?

    loadClass() 就是主要進行類加載的方法,預設的雙親委派機制就實作在這個方法中。

    findClass() 根據名稱或位置加載.class位元組碼

    definclass() 把位元組碼轉化為Class

  這裡面需要展開講一下loadClass和findClass,我們前面說過,當我們想要自定義一個類加載器的時候,并且像破壞雙親委派原則時,我們會重寫loadClass方法。那麼,如果我們想定義一個類加載器,但是不想破壞雙親委派模型的時候呢?這時候,就可以繼承ClassLoader,并且重寫findClass方法。findClass()方法是JDK1.2之後的ClassLoader新添加的一個方法。

protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }      

  這個方法隻抛出了一個異常,沒有預設實作。

  JDK1.2之後已不再提倡使用者直接覆寫loadClass()方法,而是建議把自己的類加載邏輯實作到findClass()方法中。因為在loadClass()方法的邏輯裡,如果父類加載器加載失敗,則會調用自己的findClass()方法來完成加載。是以,如果你想定義一個自己的類加載器,并且要遵守雙親委派模型,那麼可以繼承ClassLoader,并且在findClass中實作你自己的加載邏輯即可。

  • 雙親委派被破壞的例子

  雙親委派機制的破壞不是什麼稀奇的事情,很多架構、容器等都會破壞這種機制來實作某些功能。

  第一種被破壞的情況是在雙親委派出現之前。由于雙親委派模型是在JDK1.2之後才被引入的,而在這之前已經有使用者自定義類加載器在用了。是以,這些是沒有遵守雙親委派原則的。

  第二種,是JNDI、JDBC等需要加載SPI接口實作類的情況。

  第三種是為了實作熱插拔熱部署工具。為了讓代碼動态生效而無需重新開機,實作方式時把子產品連同類加載器一起換掉就實作了代碼的熱替換。

  第四種時tomcat等web容器的出現。

  第五種時OSGI、Jigsaw等子產品化技術的應用。

  • 為什麼JNDI,JDBC等需要破壞雙親委派?

  我們日常開發中,大多數時候會通過API的方式調用Java提供的那些基礎類,這些基礎類時被Bootstrap加載的。但是,調用方式除了API之外,還有一種SPI的方式。如典型的JDBC服務,我們通常通過以下方式建立資料庫連接配接:

Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "1234");      

  在以上代碼執行之前,DriverManager會先被類加載器加載,因為java.sql.DriverManager類是位于rt.jar下面的 ,是以他會被根加載器加載。

  類加載時,會執行該類的靜态方法。其中有一段關鍵的代碼是ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class):

public class DriverManager {

    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
}      
private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();               
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);
        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }      
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);這段代碼,會嘗試加載classpath下面的所有實作了Driver接口的實作類。      

那麼,問題就來了。DriverManager是被根加載器加載的,那麼在加載時遇到以上代碼,會嘗試加載所有Driver的實作類,但是這些實作類基本都是第三方提供的,根據雙親委派原則,第三方的類不能被根加載器加載。

怎麼解決這個問題呢?于是,就在JDBC中通過引入ThreadContextClassLoader(線程上下文加載器,預設情況下是ApplicationClassLoader)的方式破壞了雙親委派原則。我們深入到ServiceLoader.load方法就可以看到:

public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }      

第一行,擷取目前線程的線程上下⽂類加載器 ApplicationClassLoader,⽤于加載 classpath 中的具體實作類。

  • 為什麼Tomcat要破壞雙親委派

  我們知道,Tomcat是web容器,那麼一個web容器可能需要部署多個應用程式。不同的應用程式可能會依賴同一個第三方類庫的不同版本,但是不同版本的類庫中某一個類的全路徑名可能是一樣的。如多個應用都要依賴hollis.jar,但是A應用需要依賴1.0.0版本,但是B應用需要依賴1.0.1版本。這兩個版本中都有一個類是com.hollis.Test.class。如果采用預設的雙親委派類加載機制,那麼是無法加載多個相同的類。是以,Tomcat破壞雙親委派原則,提供隔離的機制,為每個web容器單獨提供一個WebappClassLoader加載器,重寫了loadClass()方法。Tomcat的類加載機制:為了實作隔離性,優先加載 Web 應用自己定義的類,是以沒有遵照雙親委派的約定,每一個應用自己的類加載器——WebappClassLoader負責加載本身的目錄下的class檔案,加載不到時再交給CommonClassLoader加載,這和雙親委派剛好相反。

  WebappClassLoader 繼承 WebappClassLoaderBase,而 WebappClassLoaderBase又繼承于 URLClassLoader,loadClass() 方法是 WebappClassLoaderBase 類提供實作的。

  • 子產品化技術與類加載機制

  近幾年子產品化技術已經很成熟了,在JDK 9中已經應用了子產品化的技術。其實早在JDK 9之前,OSGI這種架構已經是子產品化的了,而OSGI之是以能夠實作子產品熱插拔和子產品内部可見性的精準控制都歸結于其特殊的類加載機制,加載器之間的關系不再是雙親委派模型的樹狀結構,而是發展成複雜的網狀結構。

我竟然被“雙親委派”給虐了【轉】

  在JDK中,雙親委派也不是絕對的了。在JDK9之前,JVM的基礎類以前都是在rt.jar這個包裡,這個包也是JRE運作的基石。這不僅是違反了單一職責原則,同樣程式在編譯的時候會将很多無用的類也一并打包,造成臃腫。

  在JDK9中,整個JDK都基于子產品化進行建構,以前的rt.jar, tool.jar被拆分成數十個子產品,編譯的時候隻編譯實際用到的子產品,同時各個類加載器各司其職,隻加載自己負責的子產品。

Class<?> c = findLoadedClass(cn);

    if (c == null) {

        // 找到目前類屬于哪個子產品

        LoadedModule loadedModule = findLoadedModule(cn);

        if (loadedModule != null) {

            //擷取目前子產品的類加載器

            BuiltinClassLoader loader = loadedModule.loader();

            //進行類加載

            c = findClassInModuleOrNull(loadedModule, cn);

         } else {

              // 找不到子產品資訊才會進行雙親委派

                if (parent != null) {

                  c = parent.loadClassOrNull(cn);

                }

          }

    }      

總結以上,從什麼是雙親委派,到如何實作與破壞雙親委派,又從破壞雙親委派的示例等多個方面全面介紹了關于雙親委派的知識。相信通過學習本文,你一定對雙親委派機制有了更加深刻的了解。閱讀過本文之後,反手在履歷上寫下:熟悉Java的類加載機制,不服來問!

注:文章轉自微信公衆号-Hollis,侵權删。

繼續閱讀