JVM類加載機制以及破壞

一、類加載機制

  1. 啓動類加載器(Bootstrap ClassLoader):這個類加載器負責加載 JAVA_HOME/lib 目錄。
  2. 擴展類加載器(Extension ClassLoader):這個類加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載JAVA_HOME/lib/ext 目錄或者 java.ext.dirs 系統變量所指定的路徑中的類庫。開發者可以直接使用擴展類加載器。
  3. 應用類加載器(Application ClassLoader):這個類加載器由sun.misc.Launcher$AppClassLoader 實現。由於這個類加載器是ClassLoader 中 getSystemClassLoader方法的返回值,所以也稱爲系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫。這個是程序中默認的類加載器。

雙親委派模型:如果一個類加載器收到了類加載的請求,他首先不會自己去嘗試加載這個類,而是把這個請求委派父類加載器去完成。每一個層次的類加載器都是如此向上委託,因此所有的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父加載器自己無法加載時,子加載器纔會嘗試自己去加載。說雙親委派模型實在存在誤導性,因爲他是一個爸爸的爸爸叫爺爺的過程,是單親向上委託。並且,父類加載器無法使用由子類加載器加載的類(因爲父類加載器不會向下委託給子類加載器),相反,子類加載器可以使用父類加載器加載的類。

爲什麼要這麼做?

1. 保證類的唯一性(如果不是向上委託,則可能出現多個由不同類加載器加載的相同名稱的類)。

一個類在同一個類加載器中具有唯一性(Uniqueness),而不同類加載器中是允許同名類存在的,這裏的同名是指全限定名相同
但是在整個JVM裏,縱然全限定名相同,若類加載器不同,則仍然不算作是同一個類,無法通過 instanceOf 、equals 等方式的校驗。

2. 保證安全,防止類被篡改。

 

代碼:java.lang.ClassLoader#loadClass(java.lang.String, boolean)

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 先檢查類是否已經加載
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 交給父類加載
                    c = parent.loadClass(name, false);
                } else {
                    // 返回一個被啓動類加載器加載的類,如果未找到,則返回null
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父類無法完成加載,拋出 ClassNotFoundException
            }
            if (c == null) {
                // 如果依然沒找到(父類加載器未找到),則調用 findClass 來尋找
                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. Java SPI

一個典型的例子是MySQL驅動類的加載。

標準的接口由官方指定,不同廠商可以有自己的實現。下面是MySQL的驅動:

其中調用了 java.sql.DriverManager ,這個DriverManager裏面有個靜態代碼塊:

這個就是利用SPI來加載驅動的。

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;
    }
    // 如果驅動被打包成Service Provider,則加載它。
    // 利用爲java.sql.Driver.class服務的類加載器,加載所有驅動程序

    // java.security.AccessController提供了一個默認的安全策略執行機制,它使用棧檢查來決定潛在不安全的操作是否被允許。
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {

            // SPI的使用
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            /* Load these drivers, so that they can be instantiated.
             * It may be the case that the driver class may not be there
             * i.e. there may be a packaged driver with the service class
             * as implementation of java.sql.Driver but the actual class
             * may be missing. In that case a java.util.ServiceConfigurationError
             * will be thrown at runtime by the VM trying to locate
             * and load the service.
             *
             * Adding a try catch block to catch those runtime errors
             * if driver not available in classpath but it's
             * packaged as service and that service is there in classpath.
             */
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
            // Do nothing
            }
            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);
        }
    }
}

SPI的load方法:

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 這裏用的是線程上下文類加載器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

觀察得知:

此處用到了兩個類加載器:

1. ServiceLoader.load(Driver.class) --線程上下文類加載器

2. Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()) -- 系統類加載器

線程上下文類加載器是一種獨立的加載器嗎?

最開始也說過,除了自定義的類加載器,JVM中一共有三種類加載器。所以這個加載器應該是個持有者的身份,結合驅動類是放在ClassPath下的,它所持有的肯定不是啓動類加載器,因爲啓動類加載器不能加載ClassPath目錄下的類;那麼剩下的兩個類加載器,它是寫在 sun.misc.Launcher 裏的:

public class Launcher {
    ...

    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            // 擴展類加載器
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            // 系統類加載器
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
        // 將系統類加載器設置爲線程上下文類加載器
        Thread.currentThread().setContextClassLoader(this.loader);
        String var2 = System.getProperty("java.security.manager");
        if (var2 != null) {
            SecurityManager var3 = null;
            if (!"".equals(var2) && !"default".equals(var2)) {
                try {
                    var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
                } catch (IllegalAccessException var5) {
                } catch (InstantiationException var6) {
                } catch (ClassNotFoundException var7) {
                } catch (ClassCastException var8) {
                }
            } else {
                var3 = new SecurityManager();
            }

            if (var3 == null) {
                throw new InternalError("Could not create SecurityManager: " + var2);
            }

            System.setSecurityManager(var3);
        }

    }
    ...
}

那麼:

1. ServiceLoader.load(Driver.class) --線程上下文類加載器(默認系統類加載器)

2. Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()) -- 系統類加載器

問題一:ServiceLoader.load 裏也調用了Class.forName方法,爲什麼出來了又要調用一次?我覺得區別在於load裏調用的是Class.forName(cn, false, loader),false-只加載類,不初始化類。

問題二:根據雙親委派的可見性原則,啓動類加載器() 加載的 DriverManager 不可能拿到 系統類加載器() 加載的實現類。系統類加載器來加載驅動實現類又怎麼樣?之所以想不通,是因爲忽略了一個點:父類加載器能夠使用由線程上下文加載器加載的實現類。這就改變了父類加載器不能使用子類加載器或是其他沒有直接父子關係的類加載器所加載的類的情況,即破壞了雙親委派模型。破壞雙親委派模型的關鍵不是重寫loadClass,而是引入了線程上下文類加載器。

 

2. Tomcat

Tomcat也是破壞雙親委派模型的例子。對於一些未加載的非核心類庫,各個web應用優先用自己的類加載器(WebAppClassLoader)加載,加載不到再交給commonClassLoader走雙親委託。

熱部署:JSP文件修改了之後是不需要重啓的,當JSP發生修改之後,只需要卸載掉這個JSP的類加載器,然後重新創建類加載器,重新加載這個JSP文件即可。

隔離:一個web容器可能需要部署兩個應用程序A和B,A和B可能會依賴同一個第三方類庫的不同版本,比如mysql-connector-java.5.X和mysql-connector-java.8.X,不能只加載一個版本,因此要保證每個應用程序的類庫都是獨立的,保證相互隔離。每個應用都有自己的類加載器,類加載器之間是不可見的,起到了隔離效果。一個類的全限定名和加載該類的加載器二者共同形成了這個類在JVM中的唯一標識

 

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