Java类加载机制及自定义加载器

ClassLoader类加载器

ClassLoader类加载器主要的作用是将class文件加载到jvm虚拟机中。
jvm启动的时候,并不是一次性加载所有的类,而是根据需要动态去加载类,主要分为隐式加载和显示加载。

  • 隐式加载
    程序代码中不通过调用ClassLoader来加载需要的类,而是通过JVM类自动加载需要的类到内存中。例如,当我们在类中继承或者引用某个类的时候,JVM在解析当前这个类的时,发现引用的类不在内存中,那么就会自动将这些类加载到内存中。
  • 显式加载
    代码中通过Class.forName()、this.getClass.getClassLoader.LoadClass()、自定义类加载器中的findClass()方法等。

类加载的开放性

虚拟机规范并没有指明二进制字节流要从一个Class文件获取,或者说根本没有指明从哪里获取、怎样获取。这种开放使得Java在很多领域得到充分运用,例如:

  1. 从ZIP包中读取,这很常见,成为JAR,EAR,WAR格式的基础
  2. 从网络中获取,最典型的应用就是Applet
  3. 运行时计算生成,最典型的是动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流
  4. 有其他文件生成,最典型的JSP应用,由JSP文件生成对应的Class类

jvm自带的加载器

  • BootStrap ClassLoader
    主要加载%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
    可以通过System.getProperty("sun.boot.class.path")查看加载的路径。
    public static void main(String []args){
        System.out.println(System.getProperty("sun.boot.class.path"));
    }
//output
/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/sunrsasign.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/classes

  • Extention ClassLoader
    主要加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件,可以通过System.out.println(System.getProperty("java.ext.dirs"))查看加载类文件的路径。
    public static void main(String []args){
        System.out.println(System.getProperty("java.ext.dirs"));
    }
//output
/Users/PeristHuang/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java

  • AppClassLoader
    主要加载当前应用下的classpath路径下的类。我们在环境变量中配置的classpath就是指定AppClassLoader的类加载路径。

类加载器的继承关系


ExtClassLoader,AppClassLoder继承URLClassLoader,而URLClassLoader继承ClassLoader,BoopStrap ClassLoder不在上图中,因为它是由C/C++编写的,它本身是虚拟机的一部分,并不是一个java类。
jvm加载的顺序:BoopStrap ClassLoder-〉ExtClassLoader->AppClassLoder

看一段源码

public class Launcher {
    private static Launcher launcher = new Launcher();
    private static String bootClassPath =
        System.getProperty("sun.boot.class.path");
    public static Launcher getLauncher() {
        return launcher;
    }
    private ClassLoader loader;

    public Launcher() {
        // Create the extension class loader
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader", e);
        }

        // Now create the class loader to use to launch the application
        try {
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader", e);
        }
       Thread.currentThread().setContextClassLoader(loader);
    }
    /*
     * Returns the class loader used to launch the main application.
     */
    public ClassLoader getClassLoader() {
        return loader;
    }
    /*
     * The class loader used for loading installed extensions.
     */
    static class ExtClassLoader extends URLClassLoader {}
/**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */
    static class AppClassLoader extends URLClassLoader {}

我们可以看到:

  1. Launcher初始化的时候创建了ExtClassLoader以及AppClassLoader,并将ExtClassLoader实例传入到AppClassLoader中。
  2. 虽然上一段源码中没见到创建BoopStrap ClassLoader,但是程序一开始就执行了System.getProperty("sun.boot.class.path")。

类加载器之间的父子关系

AppClassLoader的父加载器为ExtClassLoader,ExtClassLoader的父加载器为null,BoopStrap ClassLoader为顶级加载器。

    public static void main(String[] args) {
        System.out.println(TestGc.class.getClassLoader().toString());
        System.out.println(TestGc.class.getClassLoader().getParent().toString());
        System.out.println(TestGc.class.getClassLoader().getParent().getParent().toString());
    }
//output
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@3ecf72fd
Exception in thread "main" java.lang.NullPointerException
	at com.perist.demo.classloader.TestGc.main(TestGc.java:10)

类加载机制-双亲委托机制

为什么需要双亲委派

基于这样的问题:如果不是同一个类加载器加载,即时是相同的class文件,也会出现判断不想同的情况,从而引发一些意想不到的情况,为了保证相同的class文件,在使用的时候,是相同的对象,jvm设计的时候,采用了双亲委派的方式来加载类。

双亲委派:如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。


当jvm要加载TestGc.class的时候过程如下:

  1. 首先会到自定义加载器中查找,看是否已经加载过,如果已经加载过,则返回字节码。
  2. 如果自定义加载器没有加载过,则询问上一层加载器(即AppClassLoader)是否已经加载过TestGc.class。
  3. 如果没有加载过,则询问上一层加载器(ExtClassLoader)是否已经加载过。
  4. 如果没有加载过,则继续询问上一层加载(BoopStrap ClassLoader)是否已经加载过。
  5. 如果BoopStrap ClassLoader依然没有加载过,则到自己指定类加载路径下("sun.boot.class.path")查看是否有TestGc.class字节码,有则返回,没有通
  6. 知下一层加载器ExtClassLoader到自己指定的类加载路径下(java.ext.dirs)查看。
  7. 依次类推,最后到自定义类加载器指定的路径还没有找到TestGc.class字节码,则抛出异常ClassNotFoundException。

需要破坏双亲委派的场景

双亲委派模型是Java设计者推荐给开发者的类加载器的实现方式,并不是强制规定的。大多数的类加载器都遵循这个模型,但是JDK中也有较大规模破坏双亲模型的情况。

在某些情况下父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器无法加载到需要的文件。
以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,比如mysql的就写了MySQL Connector,那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类,然后进行管理,但是DriverManager由启动类加载器加载,只能记载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派,这里仅仅是举了破坏双亲委派的其中一个情况。

类加载过程的几个方法

  • loadClass
  • findLoadedClass
  • findClass
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);// 首先,检查是否已经加载过
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);//父加载器不为空,调用父加载器的loadClass
                    } else {
                        c = findBootstrapClassOrNull(name);//父加载器为空则,调用Bootstrap Classloader
                    }
                } 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 = findClass(name);//父加载器没有找到,则调用findclass

                    // 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;
        }
    }

自定义类加载器

  1. 继承ClassLoader
  2. 重写findClass()方法
  3. 调用defineClass()方法

下面写一个自定义类加载器加载指定类TestGc

  • 新建TestGc类
public class TestGc {
    public void print() {
        System.out.println(System.getProperty("sun.boot.class.path"));
        System.out.println(System.getProperty("java.ext.dirs"));
        System.out.println(TestGc.class.getClassLoader().toString());
        System.out.println(TestGc.class.getClassLoader().getParent().toString());
//        System.out.println(TestGc.class.getClassLoader().getParent().getParent().toString());
    }
}
  • 自定义类加载器
public class MyClassLoader extends ClassLoader {
    private String classpath;
    public MyClassLoader(String classpath) {
        this.classpath = classpath;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] classData = getData(name);
            if (classData == null) {
            } else {
                //defineClass方法将字节码转化为类
                return defineClass(name, classData, 0, classData.length);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return super.findClass(name);
    }
    //返回类的字节码
    private byte[] getData(String className) throws IOException {
        InputStream in = null;
        ByteArrayOutputStream out = null;
        String path = classpath + File.separatorChar +
                className.replace('.', File.separatorChar) + ".class";
        try {
            in = new FileInputStream(path);
            out = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int len = 0;
            while ((len = in.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
            return out.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            in.close();
            out.close();
        }
        return null;
    }
}
  • 验证
public class TestMyClassLoader {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {
        //自定义类加载器的加载路径
        MyClassLoader myClassLoader = new MyClassLoader("/Users/PeristHuang/newgitea/javademos/normal-demos/target/classes");
        //全路径:包名+类名
        Class c = myClassLoader.loadClass("com.perist.demo.classloader.TestGc");
        if (c != null) {
            Object obj = c.newInstance();
            Method method = c.getMethod("print", null);
            method.invoke(obj, null);
            System.out.println(c.getClassLoader().toString());
        }
    }
}
//output
/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/sunrsasign.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/classes
/Users/PeristHuang/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@21a06946
sun.misc.Launcher$AppClassLoader@18b4aac2

自定义类加载器的作用:jvm自带的三个加载器只能加载指定路径下的类字节码。如果某个情况下,我们需要加载应用程序之外的类文件呢?比如本地路径或者去加载网络上的某个类文件,这种情况就可以使用自定义加载器了。

0 0 投票数
Article Rating
订阅评论
提醒
guest
0 评论
最旧
最新 最多投票
内联反馈
查看所有评论
0
希望看到您的想法,请您发表评论x