一、默认类加载器

Java虚拟机中系统默认的类加载器有三个:BootStrap,ExtClassLoader,AppClassLoader

Java类都是被类加载器加载的,ExtClassLoader,AppClassLoader也是Java类,显然它们也需要被加载,所以必然有一个类加载器不是Java类,并且由它来加载ExtClassLoader,AppClassLoader这两个Java类加载器,这个类加载器就是BootStrap,BootStrap是使用C/C++代码写的,已经封装到JVM内核中了,当Java虚拟机启动的时候这个类就会被加载,然后BootStrap会加载ExtClassLoader,并将它的parent赋值为null,表示它的父类加载器是BootStrap,接着加载AppClassLoader,并将它的父类加载器赋值为ExtClassLoader,表示它的父类加载器是ExtClassLoader。

public class ClassLoaderTest {

    public static void main(String[] args) {
        ClassLoader cl = ClassLoaderTest.class.getClassLoader();  
        while(cl != null){  
            System.out.print(cl.getClass().getName()+"->");  
            cl = cl.getParent();  
        }  
        System.out.println(cl);  

    }

}
sun.misc.Launcher$AppClassLoader->sun.misc.Launcher$ExtClassLoader->null

从上面的执行结果可以很清楚的看出它们之间的关系。null表示加载器为BootStrap,因为它不是Java类,所以获取不到这个ClassLoader,BootStrap的子加载器为ExtClassLoader,ExtClassLoader的子加载器为AppClassLoader,另外,ExtClassLoader和AppClassLoader都是sun.misc.Launcher的内部类。

为什么它有三个默认的加载器,第一个BootStrap肯定是必须的,因为Java类必须被类加载器加载,那么肯定有一个不是Java类的加载器祖先来加载Java类,ExtClassLoader和AppClassLoader都是Java类,它们被定义主要是因为Java类分为很多类别,不同的类别由不同的加载器来加载,也就是为ExtClassLoader和AppClassLoader它们有自己的管辖加载范围。

二、类加载器的委托机制

当Java虚拟机要加载第一个类的时候,到底派出哪个类加载器去加载呢?

(1). 首先当前线程的类加载器去加载线程中的第一个类(当前线程的类加载器:Thread类中有一个get/setContextClassLoader(ClassLoader cl);方法,可以获取/指定本线程中的类加载器)

(2). 如果类A中引用了类B,Java虚拟机将使用加载类A的类加载器来加载类B

(3). 还可以直接调用ClassLoader.loadClass(String className)方法来指定某个类加载器去加载某个类

每个类加载器加载类时,又先委托给其上级类加载器当所有祖宗类加载器没有加载到类,回到发起者类加载器,还加载不了,则会抛出ClassNotFoundException

下面我们来具体看看每个加载器的加载范围和委托过程。

这里写图片描述

如上图所示: MyClassLoader->AppClassLoader->Ext->ClassLoader->BootStrap.自定定义的MyClassLoader首先会先委托给AppClassLoader,AppClassLoader会委托给ExtClassLoader,ExtClassLoader会委托给BootStrap,这时候BootStrap就去加载,如果加载成功,就结束了。如果加载失败,就交给ExtClassLoader去加载,如果ExtClassLoader加载成功了,就结束了,如果加载失败就交给AppClassLoader加载,如果加载成功,就结束了,如果加载失败,就交给自定义的MyClassLoader1类加载器加载,如果加载失败,就报ClassNotFoundException异常,结束。

三,自定义类加载器和加载过程

自定义类加载器必须继承自ClassLoader类。

在ClassLoader的构造方法中可以指定parent,没有指定的话,就使用默认的parent

protected ClassLoader() 

protected ClassLoader(ClassLoader parent) 

默认的parent是使用ClassLoader.getSystemClassLoader()来获的,它实际就是AppClassLoader。

也就是说不管我们使用ClassLoader.loadClass还是我们自定义的MyClassLoader.loadClass来加载类,它默认的父加载类都是AppClassLoader。

下面我们来定义一个MyClassLoader,使用MyClassLoader.loadClass来加载指定的类,我们来看看loadClass源码,看看它是执行过程。

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

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {  
     //加上锁,同步处理,因为可能是多线程在加载类  
     synchronized (getClassLoadingLock(name)) {  
         //检查,是否该类已经加载过了,如果加载过了,就不加载了  
         Class c = findLoadedClass(name);  
         if (c == null) {  
             long t0 = System.nanoTime();  
             try {  
                 //如果自定义的类加载器的parent不为null,就调用parent的loadClass进行加载类  
                 if (parent != null) {  
                     c = parent.loadClass(name, false);  
                 } else {  
                     //如果自定义的类加载器的parent为null,就调用findBootstrapClass方法查找类,就是Bootstrap类加载器  
                     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();  
                 //如果parent加载类失败,就调用自己的findClass方法进行类加载  
                 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;  
    }  
} 

可以看到,如果parent加载类失败,就调用自己的findClass方法进行类加载,下面我们来看看findClass方法。

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

可以看到它就抛出了一个异常,如果我们希望使用这个类来进行加载,这个方法需要我们进行重写,它的作用就是查找指定位置的class文件,用来进行加载。

找个这个class文件之后,就需要对这个文件进行加载了,使用的是defineClass方法,返回类型为Class。它是一个native方法,不需要重写。

从上面可以看到,这三个方法的执行流程是:loadClass->findClass->defineClass,我们只需要重写findClass方法来查找指定位置的class文件,其他两个方法不需要重写。

下面我们来举个例子:

public class TestDemo {
    public static void print() {
        System.out.println("Hello World");
    }
}

它是被加载的类,它的class文件位于目录:/Users/Documents/workspace/ClassLoaderTest/bin下面。

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;

public class MyClassLoader extends ClassLoader {
    //需要加载类.class文件的目录  
    private String classDir; 

    //无参的构造方法,用于class.newInstance()构造对象使用  
    public MyClassLoader(){  
    }  

    public MyClassLoader(String classDir){  
        this.classDir = classDir;  
    }  

    protected Class<?> findClass(String name) throws ClassNotFoundException {  
        //class文件的路径  
        String classPathFile = classDir + "/" + name + ".class"; 
        try {   
            FileInputStream fis = new FileInputStream(classPathFile);  
            ByteArrayOutputStream bos = new ByteArrayOutputStream();   
            byte[] classByte = bos.toByteArray();  
            //将字节流变成一个class  
            return defineClass(classByte,0,classByte.length);  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        return super.findClass(name); 

    }
}

这个是我们自定义的ClassLoader,用来加载TestDemo类。

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ClassLoaderTest {

    public static void main(String[] args) {
        MyClassLoader classLoader = new MyClassLoader("/Users/Documents/workspace/ClassLoaderTest/bin");
        try {
            Class testDemo = classLoader.loadClass("TestDemo");
            Method method = testDemo.getMethod("print", null);
            method.invoke(null, null);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }   
    }

}

这个类中,我们使用自定义的ClassLoader来加载TestDemo类,并且执行print方法。

三、使用ClassLoader的原因

先说说java中import的作用,import中所引用的类文件有两个特点:
(1)必须存在于本地,当程序运行时如果需要某个类,内部类装载器会自动装载该类。
(2)编译时必须存在,否则会因为找不到引用文件而不能正常编译。

但是有时,我们所需要的类不能满足上面两种条件,例如我们需要动态加载某个类,这个类是从远程服务器上下载下来的,它不参与编译,而是运行时动态调用的,这是我们就需要使用ClassLoader来动态的进行加载。

import是静态导入之后,就可以直接使用某个类,ClassLoader是动态加载某个类之后,然后就可以对该类进行实例化并且使用该类。

一般情况,应用程序不需要创建一个全新的ClassLoader对象,而是使用当前环境已经存在的ClassLoader,因为Java的Runtime环境初始化时,其内部会创建一个ClassLoader对象用来加载Runtime所需的各种Java类。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐