一文读懂系列-ClassLoader

作者: monkey01 | 来源:发表于2018-05-24 10:44 被阅读31次

一文读懂系列-ClassLoader

我们都知道JVM中所有的类都是通过类加载器ClassLoader加载到JVM中才可以使用,本文就介绍下ClassLoader,让大家从底层知道一个类是如何加载出来的,当再遇到ClassNotFound Error的时候知道该怎么查问题。

类加载

类加载过程JVM需要完成3个工作:

1)通过全路径类名来获取该类的二进制字节流;

2)将字节流所代表的静态存储结构转化为方法区的运行时数据结构;

3)在java堆中生成一个代表这个类的Class对象,作为方法区访问这些数据的访问入口。

类加载过程中需要的类的二进制字节流,可以有多种形式提供,可以是最常见的class文件,也可以是网络上获取到的二进制字节流,当然也可以是jar包、war包之类的class文件压缩包,这样的设定就给了开发者很大的空间,可以利用这样的特性来自由的实现类的动态下发、动态更新加载类,实现功能的动态发布和功能的动态更新。可以发现我们日常用到的Tomcat、Jboss、WAS等这类应用服务器都可以实现不停服务动态增量更新版本,这些功能都是通过类加载器动态加载class文件来实现的,包括OSGi这样的模块化框架都是通过类加载器完成的。

加载阶段完成后,虚拟机将外部的二进制字节流按照JVM的要求将类数据存储在方法区中,然后在Java堆中实例化一个类的对象,这个对象将作为程序访问方法区的类型数据的入口。

类加载器

介绍了类加载的过程类被加载到JVM中需要有类加载器,下面介绍下类加载器,类加载器可以分为三种:

1)Bootstrap ClassLoader 启动类加载器:启动类加载器负责将JAVA_HOME/lib下的类库加载到虚拟机中;

2)Extension ClassLoader 扩展类加载器:扩展类加载器负责将JAVA_HOME/lib/ext目录下的类户籍在到虚拟机中;

3)Application ClassLoader 应用程序类加载器:负责加载用户类路径上所指定的类库,开发者可以直接通过ClassLader的getSystemClassLoader()方法获取到该类应用程序的类加载器。

在JVM中判断一个类的唯一性,除了要看类自身还要看该类的类加载器,必须类和加载该类的类加载器都一致才能说明该类在JVM中的唯一性。如果同一个类的二进制字节码,被两个不同的ClassLoader去加载那么这两个类在同一个JVM中也是不同的存在,通过Class的equals()方法和instanceof 都可以进行判断出这两个类是不同的。

下面这个例子自定义了一个ClassLoader,并且通过这个类加载器加载了com.monkey01.jvm.ClassLoaderTest这个类,可以发现获取的类全路径名是com.monkey01.jvm.ClassLoaderTest,但是通过instanceof去比对类型却是false,因为jvm启动的时候系统通过AppClassLoader也加载了一个com.monkey01.jvm.ClassLoaderTest类,所以比对下来是不一样的两个类。

public class ClassLoaderTest {

    public static void main(String[] args) throws Exception {

        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };

        Object obj = myLoader.loadClass("com.monkey01.jvm.ClassLoaderTest").newInstance();

        System.out.println(obj.getClass());
        System.out.println(obj instanceof com.monkey01.jvm.ClassLoaderTest);
    }
}

concole out:

class com.monkey01.jvm.ClassLoaderTest
false

双亲委派

类加载的过程中有个很核心的概念-双亲委派,双亲委派模型是JVM中类加载的一种父子模型,其实这种类似的模型在很多其它开发语言中也有类似的使用,例如android、iOS中的事件分发模型其实也是一种委派模型。JVM中类加载的双亲委派的流程是:如果一个类加载器收到了一个类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去加载类,每一个层次的类加载器都是这样操作,因此所有的加载请求最终都会传递到顶层的启动类加载器,只有当父加载器在指定的搜索范围内没有找到需要加载的类反馈无法完成这个加载请求时,子加载器才会去自己加载,如果还是无法加载则继续传给孙加载器,就这样一层层的进行传递,直到最终的叶子加载器还无法加载就会报ClassNotFound Exception。这里还要注意下每个加载器都会先在自己的缓存中查找是否已经加载,如果已经加载了就直接从缓存中返回,不需要再去加载class文件了。

Screenshot 2018-05-18 15.16.54

使用双亲委派模型最大的优点在于,所有的加载请求都是向上传递的,对于一些属于系统的类,例如rt.jar中的一些类,不管是哪个类加载器去加载,最后都会由Bootstrap ClassLoader去加载完成,这样就保证了系统类在同一个JVM环境中只有一个类,不会因为不同类加载器去加载而在JVM中产生多份。

类加载器加载源码

最后我们通过查看源码来了解下ClassLoader加载源码的代码实现,ClassLoader所有的加载过程都是通过loadClass()方法来实现的,在loadClass中的逻辑分为下面3步:

1)执行findLoadedClass(String)去检测这个class是不是已经加载过了。

2)执行父加载器的loadClass方法。如果父加载器为null,则jvm内置的加载器去替代,也就是Bootstrap ClassLoader。这也解释了ExtClassLoader的parent为null,但仍然说Bootstrap ClassLoader是它的父加载器。

3)如果向上委托父加载器没有加载成功,则通过findClass(String)查找。

如果class在上面的步骤中找到了,参数resolve又是true的话,那么loadClass()又会调用resolveClass(Class)这个方法来生成最终的Class对象。

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) {
                        //父加载器不为空则调用父加载器的loadClass
                        c = parent.loadClass(name, false);
                    } else {
                        //父加载器为空则调用Bootstrap Classloader
                        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();
                    //父加载器没有找到,则调用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()
                resolveClass(c);
            }
            return c;
        }
    }

总结

从整篇文章我们可以了解到JVM中ClassLoader的作用、分类、双亲委派、类加载过程,让大家能从更加全面的角度了解ClassLoader,并不是简单停留在使用的层面,让大家从底层更加深刻的认识ClassLoader。

相关文章

网友评论

    本文标题:一文读懂系列-ClassLoader

    本文链接:https://www.haomeiwen.com/subject/snijjftx.html