三次打破雙親載入機制原始碼分析

曹自標發表於2020-11-27

雙親載入機制

以下兩張圖足夠說明(jdk1.8)雙親載入機制。

注意的是AppClassLoader和ExtClassLoader都是sun.misc.Launcher的內部類,同樣都繼承URLClassLoader,parent是ClassLoader中欄位,當初始化AppClassLoader時,它的parent就被設定ExtClassLoader,而ExtClassLoader的parent則被設定為null,使用者自定義的則被設定AppClassLoader。

圖片來自宋紅康視訊截圖

圖片來自宋紅康視訊

private final ClassLoader parent;

 @CallerSensitive
 public final ClassLoader getParent() {
     if (parent == null)
         return null;
     SecurityManager sm = System.getSecurityManager();
     if (sm != null) {
         // Check access to the parent class loader
         // If the caller's class loader is same as this class loader,
         // permission check is performed.
         checkClassLoaderPermission(parent, Reflection.getCallerClass());
     }
     return parent;
 }

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

第一次打破雙親載入機制

為何出現的原由?

雙親載入模型是在JDK1.2之後才被引入,自然為向前相容,沒有把loadClass方法設定為不可覆蓋的方法,而是新增加了findClass方法,讓使用者儘量重寫此方法。

換個方向理解,其實就是可以覆蓋loadClass方法,進行直接載入一些類。如下面例子,自定義載入器繼承ClassLoader,在loadClass方法中,刪除雙親載入邏輯,後面增加一個判斷,如果不是自己的包下檔案,讓父類去載入。而測試類,直接使用自定義載入器去loadClass時,返回的類的載入器是此自定義載入器。

一個例子:

public class FirstClassLoader extends ClassLoader{
    private String classPath;

    public FirstClassLoader(String classPath) {
        this.classPath = classPath;
    }

    private byte[] getByte(String name) throws Exception{
        name = name.replaceAll("\\.", "/");
        FileInputStream fileInputStream = new FileInputStream(classPath + "/" + name + ".class");
        int len = fileInputStream.available();
        byte [] data = new byte[len];
        fileInputStream.read(data);
        fileInputStream.close();
        return data;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try{
            byte [] data = getByte(name);
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

    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();

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    // 除了刪除上面雙親載入機制地方,要加這個判斷,不然會Object等一些類無法載入
                    if(!name.startsWith("com.java.study")) {
                        return this.getParent().loadClass(name);
                    }
                    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;
        }
    }
}

測試類:

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception{
    	// 傳入你要load的class路徑
        FirstClassLoader firstClassLoader = new FirstClassLoader("xxxx");
        Class clazz = firstClassLoader.loadClass("com.java.study.StudyApplication");
        System.out.println(clazz.getClassLoader());
    }
}

第二次打破雙親載入機制

第二次打破雙親載入機制,出現在JNDI服務中。瞭解前可以先熟悉SPI(Service Provider Interface)。(參考:https://www.zhihu.com/question/49667892)
在這裡插入圖片描述
對應JDBC例子就是,呼叫方是rt.jar包下DriverManager,而具體Driver實現方在三方jar包下(如mysql-connector-java.jar),Bootstrap Class Loader載入的類在載入過程中要使用Application Class Loader才能載入的類,在雙親載入模型是不能做到的。所以這裡出現打破雙親載入機制。具體分析如下:

先看獲取連線寫法:

 Connection connection = DriverManager.getConnection("jdbc:mysql://192.168.130.1:3306/test", "root", "password");

再分析DriverManager類,其中靜態程式碼塊在類載入過程中進行初始化呼叫loadInitialDrivers(),接下來呼叫ServiceLoader#load(), 此方法中引入Thread.currentThread().getContextClassLoader(),即Application Class Loader,是具體打破雙親載入機制的實現。後面載入Driver具體實現類,便可用此Loader

public class DriverManager {
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
  
    private static void loadInitialDrivers() {
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
				ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
				......
			}
		}
	}
}            
public final class ServiceLoader<S> implements Iterable<S> {
    public static <S> ServiceLoader<S> load(Class<S> service) {
    	 // 打破雙親載入機制具體實現
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    } 
}

在Thread類中可以看到,初始化時會從父執行緒中繼承一個,如果在應用程式的全域性範圍內都沒有設定過的話,這個類載入器預設就是Application Class Loader。

public class Thread implements Runnable {
	private ClassLoader contextClassLoader;
	
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        Thread parent = currentThread();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
    }
    
    public void setContextClassLoader(ClassLoader cl) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("setContextClassLoader"));
        }
        contextClassLoader = cl;
    }

    public ClassLoader getContextClassLoader() {
        if (contextClassLoader == null)
            return null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ClassLoader.checkClassLoaderPermission(contextClassLoader,
                                                   Reflection.getCallerClass());
        }
        return contextClassLoader;
    }
}

解下來重要方法一,ServiceLoader#hasNextService() 中會去掃描jar包下META-INF/services/java.sql.Driver檔案,再通過parse(service, configs.nextElement())方法去獲取Driver具體實現類com.mysql.cj.jdbc.Driver等。

 private boolean hasNextService() {
     if (nextName != null) {
         return true;
     }
     if (configs == null) {
         try {
             String fullName = PREFIX + service.getName();
             if (loader == null)
                 configs = ClassLoader.getSystemResources(fullName);
             else
             	 // 載入jar包下META-INF/services/java.sql.Driver檔案
                 configs = loader.getResources(fullName);
         } catch (IOException x) {
             fail(service, "Error locating configuration files", x);
         }
     }
     while ((pending == null) || !pending.hasNext()) {
         if (!configs.hasMoreElements()) {
             return false;
         }
         // 獲取具體實現類
         pending = parse(service, configs.nextElement());
     }
     nextName = pending.next();
     return true;
 }

重要方法二,ServiceLoader#nextService()先傳入全路徑名和Loader獲取com.mysql.cj.jdbc.Driver未初始化的例項物件,再在c.newInstance()中進行初始化

private S nextService() {
     if (!hasNextService())
         throw new NoSuchElementException();
     String cn = nextName;
     nextName = null;
     Class<?> c = null;
     try {
         // 獲取com.mysql.cj.jdbc.Driver
         c = Class.forName(cn, false, loader);
     } catch (ClassNotFoundException x) {
         fail(service,
              "Provider " + cn + " not found");
     }
     if (!service.isAssignableFrom(c)) {
         fail(service,
              "Provider " + cn  + " not a subtype");
     }
     try {
         S p = service.cast(c.newInstance());
         providers.put(cn, p);
         return p;
     } catch (Throwable x) {
         fail(service,
              "Provider " + cn + " could not be instantiated",
              x);
     }
     throw new Error();          // This cannot happen
 }

其中com.mysql.cj.jdbc.Driver原始碼中可看到,類載入時會往DriverManager中註冊Driver

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
        	// 往DriverManager中註冊Driver
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

最後到具體DriverManager#getConnection()方法。由於在載入com.mysql.cj.jdbc.Driver時已經設定classLoader為Application class Loader,此時callerCL不為null。直接走下面遍歷註冊的Drivers,獲取連線

private static Connection getConnection(
    String url, java.util.Properties info, Class<?> caller) throws SQLException {
    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    synchronized(DriverManager.class) {
        // synchronize loading of the correct classloader.
        if (callerCL == null) {
            callerCL = Thread.currentThread().getContextClassLoader();
        }
    }

    for(DriverInfo aDriver : registeredDrivers) {
        // If the caller does not have permission to load the driver then
        // skip it.
        if(isDriverAllowed(aDriver.driver, callerCL)) {
            try {
                Connection con = aDriver.driver.connect(url, info);
            } catch (SQLException ex) {
               ......
            }
        } 
    }
}

以上第二次打破雙親載入機制就全部分析完,主要實現便是引入Thread.currentThread().getContextClassLoader()。

第三次打破雙親載入機制

第三次打破雙親載入機制由於使用者對程式動態性追求而導致的,如熱部署。主要可研究OSGi原理。

先把OSGi類搜尋順序摘錄(《深入理解java虛擬機器》):

  1. 將以java.*開頭的類委派給父類載入器載入
  2. 否則,將委派列表名單內的類,委派給父類載入器載入
  3. 否則,將Import列表中的類,委派給Export這個類的Bundle的類載入器載入
  4. 否則,查詢當前Bundle的ClassPath使用主機的類載入器載入
  5. 否則,查詢類是否在自己的Fragment Bundle中,如果在,則委派給Fragment Bundle的類載入器載入
  6. 否則,查詢Dynamic Import列表的Bundle,委派給對應Bundle的類載入器載入
  7. 否則,類查詢失敗

另補充JDK9後對應原始碼,ClassLoader中一些改變。

新增BuiltinClassLoader;ExtClassLoader替換為PlatformClassLoader。新增BootClassLoader。AppClassLoader、PlatformClassLoader、BootClassLoader三者都繼承BuiltinClassLoader。如下圖:
在這裡插入圖片描述

在AppClassLoader中先委派父類載入器進行載入,查詢要載入的包是否在module中:
不在,委派parent去載入;
在,並和當前ClassLoader相同,使用PlatformClassLoader去module中載入;
在,但和當前ClassLoader不同,使用其他載入器載入

    private static class AppClassLoader extends BuiltinClassLoader {
            @Override
        protected Class<?> loadClass(String cn, boolean resolve) throws ClassNotFoundException{
                    return super.loadClass(cn, resolve);
        }
  }
public class BuiltinClassLoader extends SecureClassLoader {
    @Override
    protected Class<?> loadClass(String cn, boolean resolve) throws ClassNotFoundException
    {
        Class<?> c = loadClassOrNull(cn, resolve);
        if (c == null)
            throw new ClassNotFoundException(cn);
        return c;
    }
}
public class BuiltinClassLoader extends SecureClassLoader {
    protected Class<?> loadClassOrNull(String cn, boolean resolve) {
        synchronized (getClassLoadingLock(cn)) {
            // check if already loaded
            Class<?> c = findLoadedClass(cn);

            if (c == null) {

                // find the candidate module for this class
                LoadedModule loadedModule = findLoadedModule(cn);
                if (loadedModule != null) {

                    // package is in a module
                    BuiltinClassLoader loader = loadedModule.loader();
                    if (loader == this) {
                        if (VM.isModuleSystemInited()) {
                            c = findClassInModuleOrNull(loadedModule, cn);
                        }
                    } else {
                        // delegate to the other loader
                        c = loader.loadClassOrNull(cn);
                    }

                } else {

                    // check parent
                    if (parent != null) {
                        c = parent.loadClassOrNull(cn);
                    }

                    // check class path
                    if (c == null && hasClassPath() && VM.isModuleSystemInited()) {
                        c = findClassOnClassPathOrNull(cn);
                    }
                }

            }

            if (resolve && c != null)
                resolveClass(c);

            return c;
        }
    }
}

參考:

《深入理解java虛擬機器》
https://www.bilibili.com/video/BV1RK4y1a7NF?p=6
https://www.jianshu.com/p/09f73af48a98
https://www.cnblogs.com/jay-wu/p/11590571.html
https://www.jianshu.com/p/78f5e2103048
https://www.cnblogs.com/lyc88/articles/11431383.html
https://www.cnblogs.com/huxuhong/p/11856786.html
https://www.zhihu.com/question/49667892
https://www.jianshu.com/p/5dc10732de6a
https://www.jianshu.com/p/a18aecaecc89

相關文章