JVM类加载器解惑——loadClass(name)和findClass方法

在上文《JVM类加载机制详解——类加载器》详细讲解了类加载器ClassLOader,但是遗留了一个问题:loadClass(name)和findClass(name)方法如何去理解,如果自定义类加载器,到底需要重写哪个方法?为什么?相信大家应该也有过这个疑惑或者正在为此疑惑着,那么就随我一起来探索吧。

说到这两个方法,就不得不说双亲委派(关于双亲委派的介绍可以参见我上篇,这里不再赘述),主要看下下面这段描述:

双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK 1.2面世以前的“远古”时代。双亲委派模型在JDK 1.2之后才被引入, 但是类加载器的概念和抽象类java.lang.ClassLoader则在Java的第一个版本中就已经存在, 面对已经存在的用户自定义类加载器的代码, Java设计者们引入双亲委派模型时不得不做出一些妥协, 为了兼容这些已有代码, 无法再以技术手段避免loadClass()被子类覆盖的可能性, 只能在JDK 1.2之后java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法, 而不是在loadClass()中编写代码。 上节我们已经分析过loadClass()方法, 双亲委派的具体逻辑就实现在这里面,按照loadClass()方法的逻辑, 如果父类加载失败, 会自动调用自己的findClass()方法来完成加载, 这样既不影响用户按照自己的意愿去加载类, 又可以保证新写出来的类加载器是符合双亲委派规则的。

这段话出自《深入理解Java虚拟机:JVM高级特性与最佳实践 第3版》,这里我将以几个“灵魂拷问”的方式来帮助你进行理解。

1、loadClass(name)方法有什么作用?—— loadClass(name)

通过查看java.lang.ClassLoader类源码可以知道,双亲委派的实现逻辑就是loadClass(String name)方法,话不多说,看源码:

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



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 {
                    // 如果父类加载器为空(根据上面所说,即为BootstrapClassLoader),则默认使用启动类加载器作为父加载器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
                // 如果父类加载器加载失败,则抛出ClassNotFoundException 异常
            }

            // 如果抛出ClassNotFoundException 异常,并且还没有被加载到,则调用自己的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) {
            //解析class:符合引用转直接引用
            resolveClass(c);
        }
        return c;
    }
}

上述代码详细描述了双亲委派机制的实现逻辑,代码其实很简单,我已加了注释,其实主要就是这样的加载流程:当加载一个类时,首先是尝试去当前类加载器的内存中找是否已经加载了(已经加载过的类会存放在加载它的的类加载器的内存中),如果找不到就委托父类加载器加载,也是一样的先去内存中找……直到启动类加载器还没有加载到的话,就会沿原路返回,往下一级一级地去各个类加载器的加载路径中去加载,直到找到。有人可能已经注意的,这段代码里调用了我今天要说的findClass(name)方法,那么,“灵魂拷问”又来了:

2、findClass(name)方法有什么作用?

回到上面那段描述,双亲委派模型这个概念其实是JDK1.2之后才出现的,但是呢,java.lang.ClassLoader类却是伴随着Java的诞生就已经存在了,也就是说,在JDK1.2之前,就已经广泛存在开发者自定义的类加载器,那么也必然会重写loadClass(String name)方法(为什么说必然呢?因为那时候还没有findClass(name)方法),这些重写的loadClass(name)方法也必然不存在双亲委派的逻辑。那么当JDK1.2之后引入了双亲委派模型之后,我们不能为了强制用户写出的代码遵循双亲委派模型而不允许重写loadClass(name)方法,如果这样做的话,那些JDK1.2之前的代码就有问题了,因此这也是JDK的妥协,这也就是上文所说的第一次打破双亲委派。但是JDK1.2之后的用户们,如果还是有用户非要去重写loadClass(name)方法怎么办呢?所以就增加了一个findClass()方法(该方法默认是空实现,由用户自己去实现),如果父类加载失败,就会自动调用自己的findClass(name)来加载,如果那段没有实现双亲委派逻辑的代码没有加载到类时,这样既不影响用户按照自己的意愿去加载类,也保证了自定义类加载器遵循了双亲委派原则。但是到这里,我还要发一句“灵魂拷问”:

3、我凭什么要重写findClass(name)方法?

有没有注意到上面我的措辞:“引导”,JDK大佬们是如何引导我们去重写findClass(name)方法的呢?且看源码:

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

这回是不是恍然大悟了,JDK的大佬们早就想到了,他们把findClass(name)方法什么都不做,默认就抛出一个异常,如果你自定义了类加载器,并且想要去重写loadClass(name)的话,你就必须重写findClass(name)!所以这样也就解释了为什么loadClass(name)方法中最后调用了findClass方法,因为JDK已经引导了我们把自己编写的类的加载逻辑写入在findClass方法中了。

注:

   当然,如果你是要打破双亲委派,那么直接在重写的loadClass(name)方法里自己加载想要加载的类,那是没关系的,但是如果要遵循双亲委派,那就必须要重写findClass(name),以上所说的“引导”,都是在遵循双亲委派的前提下说的。

拿我上篇博客中最后自定义类加载器那段代码来举例,如果我不重写findClass(String className)方法的话,会是什么结果:

public class ClassLoaderTest extends ClassLoader {


    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
 
        return super.loadClass(name);
    }

    /**
     * @Description findClass,用于使用自定义加载器加载类
     * @Param [name]
     * @return java.lang.Class<?>
     **/
    //@Override
    //protected Class<?> findClass(String className) throws ClassNotFoundException {
    //    byte[] data = readClassBytes(className.replace('.', '/'));
    //    return defineClass(className, data, 0, data.length);
    //}

    //读取Class文件到byte[]
    private byte[] readClassBytes(String name) throws ClassNotFoundException {
        InputStream inputStream = null;
        ByteArrayOutputStream outputStream = null;

        String rootPath = this.getClass().getResource("/").getPath();
        File file = new File(rootPath + name + ".class");
        if (!file.exists()) {
            throw new ClassNotFoundException(name);
        }

        try {
            inputStream = new FileInputStream(file);
            outputStream = new ByteArrayOutputStream();

            int size = 0;
            byte[] buffer = new byte[1024];

            while ((size = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, size);
            }

            return outputStream.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                inputStream.close();
                outputStream.close();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }

        return null;
    }

    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoaderTest classLoaderTest2 = new ClassLoaderTest();
        Class clazz = classLoaderTest2.loadClass(User.class.getName());
        System.out.println(clazz);
        System.out.println(clazz.getClassLoader());
    }
}

运行它,结果为:

Exception in thread "main" java.lang.ClassNotFoundException: com.jvmTest.classloader.User
	at java.lang.ClassLoader.findClass(ClassLoader.java:530)
	at com.jvmTest.classloader.ClassLoaderTest.loadClass(ClassLoaderTest.java:21)
	at com.jvmTest.classloader.ClassLoaderTest.main(ClassLoaderTest.java:80)

但是可能又有人要钻牛角尖:我就不重写findClass(String className),我就把类的加载逻辑全写到loadClass方法内难道不行吗?当然可以,Java并没有强制规定不行,所以上面那段描述中才会说“并引导用户编写的类加载逻辑时尽可能去重写这个方法”,这个方法就是JDK1.2为了在以后的版本中引入双亲委派机制而对用户所做的妥协,以防止在1.2之后应用程序中出现“破坏双亲委派”的情况出现(打破双亲委派是人为刻意为了解决某些问题而采取的解决办法,并不是JDK所默认的行为)。

 

前面通过3个“灵魂拷问”的方式讲解了loadClass(String name)和findClass(String className),不知道你们理解了没有?可能有些举例不合适,但是对帮助理解会有作用,下面我们通过对它俩的应用来加深一下理解。

4、打破双亲委派

上面说了,双亲委派的实现逻辑在loadClass(String name)方法内,那么想要打破双亲委派,那必然需要重写它。其实这里说“必须”并不准确,因为在一定的前提下,不重写loadClass(String name)方法也能打破双亲委派,这时候就需要把打破双亲委派的逻辑放到findClass(String name)方法内,为什么这么说呢?不知道有没有人注意到一个容易忽略的问题:loadClass(String name)是public修饰的,而findClass(String className)则是protected修饰,而在我们的应用中,大部分情况下调用这个自定义加载器的地方跟它并不在一个包内,但是如果在同一个包内的话,就可以调用findClass(name)方法来加载我们指定的类,但是这样做并没有实际的实用意义,所以才说要打破双亲委派,就必须重写loadClass(String name)。我们用代码先来测试一下使用findClass(name)来打破双亲委派吧,看看到底行不行?

/**
 * 打破双亲委派测试——不重写loadClass(String className)方法
 **/
public class ClassLoaderTest2 extends ClassLoader {

    /**
     * 将打破双亲委派逻辑写到这里
     **/
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        if(name.equals("com.jvmTest.classloader.User")){
            byte[] data = readClassBytes(name.replace('.', '/'));
            return defineClass(name, data, 0, data.length);
        }else{
            return getParent().loadClass(name);
        }
    }

    //读取Class文件到byte[]
    private byte[] readClassBytes(String name) throws ClassNotFoundException {
        InputStream inputStream = null;
        ByteArrayOutputStream outputStream = null;

        String rootPath = this.getClass().getResource("/").getPath();
        File file = new File(rootPath + name + ".class");
        if (!file.exists()) {
            throw new ClassNotFoundException(name);
        }

        try {
            inputStream = new FileInputStream(file);
            outputStream = new ByteArrayOutputStream();

            int size = 0;
            byte[] buffer = new byte[1024];

            while ((size = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, size);
            }

            return outputStream.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                inputStream.close();
                outputStream.close();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }

        return null;
    }

    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoaderTest2 classLoaderTest2 = new ClassLoaderTest2();
        Class clazz1 = classLoaderTest2.loadClass(User.class.getName());
        Class clazz2 = classLoaderTest2.findClass(User.class.getName());
        Class clazz3 = classLoaderTest2.findClass(Order.class.getName());

        System.out.println(clazz1);
        System.out.println(clazz1.getClassLoader());

        System.out.println(clazz2);
        System.out.println(clazz2.getClassLoader());

        System.out.println(clazz3);
        System.out.println(clazz3.getClassLoader());
    }
}

运行上述代码,打印结果为:

class com.jvmTest.classloader.User
sun.misc.Launcher$AppClassLoader@14dad5dc
class com.jvmTest.classloader.User
com.jvmTest.classloader.ClassLoaderTest2@1b6d3586
class com.jvmTest.classloader.Order
sun.misc.Launcher$AppClassLoader@14dad5dc

1)前两行输出结果,说明我如果不重写loadClass(String name)方法的话,这时使用的就是AppClassLoader;

2)中间两行输出结果,说明使用findClass(name)方法一样能打破双亲委派,User类使用的是自定义类加载器ClassLoaderTest2所加载;

3)最后两行输出结果,进一步说明了2),并且只有com.jvmTest.classloader.User类使用自定义加载器加载,其它类还是使用AppClassLoader加载。

这种调用自定义类加载器的代码与这个加载器的代码处于同一个包内,所以可以这样干,那么如果我不在这个包内调用呢?我定义了一个com.jvmTest.Main类,在这里调用看看结果如何:

看到了吧,压根调用不到,编译都不通过,因为findClass(name)是protected修饰的。

在绝大多数情况下,如果想打破双亲委派,就必须在自定义的类加载器中重写loadClass(String name)方法,但是也有例外,就是这个自定义的加载器的调用方与其同处于一个包内,所以我看到网上几乎所有的说法都是:如果想打破双亲委派,就必须重写loadClass(),如果不想打破,就只需要重写findClass()就行了,其实这个说法是不严谨的。

在Java的历史中,双亲委派模型主要出现过3次被破坏的情况。

第一次被破坏就是我上面说的,是在JDK1.2之前,那时候还没有出现双亲委派的概念,破坏必然到处存在;

第二次被破坏是由于模型自身缺陷而导致一些实际问题无法解决,只有打破双亲委派才能很好地解决这些问题,比如在使用JDBC是用到的的java.sql.Driver接口(在《JVM类加载机制详解——类加载器》中有详细说明);

第三次被破坏是对于程序热部署的需求,比较知名的实现方案如OSGi,这个我没有使用过,所以暂时不太了解。

 

以上就是我个人对java.lang.ClassLoader类中的oadClass(String name)和findClass(String name)方法的理解,仅供参考,如有理解有误的地方,也欢迎指正。

 

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值