Java_JVM_Java的雙親委派模型 與 破壞雙親委派模型實例

 

參考文章:

1.雙親委派模型的破壞(JDBC例子)

https://blog.csdn.net/awake_lqh/article/details/106171219

2.面試官:說說雙親委派模型?

https://baijiahao.baidu.com/s?id=1633056679004596814&wfr=spider&for=pc

3.【JVM】淺談雙親委派和破壞雙親委派

https://www.cnblogs.com/joemsu/p/9310226.html

 

   在我們的面試過程中,免不了會被問到 JVM 相關的知識。其中雙親委派模型 就是一個較爲經常被考察的點。下面對這個點做一個整理。

 

類的生命週期

  類從加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括, 加載,驗證,準備,解析,初始化,使用 和 卸載 7個步奏。

 

類的生命週期

 加載 -》 驗證 -》準備 -》解析 -》初始化 -》使用  -》卸載

 

以下5個階段的順序是確定的

 加載 -》驗證 -》準備 -》 初始化 -》卸載

 

類加載的全過程 5個階段

 加載 -》 驗證 -》準備 -》解析 -》初始化 

其中 驗證-》準備-》解析 又被統稱爲連接,所以 類加載又可以稱爲 加載,連接,初始化 3個階段。

 

驗證

驗證階段主要完成以下4個階段的檢驗動作

1.文件格式驗證

2.元數據驗證

3.字節碼驗證

4.符號引用驗證

 

準備

 準備階段正式爲類變量分配內存,並設置類變量的初始值的階段,這些變量所使用的內存都將是在方法去中進行分配的。

Tips:

 1.這時候進行內存分配的僅包括類變量(被 static 修飾的變量)

 2.這裏說的初始值 通常情況 下是 數據類型的零值,例如 public static int value = 123;  準備後的初始值爲0,而不是123

 

初始化的5種情況

   有且只有 以下5種情況, 必須立即對類進行 “初始化”。(而加載,驗證,準備自然需要在此次之前開始)

1.遇到 new, getstatic , putstatic 或 invokestatic 這 4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。

2.使用 java.lang.reflect 包的方法對類 進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。

3.當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。

4.當虛擬機啓動時,用戶需要指定一個要執行的主類 (包含 main() 方法的哪個類),虛擬機會先初始化這個主類。

5.當使用 JDK 1.7 的動態語言支持時,如果一個 Java.lang.invoke.MethodHandle 實例最後的解析結果 REF_getStatic , REF_putStatic, REF_invoke 的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。

對 HotSpot 虛擬機,可以通過 -XX:+TraceClassLoading 參數觀察此操作是否導致子類的加載。

 

 

雙親委派模型

 

類加載器引申的問題

     在Java中任意一個類都是由 這個類本身加載這個類的類加載器來確定 這個類在JVM中的唯一性。也就是你用你A類加載器加載的com.aa.ClassA 和 你B 類加載器加載的com.aa.ClassA它們是不同的,也就是用instanceof這種對比都是不同的。所以即使都來自於同一個class文件但是由不同類加載器加載的那就是兩個獨立的類。

 

Java 的類加載器

  Java 提供了 3種類加載器 , 啓動類加載器,擴展類加載器 和 應用程序類加載器

   啓動類加載器 (Bootstrap ClassLoader ):  它是屬於虛擬機自身的一部分,用C++實現的。 這個類加載器類 負責將存放在 <JAVA_HOME>\lib 目錄中的,或者被 -Xbootclasspath 參數指定的路徑中的,並且是虛擬機識別的(僅按照文件名識別,如 rt.jar 名字不符合的類庫 即使放到lib 目錄中也不會被加載)類庫加載到虛擬機內存中。

 

  擴展類加載器(Extension ClassLoader): Java 實現的,獨立於虛擬機,主要負責加載<JAVA_HOME>\lib\ext目錄中或被java.ext.dirs系統變量所指定的路徑的類庫。開發者可以直接使用擴展類加載器。

 

   應用程序類加載器(Application ClassLoader)它是Java實現的,獨立於虛擬機。主要負責加載用戶類路徑(classPath)上的類庫,如果我們沒有實現自定義的類加載器,  那 它 Application ClassLoader  就是我們程序中的默認加載器。

 

 

類加載器的層次模型

那麼有那麼多的類加載器,它們之前的層級關係是怎樣的呢。它們之間是使用的雙親委派模型,如下圖

 

 

  如果一個類加載器收到了加載某個類的請求,則該類加載器並不會去加載該類,而是把這個請求委派給父類加載器,每一個層次的類加載器都是如此,因此所有的類加載請求最終都會傳送到頂端的啓動類加載器;只有當父類加載器在其搜索範圍內無法找到所需的類,並將該結果反饋給子類加載器,子類加載器會嘗試去自己加載。

這裏有幾個流程要注意一下:

  1. 子類先委託父類加載
  2. 父類加載器有自己的加載範圍,範圍內沒有找到,則不加載,並返回給子類
  3. 子類在收到父類無法加載的時候,纔會自己去加載

 

雙親委派的實現

 雙親委派對保證Java 程序運行的穩定性很重要,實現卻很簡單,實現代碼都在 java.lang.ClassLoader 的 loadClass() 方法中

/**
     * Loads the class with the specified <a href="#name">binary name</a>.  The
     * default implementation of this method searches for classes in the
     * following order:
     *
     * <ol>
     *
     *   <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
     *   has already been loaded.  </p></li>
     *
     *   <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
     *   on the parent class loader.  If the parent is <tt>null</tt> the class
     *   loader built-in to the virtual machine is used, instead.  </p></li>
     *
     *   <li><p> Invoke the {@link #findClass(String)} method to find the
     *   class.  </p></li>
     *
     * </ol>
     *
     * <p> If the class was found using the above steps, and the
     * <tt>resolve</tt> flag is true, this method will then invoke the {@link
     * #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
     *
     * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
     * #findClass(String)}, rather than this method.  </p>
     *
     * <p> Unless overridden, this method synchronizes on the result of
     * {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
     * during the entire class loading process.
     *
     * @param  name
     *         The <a href="#name">binary name</a> of the class
     *
     * @param  resolve
     *         If <tt>true</tt> then resolve the class
     *
     * @return  The resulting <tt>Class</tt> object
     *
     * @throws  ClassNotFoundException
     *          If the class could not be found
     */
    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
                }

                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;
        }
    }

 

雙親委派的一些思考

  1.Object 類複寫,還是加載到 系統類的Object, 就是由雙親委派模型保證的。

 

 

 

破壞雙親委派模型

   雙親委派模型有一系列的優勢,還是需要去破壞雙親委派模型。比如 :基礎類去調用回用戶的代碼。

SPI 破壞雙親委派

  具體的例子:以Driver 接口爲例,由於Driver 接口定義在JDK 當中,其實現由各個數據庫的服務商來提供,比如 mysql 的就寫了 MySQL Connector , 那麼爲題來了,DriverManager(也由JDK 提供)要加載各個實現了Driver接口的實現類,然後進行管理,但是DriverManager由啓動類加載器加載,只能記載JAVA_HOME的lib下文件,而其實現是由服務商提供的,由系統類加載器加載,這個時候就需要啓動類加載器來委託子類來加載Driver實現,

  這裏是通過 :線程上下文類加載器 Thread Context ClassLoader 實現的。這個類加載器可以通過 java.lang.Thread 類的 setContextClassLoaser() 方法進行設置,如果 創建線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局範圍內都沒有進行設置過的話,那這個類加載器默認就是應用程序類加載器。

  Java 中涉及SPI 的加載動作都採用這種方式,例如JNDI, JDBC,JCE,JAXB,JBI 等

OSGi 熱部署類加載機制 破壞雙親委派

    雙親委派第三次被破壞是基於程序動態性導致的,如代碼熱替換(HotSwap), 模塊熱部署 (Hot Deployment)

    目前OSGi 已經成了業界 事實上的 Java 模塊化標準。其實先模塊化熱部署的關鍵是自定的類加載機制。每一個程序模塊 (OSGi 稱爲 Bundle)都有一個自己的類加載器,當需要更換一個Bundle 時,就把 Bundle 連同類加載器一起替換掉以實現代碼的熱替換。

 

JDBC 破壞雙親委派的例子

package thread.classLoad;

import java.sql.Connection;

/**
 * Created by szh on 2020/6/15.
 */
public class TestJDBC {

    public static void main(String[] args) throws Exception{
        String url = "jdbc:mysql://localhost:3306/testdb";
        Connection conn = java.sql.DriverManager.getConnection(url, "root", "root");

    }

}

追蹤 getConnection 方法

    @CallerSensitive
    public static Connection getConnection(String url,
        String user, String password) throws SQLException {
        java.util.Properties info = new java.util.Properties();

        if (user != null) {
            info.put("user", user);
        }
        if (password != null) {
            info.put("password", password);
        }

        return (getConnection(url, info, Reflection.getCallerClass()));
    }

追蹤 getConnection 方法

 //  Worker method called by the public getConnection() methods.
    private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        /*
         * When callerCl is null, we should check the application's
         * (which is invoking this class indirectly)
         * classloader, so that the JDBC driver class outside rt.jar
         * can be loaded from here.
         */
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            // synchronize loading of the correct classloader.
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }

        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }

        println("DriverManager.getConnection(\"" + url + "\")");

        // Walk through the loaded registeredDrivers attempting to make a connection.
        // Remember the first exception that gets raised so we can reraise it.
        SQLException reason = null;

        for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    Connection con = aDriver.driver.connect(url, info);
                    if (con != null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }

            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }

        }

        // if we got here nobody could connect.
        if (reason != null)    {
            println("getConnection failed: " + reason);
            throw reason;
        }

        println("getConnection: no suitable driver found for "+ url);
        throw new SQLException("No suitable driver found for "+ url, "08001");
    }

 

 

獲取線程上下爲類加載器

callerCL = Thread.currentThread().getContextClassLoader();

isDriverAllowed對於mysql連接jar進行加載

isDriverAllowed(aDriver.driver, callerCL))

  isDriverAllowed將傳入的Thread.currentThread().getContextClassLoader();拿到的應用類加載器用去Class.forName加載我們mysql連接jar,這樣子就可以加載到我們自己的mtsql連接jar

    private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
        boolean result = false;
        if(driver != null) {
            Class<?> aClass = null;
            try {
                aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
            } catch (Exception ex) {
                result = false;
            }

             result = ( aClass == driver.getClass() ) ? true : false;
        }

        return result;
    }


爲什麼必須要破壞?

    DriverManager::getConnection 方法需要根據參數傳進來的 url 從所有已經加載過的 Drivers 裏找到一個合適的 Driver 實現類去連接數據庫.
    Driver 實現類在第三方 jar 裏, 要用 AppClassLoader 加載. 而 DriverManager 是 rt.jar 裏的類, 被 BootstrapClassLoader 加載, DriverManager 沒法用 BootstrapClassLoader 去加載 Driver 實現類(不再lib下), 所以只能破壞雙親委派模型, 用它下級的 AppClassLoader 去加載 Driver.
 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章