1.ClassLoader 做什么的?

ClassLoader是用来加载Class文件。他负责将Class的字节码形式转换成内存形式的Class对象。Class的字节码文件可以来自磁盘文件*.class,也可以是jar包里面的*.class,或者使网络上的字节流。其加载的本质是byte[ ]形式的字节数组。

java ClassLoader读不到jar包里面的配置文件 classloader加载jar包_ClassLoader


类加载器主要有两种,一种是jvm自带的类加载器,另一种用户继承了抽象类java.lang.ClassLoader 来自定义加载器。通常自定义加载器作用除了加载某些特性的文件夹,或者对class文件进行解密后在加载进内存中。

每个Class对象内部都有一个classLoader字段来标识自己是由哪个加载器加载的。

public final class Class<T>{
	//  ....
	// Initialized in JVM not by private constructor
    // This field is filtered from reflection access, i.e. getDeclaredField
    // will throw NoSuchFieldException
    private final ClassLoader classLoader;
}

2.延迟加载

jvm运行不是一次性的把类都加载到内存中,他是按需加载,也就是延迟加载。程序在运行的过程中,会逐步遇到很多不认识的新类,这时候就会调用ClassLoader来加载这些类。加载完成后,就会将 Class 对象存在 ClassLoader 里面,下次就不需要重新加载了。

比如你在调用某个类的静态方法时,首先这个类肯定是需要被加载的,但是并不会触及这个类的实例字段,那么实例字段的类别 Class 就可以暂时不必去加载,但是它可能会加载静态字段相关的类别,因为静态方法会访问静态字段。而实例字段的类别需要等到你实例化对象的时候才可能会加载。

3.类加载器

jvm提供了3个类加载器:

  • 启动(BootStrap)类加载器:负责加载<Java_Home>/lib下面的核心类库,或者-Xbootclasspath选项指定的jar包,主要是 JVM 运行时核心类,这些类位于 JAVA_HOME/lib/rt.jar 文件中,我们常用内置库 java.xxx.* 都在里面,比如 java.util.*、java.io.*、java.nio.*、java.lang.* 等等。这个类加载器是内嵌与jvm中的机器码,由c++完成,启动类加载器不存在java对象,用null表示。
  • 扩展(Extension)类加载器:负责加载<Java_Home>/lib/ext或者由系统变量-Djava.ext.dir指定的位置。加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 JAVA_HOME/lib/ext/*.jar 中
  • 系统(System)类加载器:负责加载系统类路径-classpath或-Djava.class.path变量所指的目录下的类库。程序可以访问并使用系统类加载器。

那些位于网络上静态文件服务器提供的 jar 包和 class文件,jdk 内置了一个 URLClassLoader,用户只需要传递规范的网络路径给构造器,就可以使用 URLClassLoader 来加载远程类库了。URLClassLoader 不但可以加载远程类库,还可以加载本地路径的类库,取决于构造器中不同的地址形式。ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子类,它们都是从本地文件系统里加载类库。

4.双亲委派类加载机制

java ClassLoader读不到jar包里面的配置文件 classloader加载jar包_ClassLoader_02


双亲委派类加载机制是当某个类需要被加载时,当前类加载器,委派给父类加载器,如果父类加载器还有父类加载器,则继续向上委托,从最上的父类去查找该类,尝试加载,如果加载不到,则子类加载,直到最初的类加载器,如果任然查找不到,则会抛出classnotFoundException。注意,他们之间不是继承继承关系,是一种组合关系,子加载器中含有父类加载器的引用。

双亲委托机制实现原理(递归加载类)

一个将类class文件形式,加载到内存class表现的流程

public static void main(String[] args) throws ClassNotFoundException {
      //获取系统默认的类加载器(SystemClassLoader)
      ClassLoader classLoader = ClassLoader.getSystemClassLoader();
      //输入class的限定名
      Class<?> clazz = classLoader.loadClass("bean.Person");
    }

loadClass()是抽象类ClassLoader中的类加载的核心方法.

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) {
                    	//递归调用,parent中还含有父加载器,实现了双亲委托机制
                        c = parent.loadClass(name, false);
                    } else {//父加载器为null,表示是根加载器,直接调用根加载器本地方法加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    //吃掉父加载器加载不到的异常,如果父加载器加载不到
                }

                if (c == null) {                              
                    long t1 = System.nanoTime();
                    //如果c任然为空,表示父加载加载不到,在自己去加载  
        // findClass找到class文件后将调用defineClass方法把字节码导入方法区,同时缓存结果 
                    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();
                }
            }
            // 是否解析,默认false
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

可以看到双亲委托机制其实就是下面代码,通过递归调用父加载,去尝试加载类,加载不到,吃掉异常,继续让子加载器去加载。

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
           }
1. 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类 
2. 类加载后将进入连接(link)阶段,它包含验证、准备、解析,resolve参数决定是否执行解析阶段,
   jvm规范并没有严格指定该阶段的执行时刻 
3. 由于先使用findLoadedClass()查找缓存,相同的类只会被加载一次

5.用户自定义类加载器

用户通过继承ClassLoader类,重写里面的findClass()方法,自定义加载类的class文件,即可完成
自定义类加载器的操作。通常,构造方法可以指定父加载,默认的加载器是系统类加载器。
// The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;
    //默认使用系统类加载器
	protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }
    
 	protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    }

当实现自定义类加载器时不应重写loadClass(),除非你不需要双亲委派机制。要重写的是findClass()的逻辑,也就是寻找并加载类的方式。

使用自定义类加载器获取到的Class对象需通过newInstance()获取实例,要比较具有相同类全限定名的两个Class对象是否是同一个,取决于是否是同一类加载器加载了它们,也就是调用defineClass()的那个类加载器,而非之前委派的类加载器。

5.1 常用方法分析
1. java.lang.Class
  • Class<?> forName(……) 这是手动加载类的常见方式,在Class类中有两个重载
//使用调用者的加载器去加载文件
public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }
//类名,是否初始化,指定类加载器加载
public static Class<?> forName(String name, boolean initialize, ClassLoader loader)
2.java.lang.ClassLoader
  • ClassLoader getParent();//获取parent加载器
  • Class loadClass(String);//加载类,传入类的限定名
  • URL getResource(String); 获取具有给定名称的资源定位符。资源可以是任何数据,名称须以“/”分离路径名。实际调用findResource()方法,该方法无实现,需子类继承实现。
  • InputStream getResourceAsStream(String); 获取可以读取资源的InputStream输入流,实际上就是用上面的方法获取到URL后调用url.openStream()得到 InputStream。
5.2 在自定义类加载器之前先去分析下ExtClassLoa和SystemClassLoader的实现

这两个类是sun.misc.Launcher的内部类,两者都继承了Classloader,关系图如下:

java ClassLoader读不到jar包里面的配置文件 classloader加载jar包_类加载器_03

Launcher.AppClassLoader
/**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */
    static class AppClassLoader extends URLClassLoader {

        static {
            ClassLoader.registerAsParallelCapable();
        }
		//在Launcher类初始化的时候,会调用这个方法,获取AppClassLoader
		//extcl是扩展类加载器,先加载完扩展类加载器,作为AppClassLoader的父加载器
        public static ClassLoader getAppClassLoader(final ClassLoader extcl)
                throws IOException
        {
            final String s = System.getProperty("java.class.path");
            //通过系统环境变量,获取Classpath路径
            final File[] path = (s == null) ? new File[0] : getClassPath(s);

            // Note: on bugid 4256530
            // Prior implementations of this doPrivileged() block supplied
            // a rather restrictive ACC via a call to the private method
            // AppClassLoader.getContext(). This proved overly restrictive
            // when loading  classes. Specifically it prevent
            // accessClassInPackage.sun.* grants from being honored.
            //
            return AccessController.doPrivileged(
                    new PrivilegedAction<AppClassLoader>() {
                        public AppClassLoader run() {
                        //将获取的路径,封装成URL,创建对象。
                            URL[] urls =
                                    (s == null) ? new URL[0] : pathToURLs(path);
                            return new AppClassLoader(urls, extcl);
                        }
                    });
        }

        /*
         * Creates a new AppClassLoader
         * 创建一个新的类加载器,通过调用父类构造方法
         */
        AppClassLoader(URL[] urls, ClassLoader parent) {
            super(urls, parent, factory);
        }

        /**
         * 系统类加载器,重写了loadClass方法,也是调用父类(URLClassLoader )的方法
         */
        public Class<?> loadClass(String name, boolean resolve)
                throws ClassNotFoundException
        {
            int i = name.lastIndexOf('.');
            if (i != -1) {
                SecurityManager sm = System.getSecurityManager();
                if (sm != null) {
                    sm.checkPackageAccess(name.substring(0, i));
                }
            }
            //调用父类的构造方法
            return (super.loadClass(name, resolve));
        }
       
    }

通过AppClassLoader发现,主要实现在URLClassLoader中,包括创建系统类加载器实例和loadClass(),findClass()的实现。
ClassLoader只是一个抽象类,很多方法是空的需要自己去实现,比如 findClass()、findResource()等。而java提供了java.net.URLClassLoader这个实现类,适用于多种应用场景。

该类加载器用于从一组URL路径(指向JAR包或目录)中加载类和资源。约定使用以 ‘/’结束的URL来表示目录。
如果不是以该字符结束,则认为该URL指向一个JAR文件。

之前提到的AppClassLoader、ExtClassLoader都是URLClassLoader的子类,自定义类加载器推荐直接继承它。

URLClassLoader

使用URL[] getURLs()方法可以获取URL路径,参考代码:

public static void main(String[] args) {
	//这个方法作用是根据加载器的不同,去表示的不同的文件路径
    URL[] urls = ((URLClassLoader)ClassLoader.getSystemClassLoader()).getURLs();
    for (URL url : urls) {
        System.out.println(url);
    }
}
// file:/D:/Workbench/Test/bin/

加载方式:
findClass()中其使用了URLClassPath类中的Loader类来加载类文件和资源。URLClassPath类中定义了两个Loader类的实现,分别是FileLoader和JarLoader类,顾名思义前者用于加载目录中的类和资源,后者是加载jar包中的类和资源。Loader类默认已经实现getResource()方法,即从网络URL地址加载jar包然后使用JarLoader完成后续加载,而两个实现类不过是重写了该方法。

那URLClassPath是如何选择使用正确的Loader的呢?答案是——根据URL格式而定。下面是删减过的核心代码,简单易懂。

private Loader getLoader(final URL url)
{
    String s = url.getFile();
    // 以"/"结尾时,若url协议为"file"则使用FileLoader加载本地文件
    // 否则使用默认的Loader加载网络url
    if(s != null && s.endsWith("/"))
    {
        if("file".equals(url.getProtocol()))
            return new FileLoader(url);
        else
            return new Loader(url);
    } else {
        // 非"/"结尾则使用JarLoader
        return new JarLoader(url, jarHandler, lmap);
    }
}
/* The search path for classes and resources */
    private final URLClassPath ucp;
	 
	 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,负责搜索加载路径下的资源,不同加载器会创建不同的
        ucp = new URLClassPath(urls, factory, acc);
    }
	
	//实现了父类尚未实现的方法,根据name,去加载路径下查找资源
	protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        String path = name.replace('.', '/').concat(".class");
                        //用ucp查找加载路径下的资源,由于这个URLClassLoader是两个类加载器的基类,
                        //所以ucp对于不同的参数,会有不同的实现。
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                            	//找到了,就加载类
                                return defineClass(name, res);
                            } catch (IOException e) {
                            	//name不在加载路径下
                                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;
    }

上述是讲解关于系统类加载器是如何被创建出来了,已经实现的机制。通过上面源码,分析了双亲委托机制是如何实现了(递归调用父加载的loadClass(String name)方法)。除了根类加载器,其余的类加载器全部都是继承java.;lang.Classloader的子类,为便于实现,提供了URLClassLoader类的支持,这个类中实现了一些常用的方法,比如URL findResource(final String name);负责查找资源。因为URLClassLoader要负责加载每个加载器的加载目录,所以,该类为每个加载器提供了一个URLClassPath对象,这个对象负责类加载的加载路径的维护和资源加载,而URLClassLoader则是重写findClass()方法,通过资源加载器的返回结果,判断是否资源存在。