1. ホーム
  2. java

Javaクラスローダーにソースコードから潜り込む

2022-02-10 14:51:55
<パス

JVMクラスローダー

JVMは、主に以下のクラスローダーを持っています。

  1. ブートストラップクラスローダ
    主に、JREのlibディレクトリにあるJVMランタイムコアクラスライブラリ(rt.jarのクラスなど)をロードします。
  2. 拡張クラスローダー
    主にJREのextディレクトリにあるJVMから拡張クラスをロードします。
  3. アプリケーションクラスローダ
    主に、ClassPath パス下のクラス(ビジネスクラス)のロードを担当します。
  4. カスタムローダー
    ユーザが定義したパスでクラスをロードする役割を果たします。

クラスローダー関係

ソースコードのパース

ExtClassLoaderとAppClassLoaderの作成フロー

まず、Launcherのコンストラクタから見ていきましょう。

public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
        	//getExtClassLoader
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }
        
        try {
        	//get the application class loader, this.loader is the default class loader: i.e. AppClassLoader
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
        // Set the default classLoader
        Thread.currentThread().setContextClassLoader(this.loader);
        String var2 = System.getProperty("java.security.manager");
    }


ExtClassLoader

ExtClassLoaderの取得メソッドgetExtClassloader()を見てみましょう。
ExtClassLoaderは、URLClassLoaderを継承したLauncherの内部クラスであることが分かります。

public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
			//Get the class file to be loaded
            final File[] var0 = getExtDirs();

            try {
                return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction
Look at the getExtDirs() method: you can see that the class files to be loaded are all located in the ext folder.
private static File[] getExtDirs() {
            String var0 = System.getProperty("java.ext.dirs");
            File[] var1;
            if (var0 ! = null) {
                StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
                int var3 = var2.countTokens();
                var1 = new File[var3];

                for(int var4 = 0; var4 < var3; ++var4) {
                    var1[var4] = new File(var2.nextToken());
                }
            } else {
                var1 = new File[0];
            }

            return var1;
        }

Continuing with the ExtClassLoader constructor.
  public ExtClassLoader(File[] var1) throws IOException {
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }

The constructor method of the parent class is called.

You can see that the parent assignment of ExtClassLoader is null, because the bootstrap class loader is written in C++ and has no actual java objects. public URLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) { super(parent); // this is to make the stack depth consistent with 1.1 SecurityManager security = System.getSecurityManager(); if (security ! = null) { security.checkCreateClassLoader(); } acc = AccessController.getContext(); ucp = new URLClassPath(urls, factory, acc); } So an ExtClassLoader is created. AppClassLoader AppClassLoader also inherits from the URLClassLoader class
Look at the getAppClassLoader method.
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
            final String var1 = System.getProperty("java.class.path");
            final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
            return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction

As you can see, getAppClassLoader mainly loads the class files under the project classPath.

Moving on to the getAppClassLoader constructor method. AppClassLoader(URL[] var1, ClassLoader var2) { super(var1, var2, Launcher.factory); this.ucp.initLookupCache(this); } You can see from the Launcher constructor at the beginning that the parameter var2 is the extClassLoader that was initialized first.
The parent URLClassLoader constructor is also called, setting the extClassLoader as the parent, so the appClassLoader's parent is the extClassLoader. So the relationship between the three main class loaders is clear, as is the scope of what each will load. Let's look at the implementation of a custom class loader. Custom class loader To inherit from the ClassLoader method, the custom class loader just needs to override the findClass method: the package classload; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; /** * @author zhw * @description * @date 2021-07-15 14:36 */ public class MyClassLoader extends ClassLoader{ @Override protected Class findClass(String name) throws ClassNotFoundException { File file = new File("C:/Users/hiwei/Desktop/hiwei/test/Person.class"); try{ byte[] bytes = getClassBytes(file); //the defineClass method converts a file consisting of binary stream bytes into a java.lang. Class c = defineClass(name, bytes, 0, bytes.length); return c; } catch (Exception e) { e.printStackTrace(); } return super.findClass(name); } private byte[] getClassBytes(File file) throws Exception { FileInputStream inputStream = new FileInputStream(file);// original input stream ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len; while ((len = inputStream.read(buffer)) ! = -1 ) { baos.write(buffer, 0, len); } baos.flush(); return baos.toByteArray(); } } For more information on who the parent of a custom class loader is, check out. protected ClassLoader() { this(checkCreateClassLoader(), getSystemClassLoader()); } Continue with getSystemClassLoader(): public static ClassLoader getSystemClassLoader() { initSystemClassLoader(); if (scl == null) { return null; } SecurityManager sm = System.getSecurityManager(); if (sm ! = null) { checkClassLoaderPermission(scl, Reflection.getCallerClass()); } return scl; } private static synchronized void initSystemClassLoader() { if (!sclSet) { if (scl ! = null) throw new IllegalStateException("recursive invocation"); sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); if (l ! = null) { Throwable oops = null; scl = l.getClassLoader(); } sclSet = true; } } public ClassLoader getClassLoader() { return this.loader; } The default parent of a custom class loader is the AppClassLoader. Two-parent delegation The first call in the class loading process is to the Launcher.loader.loadClass() method. public Launcher() { Launcher.ExtClassLoader var1; try { //getExtClassLoader var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } try { //get the application class loader, this.loader is the default class loader: i.e. AppClassLoader this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } // Set the default classLoader Thread.currentThread().setContextClassLoader(this.loader); String var2 = System.getProperty("java.security.manager"); } The loader is the AppClassLoader. so go ahead and look at the AppClassLoader.loadClass method. public Class loadClass(String var1, boolean var2) throws ClassNotFoundException { int var3 = var1.lastIndexOf(46); if (var3 ! = -1) { SecurityManager var4 = System.getSecurityManager(); if (var4 ! = null) { var4.checkPackageAccess(var1.substring(0, var3)); } } if (this.ucp.knownToNotExist(var1)) { Class var5 = this.findLoadedClass(var1); if (var5 ! = null) { if (var2) { this.resolveClass(var5); } return var5; } else { throw new ClassNotFoundException(var1); } } else { // Call the parent class's loadClass method return super.loadClass(var1, var2); } } Moving on to super.loadClass(var1, var2): here comes the core code for the two-parent delegation mechanism protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException throws synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded // First, check if the class has already been loaded, if so return the Class c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { //If parent loader is not null, hand over to parent loader to load. if (parent ! = null) { c = parent.loadClass(name, false); } else { //If parent loader is null, hand over to bootstrap class loader to load. c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } // If the parent loader does not load the changed class, load it yourself if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); // Load it yourself 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; } } After looking at the above code, doesn't it seem like a simple implementation of the two-parent delegation mechanism?

The role of biparental delegation. Sandbox security, ensuring that JVM core code is not overwritten by user-defined classes. Guaranteed uniqueness of class loading. How do you break a two-parent commission? Looking at the source code of the two-parent delegation mechanism, you can see that the main implementation is in the loadClass method, so all you need to do is override the loadClass(String name, boolean resolve) method to package classload; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; /** * @author zhw * @description * @date 2021-07-15 14:36 */ public class MyClassLoader extends ClassLoader{ @Override protected Class findClass(String name) throws ClassNotFoundException { File file = new File("C:/Users/hiwei/Desktop/hiwei/test/Person.class"); try{ byte[] bytes = getClassBytes(file); //the defineClass method converts a file consisting of binary stream bytes into a java.lang. Class c = defineClass(name, bytes, 0, bytes.length); return c; } catch (Exception e) { e.printStackTrace(); } return super.findClass(name); } protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException throws synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); //Remove the two-parent delegation logic /*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 }*/ // Add your own logic // if it is a class you want to load do not load it for the parent loader, the rest still go through the bi-parental delegation mechanism if("hiwei.test.Person".equals(name)){ c = findClass(name); }else{ c = getParent().loadClass(name); } 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; } } private byte[] getClassBytes(File file) throws Exception { FileInputStream inputStream = new FileInputStream(file);// original input stream ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len; while ((len = inputStream.read(buffer)) ! = -1 ) { baos.write(buffer, 0, len); } baos.flush(); return baos.toByteArray(); } } Test class. package classload; /** * @author zhw * @description * @date 2021-07-15 15:09 */ public class ClassLoadTest { public static void main(String[] args) throws Exception { MyClassLoader myClassLoader = new MyClassLoader(); Class clazz = Class.forName("hiwei.test.Person", true, myClassLoader); Object o = clazz.newInstance(); System.out.println(o.toString()); System.out.println(clazz.getClassLoader()); } } Testing.
Person.class exists in both the target folder and classPath Test one.
Result: Loaded using a custom loader.
Test 2: Do not override the loadClass method.
Result: Use AppClassLoader
Disrupting biparental delegated applications tomcat breaking double parent delegation
Different applications in tomcat may rely on different versions of the same jar, and sharing a class loader would result in the inability to isolate the environment. So tomcat customizes the class loader so that each application has its own class loader that is responsible for loading classes under its own application, breaking the two-parent delegation mechanism and not letting the parent loader load first. Source code analysis The class loader for tomcat core classes is initialized in tomcat's Bootstrap.initClassLoaders() method at private void initClassLoaders() { try { commonLoader = createClassLoader("common", null); if( commonLoader == null ) { // no config file, default to this loader - we might be in a 'single' env. commonLoader = this.getClass().getClassLoader(); } catalinaLoader = createClassLoader("server", commonLoader); sharedLoader = createClassLoader("shared", commonLoader); } catch (Throwable t) { handleThrowable(t); log.error("Class loader creation threw exception", t); System.exit(1); } } These three class loaders do not break the bi-parental delegation model; all three are instances of URLClassLoader.
What really breaks the bi-parental delegation model is the WebappClassLoader class loader, which inherits from WebappClassLoaderBase, which overrides the loadClass method to. @Override //todo This breaks the two-parent delegation model public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { if (log.isDebugEnabled()) log.debug("loadClass(" + name + ", " + resolve + ")"); Class clazz = null; // Log access to stopped class loader checkStateForClassLoading(name); // (0) Check our previously loaded local class cache clazz = findLoadedClass0(name); if (clazz ! = null) { if (log.isDiscovery(name)) if (log.isDebugEnabled()) log.debug(" Returning class from cache"); if (resolve) resolveClass(clazz); return clazz; } // omit ,,,, } As you can see, the rewritten loadClass method breaks the two-parent delegation model. JDBC breaks bi-parental delegation The Driver driver itself in native JDBC is just an interface with no concrete implementation, and the concrete implementation is left to different database types to implement. For example, the Driver class in MySQL's jar is specifically implemented.
As an example, take the following versions. mysql mysql-connector-java 8.0.25 Driver implementation class. public class Driver extends NonRegisteringDriver implements java.sql. static { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } } As you can see, the DriverManager class is used. In the DriverManager class there are static code blocks. static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } Continue with loadInitialDrivers() private static void loadInitialDrivers() { String drivers; try { drivers = AccessController.doPrivileged(new PrivilegedAction () { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } AccessController.doPrivileged(new PrivilegedAction () { public Void run() { ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class); Iterator driversIterator = loadedDrivers.iterator(); 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); } } } See the following methods. ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class); public static ServiceLoader load(Class service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } The classLoader of the current thread is used. private ServiceLoader(Class svc, ClassLoader cl) { service = Objects.requireNonNull(svc, "Service interface cannot be null"); loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; acc = (System.getSecurityManager() ! = null) ? AccessController.getContext() : null; reload(); } Returning to the loadInitialDrivers() method, continue on to the following. AccessController.doPrivileged(new PrivilegedAction () { public Void run() { ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class); //load Driver.class Iterator driversIterator = loadedDrivers.iterator(); try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); Enter loadedDrivers.iterator(): public Iterator iterator() { return new Iterator () { Iterator You can see that an anonymous Iterator class that overrides the hasNext() and next() methods is returned. try{ while(driversIterator.hasNext()) { driversIterator.next(); } } All the calls here are overridden methods.
From the call relationship, you end up with the following methods. private boolean hasNextService() { if (nextName ! = null) { return true; } if (configs == null) { try { String fullName = PREFIX + service.getName(); if (loader == null) // find Driver.calss configs = ClassLoader.getSystemResources(fullName); else 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; } private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class c = null; try { //load Driver.calss 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 } As you can see, Driver.class is fetched in hasNextService() and loaded in nextService() at c = Class.forName(cn, false, loader); The class loader loader here is the above ClassLoader cl = Thread.currentThread().getContextClassLoader(); Now the truth is clear, when using the spi mechanism, the Driver.class under "META-INF/services/" is loaded using the current thread's class loader.
Under the two-parent delegation model, class loading is delegated from the bottom up, and jdk cannot load class files under other folders. But in jdbc, Driver has to be implemented by the provider, so it needs to be loaded. This is solved by using the thread context class loader to load the Driver.class file under the specified path in the spi usage method.
The JDBC implementation that breaks bi-parental delegation uses the parent loader to load the class file under the specified path.
private static File[] getExtDirs() { String var0 = System.getProperty("java.ext.dirs"); File[] var1; if (var0 ! = null) { StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator); int var3 = var2.countTokens(); var1 = new File[var3]; for(int var4 = 0; var4 < var3; ++var4) { var1[var4] = new File(var2.nextToken()); } } else { var1 = new File[0]; } return var1; }