JVM类加载器与双亲委派模型

Posted LackMemory

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM类加载器与双亲委派模型相关的知识,希望对你有一定的参考价值。

(7)URLClassLoader类

前面说到,ClassLoader这个顶级父类只是定义好了双亲委派模型的工作机制;但是ClassLoader是个抽象类,无法直接创建对象,所以需要由继承它的子类完成创建对象的任务。子类需要自己实现findClass方法,并且在实例化时指定parent属性的值。如果parent设为null,则意味着它的父“类加载”是启动类加载器。

继承体系如下:URLClassLoader extends SecureClassLoader extends ClassLoader 其中SecureClassLoader只是做了一些安全方面的限制,而关键的业务代码并没有实现,把这些实现任务交给了子类URLClassLoader。 URLClassLoader不是一个抽象类,所以可以直接拿来创建对象,然后调用loadClass方法加载类。那么需要分析它的构造方法,一共有5个,其中三个声明为public:
public URLClassLoader(URL[] urls, ClassLoader parent) 
    super(parent);
    // this is to make the stack depth consistent with 1.1
    SecurityManager security = System.getSecurityManager();
    if (security != null) 
        security.checkCreateClassLoader();
    
    ucp = new URLClassPath(urls);
    this.acc = AccessController.getContext();
第一个需要传入的参数有包括一个URL数组和父“类加载器”ClassLoader:parent变量的意义无需多言;而第一个参数 urls用来构造私有常量ucp:
private final URLClassPath ucp;
URLClassLoader还有另外一个私有常量acc:
private final AccessControlContext acc;
它跟权限控制有关,也会频繁出现在URLClassLoader的构造方法中,暂时不用理会。 于是,这个构造方法中,除了指定父“类加载器”外,最重要的代码就是这行了:
ucp = new URLClassPath(urls);
根据传入的URL数组构造一个URLClassPath对象,这个对象用来根据class文件的路径生成一个Resource对象,其中包含了文件的二进制数据:
Resource res = ucp.getResource(path, false);
这个URLClassPath对象才是整个URLClassLoader对象进行类加载的核心,下文我们将会分析到。那么传进来的这些urls都长啥样?先看下构造方法的注释:
/**
 * Constructs a new URLClassLoader for the given URLs. The URLs will be
 * searched in the order specified for classes and resources after first
 * searching in the specified parent class loader. Any URL that ends with
 * a '/' is assumed to refer to a directory. Otherwise, the URL is assumed
 * to refer to a JAR file which will be downloaded and opened as needed.
 */
字面理解,url可以是本地文件系统的路径,如果以“/”结尾代表其是一个目录,否则默认为jar包文件。其实,这个url也可以是一个网络地址,只要下载下来的数据是个jar包就行。

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();
    
    ucp = new URLClassPath(urls, factory);
    acc = AccessController.getContext();
对比前一个,这个构造方法只是生成URLClassPath对象的方式不一样,使用了传入的URLStreamHandlerFactory对象。
public URLClassLoader(URL[] urls) 
    super();
    // this is to make the stack depth consistent with 1.1
    SecurityManager security = System.getSecurityManager();
    if (security != null) 
        security.checkCreateClassLoader();
    
    ucp = new URLClassPath(urls);
    this.acc = AccessController.getContext();
这个构造方法最省事,只需要传入一个URL数组,但是其实现是最复杂的,复杂在父“类加载器”的构造上。这个空参的super()方法,最终调用了ClassLoader的如下构造方法:
protected ClassLoader() 
    this(checkCreateClassLoader(), getSystemClassLoader());
看下它的注释:
/**
 * Creates a new class loader using the <tt>ClassLoader</tt> returned by
 * the method @link #getSystemClassLoader()
 * <tt>getSystemClassLoader()</tt> as the parent class loader.
 */
也就是说,当创建URLClassLoader对象时如果不指定parent值,那么parent的值最终由ClassLoadder.getSystemClassLoader方法决定,其代码如下:
public static ClassLoader getSystemClassLoader() 
    initSystemClassLoader();
    if (scl == null) 
        return null;
    
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) 
        checkClassLoaderPermission(scl, Reflection.getCallerClass());
    
    return scl;
显然,返回值scl会在initSystemClassLoader方法中完成初始化:
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();
            try 
                scl = AccessController.doPrivileged(
                    new SystemClassLoaderAction(scl));
             catch (PrivilegedActionException pae) 
                oops = pae.getCause();
                if (oops instanceof InvocationTargetException) 
                    oops = oops.getCause();
                
            
            if (oops != null) 
                if (oops instanceof Error) 
                    throw (Error) oops;
                 else 
                    // wrap the exception
                    throw new Error(oops);
                
            
        
        sclSet = true;
    
其核心逻辑是:1、获取一个Laucher对象;2、调用Laucher对象的getClassLoader方法,用其返回值初始化scl变量;3、使用SystemClassLoaderAction再次为scl赋值。先看一下 Laucher.getClassLoader()方法:
public ClassLoader getClassLoader() 
    return this.loader;
而loader变量的初始化在Laucher的构造方法中:
public Launcher() 
    Launcher.ExtClassLoader var1;
    try 
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
     catch (IOException var10) 
        throw new InternalError("Could not create extension class loader");
    

    try 
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
     catch (IOException var9) 
        throw new InternalError("Could not create application class loader");
    
可以看到,在Launcher的构造方法中,先后创建了两个内部类的对象Launcher.ExtClassLoader和Launcher.AppClassLoader,并且后者的parent变量就设置为前者;然后,将Launcher.AppClassLoader对象赋值给this.loader。也就是说,第二步scl的值就是一个Launcher.AppClassLoader对象。我们看下这个内部类:
static class AppClassLoader extends URLClassLoader 
是URLClassLoader的子类,其getClassLoader方法如下:
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<Launcher.AppClassLoader>() 
        public Launcher.AppClassLoader run() 
            URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
            return new Launcher.AppClassLoader(var1x, var0);
        
    );
首先,获取系统属性java.class.path的值,这个值其实就是经常设置的环境变量ClassPath的值;然后,根据根据这个系统属性的值生成一个File数组,数组包含了ClassPath指定的各个类的class文件;最后,根据File数组生成URL数组,然后把这个数组和传入的parent ClassLoader一起传入给构造方法生成一个Launcher.AppClassLoader对象。 接着看下Launcher.AppClassLoader的构造方法: 
AppClassLoader(URL[] var1, ClassLoader var2) 
    super(var1, var2, Launcher.factory);
直接调用了其父类URLClassLoader的构造方法,这个构造方法在前文已经分析过。

第三步,调用了SystemClassLoaderAction类的run方法为scl重新赋值,来看下这个类:
class SystemClassLoaderAction
    implements PrivilegedExceptionAction<ClassLoader> 
    private ClassLoader parent;

    SystemClassLoaderAction(ClassLoader parent) 
        this.parent = parent;
    

    public ClassLoader run() throws Exception 
        String cls = System.getProperty("java.system.class.loader");
        if (cls == null) 
            return parent;
        

        Constructor ctor = Class.forName(cls, true, parent)
            .getDeclaredConstructor(new Class[]  ClassLoader.class );
        ClassLoader sys = (ClassLoader) ctor.newInstance(
            new Object[]  parent );
        Thread.currentThread().setContextClassLoader(sys);
        return sys;
    
这个类和ClassLoader在同一个类文件中,但是没有生命为public,我没想明白为何这么设计(内部类不行吗?)。 这个类的作用通过下面这行代码就已经自解释了: 
String cls = System.getProperty("java.system.class.loader”);
又是一个环境变量,或者说jvm参数,当指定了 java.system.class.loader系统变量时,那么会把指定的这个类作为System Class Loader,这段代码就是为了生成一个该类的实例(利用了反射的方式),并且它的的父“类加载器”就是上一步生成的 Launcher.AppClassLoader对象。如果没有指定这个系统变量的值,那么就返回 Launcher.AppClassLoader
绕了这么大一个圈子,原来在调用 public URLClassLoader(URL[] urls)生成对象时,这个对象的父“类加载器”要么是环境变量 java.system.class.loader指定的类,要么是 URLClassLoader的子类 Launcher.AppClassLoader。
说完构造方法,也就是如何创建对象,接着该分析这个URLClassLoader类是如何去加载其他类的,也就是findClass方法是如何实现的。URLClassLoader中定义的findClass方法如下:
protected Class<?> findClass(final String name)  throws ClassNotFoundException 
    try 
        return AccessController.doPrivileged(
            new PrivilegedExceptionAction<Class>() 
                public Class run() throws ClassNotFoundException 
                    String path = name.replace('.', '/').concat(".class");
                    Resource res = ucp.getResource(path, false);
                    if (res != null) 
                        try 
                            return defineClass(name, res);
                         catch (IOException e) 
                            throw new ClassNotFoundException(name, e);
                        
                     else 
                        throw new ClassNotFoundException(name);
                    
                
            , acc);
     catch (java.security.PrivilegedActionException pae) 
        throw (ClassNotFoundException) pae.getException();
    
抛开安全检查等代码之后,核心代码如下:
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) 
    try 
        return defineClass(name, res);
     catch (IOException e) 
        throw new ClassNotFoundException(name, e);
    
 else 
    throw new ClassNotFoundException(name);
 
第一行:
String path = name.replace('.', '/').concat(".class”);
name是传入的参数,这个参数显然是loadClass方法传进来的,而loadClass的这个参数是调用时传入的,一般形式如下:
ClassLoader.loadClass(“xx.xx.Xxx”);
当loadClass没有找到时,就会把这个类传递给findClass让它去找。而这行代码显然是在把一个类名转换为其对应的class文件的相对路径名。 第二行:
Resource res = ucp.getResource(path, false);
根据方法名,可以推测是在根据path去磁盘加载这个文件,在内存中生存一个Resource对象。构造方法中初始化的ucp变量终于派上用场了:
private final URLClassPath ucp;
其类型是URLClassPath,这个变量的初始化是在构造方法中,这个后面再分析。getResource的实现也暂时略过。 如果这个res不为空,那么进入下一行关键代码:
return defineClass(name, res);
调用了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);
        sun.misc.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);
        sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
        return defineClass(name, b, 0, b.length, cs);
    
方法很长,其参数是类名和对应的文件资源(Resource对象),返回值类型是Class,这意味着,这个方法将会根据clas文件流生成一个Class对象。这个方法在逻辑上分为两个部分,第一部分用来解析、验证包名,具体细节就不再分析,我们假设包名通过了验证。然后进入到第二部分:
java.nio.ByteBuffer bb = res.getByteBuffer();
    if (bb != null) 
        // Use (direct) ByteBuffer:
        CodeSigner[] signers = res.getCodeSigners();
        CodeSource cs = new CodeSource(url, signers);
        sun.misc.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);
        sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
        return defineClass(name, b, 0, b.length, cs);
    
If-else分支中的逻辑是类似的:根据url和CodeSigner构造一个CodeSource对象,最终还都是调用了父类中的defineClass方法,传入的参数包括类名name,刚刚生成的CodeSource对象cs,以及字节流。来看下父类中的defineClass方法:
protected final Class<?> defineClass(String name, java.nio.ByteBuffer b, CodeSource cs) 
    return defineClass(name, b, getProtectionDomain(cs));
被声明为final说明不可覆盖。逻辑很简单,就是继续调用父类的defineClass方法,只是把刚刚的CodeSource对象传递给getProtectionDomain方法得到一个ProtectionDomain对象。继续跟踪父类也就是ClassLoader类的defineClass方法:
protected final Class<?> defineClass(String name, java.nio.ByteBuffer b, ProtectionDomain protectionDomain)
    throws ClassFormatError ...
对于这个方法,只需要看注释就足够了,具体逻辑有点复杂就不分析了:
Converts a @link java.nio.ByteBuffer <tt>ByteBuffer</tt> into an instance of class <tt>Class</tt>
说白了,就是把一段字节流转化为一个Class对象。那么URLClassLoader加载类的机制已经分析完毕,最核心的一行代码其实就是:
Resource res = ucp.getResource(path, false);
利用了URLClassPath类的getResource方法根据class文件的相对路径去文件系统中加载字节流。 这个方法就不在本文的讨论范围内了。

以上是关于JVM类加载器与双亲委派模型的主要内容,如果未能解决你的问题,请参考以下文章

JVM类加载机制详解类加载器与双亲委派模型

聊聊类加载器与双亲委派模型

JVM类加载器与双亲委派模型

JVM类加载器与双亲委派模型

深入理解JVM类加载器与双亲委派模型

JVM类加载器与双亲委派模型