JVM(六):探究类加载过程-下

定义

前面说过加载阶段是一个可以让设计人员高度自控的模块,因为类文件的源头可以是多种多样的,代码生成、反射生成或从网络中生成等。因此类加载器作为对这些文件的处理就显得尤为重要。

但类加载器的功能不仅如此,其还有一个重要的功能就是和一个类的全限定名唯一确定一个类。通俗来说,要说两个类是相同的,不仅其全限定名要一样,其对应的类加载器也必须相同,才能说明两个类是相等的。

正因为类加载器的功能角色如此重要,因此虚拟机对其的实现规范也十分重视。在Java虚拟机中,对其的实现模型是双亲委派模型

模型

JVM(六):探究类加载过程-下-LMLPHP

双亲委派模型的主要执行过程示意图如上所示,其分为启动类加载器(Bootstrap Class-loader),拓展类加载器(Extension Class-loader),应用程序类加载(Application Class-loader)。

其中启动类加载器主要负责加载 JRE 的核心类库,如 JRE 目录下的 rt.jar。但其实根据《深入分析 Java Web 技术内幕》上所说,启动类加载器并不严格符合双亲委派模型,因为Bootstrap Class-loader 并不属于 JVM 的类等级层次。Bootstrap Class-loader 是没有子类的,Extension Class-loader 也是没有父类的。不过在这里我们并不深究,只要知道有这一点就可以了。

Extension Class-loader 主要负责加载 JRE 拓展目录 ext 下的类。

Application Class-loader 主要负责用户类路径(Class-path)下的类,这个类加载器是使用的最多的,因为大大多数情况下,一般开发者并没有实现自定义的类加载器,那么 JVM 就会使用这个来加载类大部分类。

执行过程

JVM(六):探究类加载过程-下-LMLPHP

上图就是双亲委派模型的执行过程,当类开始加载的时候,先检查是否已经被加载过,如果没有被加载过,则调用父类的加载方法,如果父类加载失败,抛出异常,则调用自身的 findClass() 方法进行加载。

JDK 中加载过程的源码分析:

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);
                } else {
                    // 无父亲就调用 bootstarp 加载器来加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
        // 父加载器和 bootstarp 加载器都没有找到指定类,调用当前类的 findClass() 来完成类加载
        // 因此,自定义类加载器,就是重写 findClass() 方法
        if (c == null) {
            // If still not found, then invoke findClass in order
            // to find the class.
            long t1 = System.nanoTime();
            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;
}
}

从源码中,我们可以看到其实符合规范要求的 双亲委派模型 的。而当我们要自定义一个类加载器的时候就是通过重写 findClass() 来实现的。

自定义类加载器

/**
 * 1. 自定义类加载器通过集成ClassLoader来实现,主要通过重写findClass方法
 * 2. findClass方法首先通过自定义的loadByte()方法将Class文件转换成byte[]字节流
 * 3. 然后通过defineClass()方法将其转换为Class对象
 */
public class SelfClassLoader extends ClassLoader {
    private String classPath;
    public SelfClassLoader(String classPath) {
        this.classPath = classPath;
    }

    /**
     * 通过 difineClass,将一个字节数组转换为Class 对象
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = loadByte(name);
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }

    /**
     * 根据路径将指定的文件读取为byte 流
     * @param name
     * @return
     * @throws IOException
     */
    private byte[] loadByte(String name) throws IOException {
        name = name.replaceAll("\\.", "/");
        FileInputStream fis = new FileInputStream(classPath + "/" + name
                + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }
}

另一个种实现自定义类加载器的方法:

/**
 * 1. 加载指定packageName下的类
 * 2. 用自定义类加载器进行加载,如果加载失败,再交给父加载器进行加载
 */
public class UrlSelfClassloader extends URLClassLoader {
    private String packageName = "";

public UrlSelfClassloader(URL[] urls, ClassLoader parent) {
    super(urls, parent);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    Class<?> aClass = findLoadedClass(name);
    if (Objects.nonNull(aClass)){
        return aClass;
    }
    if (!packageName.startsWith(name)){
        return super.loadClass(name);
    }else {
        return findClass(name);
    }
}
}

如何使用自定义的类加载器

public static void main(String args[]) throws Exception {
    MyClassLoader classLoader = new MyClassLoader("");
    Class clazz = classLoader.loadClass("");
    Object obj = clazz.newInstance();
    Method helloMethod = clazz.getDeclaredMethod("hello", null);
    helloMethod.invoke(obj, null);
}

总结

在本文中,我们讲解了类加载器的实现模型,分析了在 JDK 中类加载器的源码实现,并根据源码中的代码实现,自定义了一个类加载器的实现。

此外相信经过五和六两篇文章的学习,大家应该对如何将类加载入虚拟机中有了系统的理解。

后面的文章中,我们就要进入 JVM 的内部了,从下篇文章开始,我们就开始逐步讲解 JVM 的内存布局,了解 JVM 中的各个逻辑上划分的存储结构以及其作用,欢迎各位读者浏览。

JVM(六):探究类加载过程-下-LMLPHP

文章在公众号「iceWang」第一手更新,有兴趣的朋友可以关注公众号,第一时间看到笔者分享的各项知识点,谢谢!笔芯!

06-28 14:41