ClassLoader详解
阅读原文时间:2021年04月20日阅读:1

ClassLoader详解

什么是ClassLoader

ClassLoader 叫做类加载器。虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流” 这个动作放到java虚拟机外部去实现,以便让应用程序自己决定去如何获取所需要的类,实现这个动作的代模块称之为“类加载器”。

类与类加载器

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、连接(验证、准备、解析)、使用、卸载

加载:类加载过程的一个阶段:通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象

验证:目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

准备:为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

解析:主要将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析(这里涉及到字节码变量的引用,如需更详细了解,可参考《深入Java虚拟机》)。

初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。

加载阶段,实际需要完成以下3件事情:
1.通过“类全名”来获取定义此类的二进制字节流
2.将字节流所代表的静态存储结构转换为方法区的运行时数据结构
3.在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口

相对于类加载过程的其他阶段,加载阶段(准备地说,是加载阶段中获取类的二进制字节流的动作)是开发期可控性最强的阶段,因为加载阶段可以使用系统提供的类加载器(ClassLoader)来完成,也可以由用户自定义的类加载器完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式。

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。也就是说:比较两个类是否“相等”,只有在这个两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类来源于同一个Class 文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。

获取二进制字节流的来源

通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源:

(1)从本地文件系统加载class文件,这是绝大部分程序的加载方式
(2)从jar包中加载class文件,这种方式也很常见,例如jdbc编程时用到的数据库驱动类就是放在jar包中,jvm可以从jar文件中直接加载该class文件
(3)通过网络加载class文件
(4)把一个Java源文件动态编译、并执行加载

为什么要有不同的类加载器

  1. 保护善意代码不受恶意代码的干扰
  2. 可以自定义指定从不同的源获取class文件

备注
类加载体系通过使用不同的类加载器把类放入不同的名称空间中从而保护善意代码不受恶意代码的干扰。 在jvm中,同一个名称空间中的类是可以直接交互的,但在不同名称空间中的类就不行,除非提供另外的机制。这样,名字空间就起到了一个屏障的作用。

双亲委派模型

既然可以有多个类加载器,如果用户自己编写了一个称为java.lang.Object,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,java类体系中最基础的行为也无法保证,应用程序会变得一片混乱。所以衍生出双亲委派模型。
绝大部分Java程序都会使用到以下3中系统提供的类加载器。
- 启动类加载器(Bootstrap ClassLoader):这个类加载器负责装载Java API(java 核心类库) 。将存放在\lib 目录中的或者被-Xbootclasspath参数所指定的路径中的,并且是被虚拟机识别的类库加载到虚拟机内存中。这个类加载器是在JVM启动的时候创建的,和其他的类装载器不同的地方在于这个装载器是通过native code来实现的,而不是用Java代码。
- 扩展类加载器(Extension classLoader):它负责加载\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可用直接使用扩展类加载器。
- 应用程序类加载器(Application ClassLoader):也称为系统类加载器,它负责加载类路径(ClassPath) 上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

我们的应用程序都是由这3种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器,这些类加载之前的关系一般如下图所示:

上图所示的类加载器之间的这种层次关系,就称为类加载器的双亲委派模型(Parent Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。子类加载器和父类加载器不是以继承(Inheritance)的关系来实现,而是通过组合(Composition)关系来复用父加载器的代码。

双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
使用这种模型来组织类加载器之间的关系的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如java.lang.Object类,无论哪个类加载器去加载该类,最终都是由启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。

双亲委派模型的实现

下面我们从代码层面了解几个Java中定义的类加载器及其双亲委派模式的实现,它们类图关系如下:

顶层的类加载器是ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器),AppClassLoader与ExtClassLoader都是Lancher的内部类。
下面主要介绍ClassLoader中几个比较重要的方法:

  • loadClass()
    该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法,loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,其源码如下,loadClass(String name, boolean resolve)是一个重载方法,resolve参数代表是否生成class对象的同时进行链接相关操作。
    虚拟机在进行类加载的时候胡调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是调用自己的loadClass()方法。

    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) {
    //如果找不到,则委托给父类加载器去加载
    c = parent.loadClass(name, false);
    } else {
    //如果没有父类,则委托给启动加载器去加载
    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.
                   //如果还是没有找到,再调用本身的findClass 方法来进行类加载
                    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;
    }
    }

从代码中可以看出,loadClass的逻辑为:当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载去的父加载器去加载,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()方法去加载。从loadClass实现也可以知道如果不想重新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载自己指定的类,那么我们可以直接使用this.getClass().getClassLoder.loadClass(“className”),这样就可以直接调用ClassLoader的loadClass方法获取到class对象。

  • findClass()

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

代码中findClass 直接抛出ClassNotFoundException异常。findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样我们就可以自己重写findClass()方法来完成类的加载。这样也就可以保证自定义的类加载器也符合双亲委派模式。

  • defineClass()
    defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象:使用示例如下:

    protected Class findClass(String name) throws ClassNotFoundException {
    // 获取类的字节数组
    byte[] classData = getClassData(name);
    if (classData == null) {
    throw new ClassNotFoundException();
    } else {
    //使用defineClass生成class对象
    return defineClass(name, classData, 0, classData.length);
    }
    }

  • resolveClass()
    为native 方法,在loadClass中被调用。功能:链接指定的 Java 类。链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。

接下来 SecureClassLoader 和URLClassLoader。
SecureClassLoader 继承ClassLoader,新增了几个与使用相关的代码源(对代码源的位置及其证书的验证)和权限定义类验证(主要指对class源码的访问权限)的方法,一般我们不会直接跟这个类打交道,更多是与它的子类URLClassLoader有所关联。
ClassLoader是一个抽象类,很多方法是空的没有实现,比如 findClass()、findResource()等。而URLClassLoader这个实现类为这些方法提供了具体的实现,并新增了URLClassPath类协助取得Class字节码流等功能,在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。但是URLClassLoader

ExtClassLoader 和AppClassLoader 都继承URLClassLoader。ExtClassLoader并没有重写loadClass()方法,而AppClassLoader重载了loadCass()方法,但最终调用的还是父类loadClass()方法。所以还是保证了双亲委派模型。

上面是类之前的关系,之前说了。子类加载器和父类加载器不是以继承(Inheritance)的关系来实现,而是通过组合(Composition)关系来复用父加载器的代码。从父到子以此为 Bootstrap ClassLoader——>ExtClassLoader——>AppClassLoader——>User ClassLoader。
从上面的类图来看,ExtClassLoader和AppClassLoader之前都是继承的URLClassLoader。所以从继承上他们没有父子关系。看下java程序的入口 Launcher类的构造方法源码:

public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
        //先初始化了个ExtClassLoader
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
        //初始化了个AppClassLoader,然后把ExtClassLoader作为AppClassLoader的父loader
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
//把初始化的AppClassLoader 作为全局变量保存起来,并设置到当前线程contextClassLoader
        Thread.currentThread().setContextClassLoader(this.loader);
        String var2 = System.getProperty("java.security.manager");
        if(var2 != null) {
            SecurityManager var3 = null;
            if(!"".equals(var2) && !"default".equals(var2)) {
                try {
                    var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
                } catch (IllegalAccessException var5) {
                    ;
                } catch (InstantiationException var6) {
                    ;
                } catch (ClassNotFoundException var7) {
                    ;
                } catch (ClassCastException var8) {
                    ;
                }
            } else {
                var3 = new SecurityManager();
            }

            if(var3 == null) {
                throw new InternalError("Could not create SecurityManager: " + var2);
            }

            System.setSecurityManager(var3);
        }

    }

可以看到Launcher类初始化时,先初始化了个ExtClassLoader,然后又初始化了个AppClassLoader,然后把ExtClassLoader作为AppClassLoader的父loader。
ExtClassLoader没有指定父类,从ClassLoader中的loadClass()方法中可以看到,如果没有父类,则委托给Bootstrap ClassLoader去加载。概念上可以称为: ExtClassLoader的父类是BootstrapClassLoader。

类加载器命名空间

JAVA中,由不同类加载器加载的类在虚拟机中位于不同的命名空间下,不同命名空间下的类相互不可见。
通常会有一些疑问:如果有一个类A使用了java.util.List类,为什么在运行时会没有错误。因为按照类加载的双亲委派机制,自己写的类A一般由系统类加载器加载,而java.util.List肯定是由启动类加载器(也叫Root类加载器)加载的,所以这两个类应该不在一个命名空间下。那在运行时为什么类A还 是能访问到java.util.List?
每一个JAVA类经过加载后,在虚拟机中都有一个对应的类型。
如果类A被AppClassLoader加载,那么AppClassLoader就是此A在虚拟机中对应类型的初始类加载器。
所说的“命名空间”,是指jvm为每个类加载器维护的一个“表”,这个表记录了所有以此类加载器为“初始类加载器”(而不是定义类加载器,所以一个类可以存在于很多的命名空间中)加载的类的列表。
根据Java虚拟机规范规定,在这个过程中涉及的所有类加载器–即从AppClassLoader到Bootstrap ClassLoader间,参与过加载的,都被标记为该类型的初始类加载器。实际加载的称为定义类加载器。
所以

再回到刚到A使用java.util.List的例子,当A被加载后,解析到A使用了List,就会请求加载java.util.List。根据类的加载原理及双亲委派机制。会先请类A的类加载器,即AppClassLoader加载java.util.List,AppClassLoader当然加载不了这个List,所以它会委派给自己的父加载器,即ExtClassLoader;同理,最终会由Bootstrap ClassLoader加载这个java.util.List,并成功返回。 所以对弈List来说,AppClassLoader是初始类加载器,bootstrap是定义类加载器。List在AppClassloader的命名空间中,所以类A可以访问List。

线程上下文类加载器

上面Launcher的代码中有一行是那AppClassLoader 设置到当前线程contextClassLoader。 那么contextClassLoader 是做什么的呢?
双亲委派模型很好地解决了个各类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为”基础”,是因为他们总是作为被用户代码调用的API,但如果基础类又要调用回用户的代码,那怎么办呢?
比如rt.jar中的spi服务,比如JDBC。它存在的包rt.jar是由BootStrapClassLoader加载的,但是在它的DriverManager中会调用spi具体的实现类,而DriverManager是由BootStrapClassLoader定义的,所以也会用BootStrapClassLoader 来加载实现类 ,但BootStrapClassLoader加载器无法加载这些代码。
为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader),有了线程上下文加载器,spi服务就可以使用contextClassLoader加载器去加载所需要的spi实现类的代码,所以BootStrapClassLoader就是spi实现类的初始加载器,contextClassLoader是spi实现类的定义类加载器,这样的话,在BootStrapClassLoader命名空间中就会有spi实现类,所以DriverManager里面就可以直接访问mysql类了。