目录

  • 前言
  • 一、双亲委派机制
  • 二、浅析类加载器
  • 1.类加载器的关系
  • 2.方法分析
  • 总结
  • 后记
  • 补充



前言

最近在学习类加载机制,看了一些JDK源码,记录一下自己的认识



一、双亲委派机制

老生常谈了属于是,加载器会先让他的父加载器先进行加载,如果父加载器抛出ClassNotFound,自己再进行加载。

康康源码>>

ClassLoader类的loadClass源码,它会调用loadClass(String, Boolean)方法

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

ClassLoader类的loadClass(String, Boolean)方法,是一个protected方法
英文注释是源码里的

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            //findLoadedClass这个方法是查缓存的
            Class<?> c = findLoadedClass(name);
            //如果缓存有就判断是否要解析然后返回了,如果缓存没有
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                	//如果父加载器不是bootstrap,则让父加载器执行loadClass方法
                    if (parent != null) {
                    	//父加载器的loadClass会递归丢给他的父加载器
                        c = parent.loadClass(name, false);
                    } else {
                    	//如果父加载器是bootstrap,则执行下面这个方法,这个方法会调用findBootstrapClass(String)方法,这是个native方法
                        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仍然是null说明父加载器加载不了)
                    //那么调用自己的findClass(String)方法,在ClassLoader类里findClass是个空方法,需要自己在子类重写
                    c = findClass(name);
					//下面三行我也不知道在做什么,不过估计跟类加载没关系
                    // this is the defining class loader; record the stats
                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
            }
            //如果resolve为true,会对这个类进行解析
            //但是调loadClass(String)方法传到这的resolve是false
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }


至此我们就明白了双亲委派模型的原理

findClass里是自己的加载逻辑,loadClass方法的作用就是丢给父类。如果loadClass成功了就返回加载的类,如果loadClass失败了就调findClass方法,这就是双亲委派的:每个儿子都很懒,有活先给父亲做,父亲做不了自己再做。

顺便也明白了为什么官方不推荐重写loadClass方法,而建议重写findClass方法:如果把加载逻辑写在loadClass方法里面,那loadClass方法就不会向上请求父加载器加载了,就破坏了双亲委派

二、浅析类加载器

1.类加载器的关系

双亲委派模型分三层,这都是老生常谈了
顶层的bootstrap启动类加载器
第二层的扩展类加载器(JDK9之后好像改名叫platform平台类加载器?)
第三层的AppClassLoader系统类加载器

但是分析他们的方法,要看继承关系

//最顶层是ClassLoader类
public abstract class ClassLoader {...}
//第二层是SecureClassLoader类
public class SecureClassLoader extends ClassLoader {...}
//第三层是URLClassLoader类和BuiltinClassLoader类
public class URLClassLoader extends SecureClassLoader implements Closeable {..}
//第四层是AppClassLoader类和PlatformClassLoader类,他俩继承自BuiltinClassLoader类

//App和Platform是java.base下的jdk.internal.loader包下的ClassLoaders类里的内部类,继承BulitinClassLoader类。这是JDK11,JDK8之前他俩好像在什么sun.misc.Launcher类里。

2.方法分析


ClassLoader类:有loadClass方法,findClass为空,有四个defineClass方法

SecureClassLoader类:重写了两个defineClass方法

URLClassLoader类:重写了findClass方法,重写了一个defineClass方法


首先看看URLClassLoader类的findClass方法

protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
        	//这个内部类我是没看懂
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<>() {
                    public Class<?> run() throws ClassNotFoundException {
                    	//把包名的.替换为路径符号/,然后在最后加上.class
                        String path = name.replace('.', '/').concat(".class");
                        //ucp是这个类的一个final修饰的URLClassPath变量
                        //去看了看URLClassPath类的源码也没看懂,总之就是返回了一个Resource的实例
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                            	//调用URLClassLoader类自己重写的defineClass方法,稍后分析
                                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;
    }

可以看到findClass虽然一顿操作,但是核心还是调用defineClass方法,接下来看一下URLClassLoader类重写的defineClass方法

private Class<?> defineClass(String name, Resource res) throws IOException {
        long t0 = System.nanoTime();
        int i = name.lastIndexOf('.');
        URL url = res.getCodeSourceURL();
        if (i != -1) {
            String pkgname = name.substring(0, i);
            // Check if package already loaded.
            Manifest man = res.getManifest();
            if (getAndVerifyPackage(pkgname, man, url) == null) {
                try {
                    if (man != null) {
                        definePackage(pkgname, man, url);
                    } else {
                        definePackage(pkgname, null, null, null, null, null, null, null);
                    }
                } catch (IllegalArgumentException iae) {
                    // parallel-capable class loaders: re-verify in case of a
                    // race condition
                    if (getAndVerifyPackage(pkgname, man, url) == null) {
                        // Should never happen
                        throw new AssertionError("Cannot find package " +
                                                 pkgname);
                    }
                }
            }
        }
        //又是一顿操作,前面的估计在找包吧,没细看
        // Now read the class bytes and define the class
        java.nio.ByteBuffer bb = res.getByteBuffer();
        if (bb != null) {
            // Use (direct) ByteBuffer:
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
            return defineClass(name, bb, cs);
        } else {
            byte[] b = res.getBytes();
            // must read certificates AFTER reading bytes.
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
            return defineClass(name, b, 0, b.length, cs);
        }
    }

总之URLClassLoader类的defineClass最终也是调用父类SecureClassLoader的defineClass方法,接着我们再康康SecureClassLoader类的defineClass方法

protected final Class<?> defineClass(String name,
                                         byte[] b, int off, int len,
                                         CodeSource cs)
    {
        return defineClass(name, b, off, len, getProtectionDomain(cs));
    }
    
    protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
                                         CodeSource cs)
    {
        return defineClass(name, b, getProtectionDomain(cs));
    }

没什么意思,其实也就是根据URLClassLoader传来的参数,调用了getProtectionDomain(CodeSource)这个方法之后,把返回值作为参数,调用了父类ClassLoader的defineClass方法
那么我们再康康ClassLoader类的defineClass方法,ClassLoader类的define方法有好几个,这里随便选了其中一个

protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                         ProtectionDomain protectionDomain)
        throws ClassFormatError
    {
        protectionDomain = preDefineClass(name, protectionDomain);
        String source = defineClassSourceLocation(protectionDomain);
        Class<?> c = defineClass1(this, name, b, off, len, protectionDomain, source);
        postDefineClass(c, protectionDomain);
        return c;
    }

更没意思了,虽然又是一顿操作,但是核心还是调用了defineClass1这个native方法,ClassLoader类还有defineClass2和defineClass0这两个本地方法。
至此,我们大概就知道类加载的逻辑是怎么回事了,URLClassLoader类的findClass方法经过一顿操作,最终调用了native方法defineClass0或1,2


接下来是AppClassLoader类的loadClass方法(他也未重写findClass)

@Override
        protected Class<?> loadClass(String cn, boolean resolve)
            throws ClassNotFoundException
        {
            // for compatibility reasons, say where restricted package list has
            // been updated to list API packages in the unnamed module.
            SecurityManager sm = System.getSecurityManager();
            if (sm != null) {
                int i = cn.lastIndexOf('.');
                if (i != -1) {
                    sm.checkPackageAccess(cn.substring(0, i));
                }
            }

            return super.loadClass(cn, resolve);
        }

App没有交给他的父加载器去加载,而是尝试调用父类(BuiltinClassLoader)的loadClass方法
BuiltinClassLoader的loadClass方法如下:

@Override
    protected Class<?> loadClass(String cn, boolean resolve)
        throws ClassNotFoundException
    {
        Class<?> c = loadClassOrNull(cn, resolve);
        if (c == null)
            throw new ClassNotFoundException(cn);
        return c;
    }

调用了loadClassOrNull方法(先要判断一个loadedModule然后里面一堆逻辑),然后如果parent!=null的话,这个方法会请求parent调用loadClassOrNull方法。看起来像是双亲委派,但是我没搞懂这是在做什么:AppClassLoader的parent是PlatformClassLoader(以前好像叫ExtClassLoader?),这个Platform他没有loadClassOrNull方法,而且这里是父关系不是父类,parent没有就是没有了,不会继续向上查找有这个方法的父类。
如果加载仍然失败,BuiltinClassLoader会调用findClassOnClassPathOrNull(String)这个方法,这个方法最终会调用defineClass方法,这样就完成了加载。



总结

分析了源码之后,对于类加载的5个过程也有了一定的认识,类加载的五个过程:加载、验证、准备、解析、初始化
加载是调用loadClass之类的方法、然后验证大概是那什么SecureClassLoader负责的,准备这一步我也不清楚、然后解析就是调用resolveClass函数,初始化这个只有Class.forName会完成,defineClass0或1或2不做初始化(大概)
本文之所以说是浅析,一是因为我个人才疏学浅,二是因为分析到最后,发现怎么什么都是调用的本地方法啊,这玩意又分析不了,所以最后也只是知道“奥,他调了native方法”,只称得上浅析。



后记

补充

在继承URLClassLoader类的MyLoader类里,我重写了findClass方法和loadClass方法,findClass方法的内容仅仅是调用super.findClass,但是我偶然间发现,当我仅调用findClass时,loadClass也会被调用。
我感觉非常的迷惑和不解,因为我没看到有任何地方会去调用Myloader类的loader方法,但是实际上他就是被执行了,为什么呢?

String className = Thread.currentThread().getStackTrace()[2].getClassName();//调用的类名
String methodName = Thread.currentThread().getStackTrace()[2].getMethodName();//调用的方法名
int lineNumber = Thread.currentThread().getStackTrace()[2].getLineNumber();//调用的行数
System.out.println(className+" "+methodName+" "+lineNumber);

通过这几句查看调用栈的代码,我发现在defineClass1和forName0等本地方法调用了MyLoader类的loadClass方法,但是至于具体代码,我就无从分析了。