java并发基础篇-ClassLoader

/ java并发基础篇 / 没有评论 / 66浏览

ClassLoader介绍

ClassLoader最主要的职责就是加载各类class文件到内存中,并生成这个类的数据结构。加载的触发条件包括,使用new,试用其静态常量,方法,反射,启动类等

对象数组并不会导致该类的加载

ClassLoader加载流程

ClassLoader加载过程大致可以分成三个步骤,分别是加载,链接和初始化。

加载

通常来说,加载过程指的是将二进制文件数据加载到内存,然后将该二进制的静态数据结构转换为元数据的数据结构,并在虚拟机堆区生成class对象。当然,除了二进制文件还有些其他的二进制形式。jar包,war包,运行时动态生成(动态代理Proxy,ASM开源框架),网络获取等

链接

链接阶段可以简单的分为三个小步骤

  1. 验证阶段:主要包括验证文件格式(版本,md5,常量引用合法性等),元数据语义分析(CLASS字节流是否符合JVM规范,继承,覆盖,final),字节码验证(控制流正确性,操作栈准确性,符号引用验证等)

  2. 准备阶段:为类变量分配内存空间,并赋初始值

  3. 解析阶段:在常量池中寻找类接口,字段,类方法,接口方法的符号引用,并将符号引用转换为直接引用。

符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标就行,例如在Class文件中的它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

直接引用:直接指向目标的指针,相对偏移量,一个能间接定位到目标的句柄等等类似的虚拟机能够直接使用的形式。

初始化阶段

这个阶段主要执行的任务就是执行方法,方法是在编译阶段生成的,包含了所有的类变量的赋值和静态代码快的收集,编译器收集器的顺序是按照源文件中顺序的决定的。方法只能由虚拟机执行,并且保证线程安全。

ClassLoader分类

虚拟机提供了三个内置的类加载起,Bootstrap ClassLoader,Ext ClassLoader,Application ClassLoader。

BootStrap ClassLoader

BootStrap ClassLoader根加载器称为启动类加载器,顶层加载起,没有父加载器,C++实现,无法获取引用。负责加载jdk核心类库rt.jar、resources.jar、charsets.jar等

    @Test
    public void testBootStrap(){
        String string=System.getProperty("sun.boot.class.path");
        for(String s:string.split(":")){
            System.out.println(s);
        }
        System.out.println("---------");
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        for (int i = 0; i < urls.length; i++) {
            System.out.println(urls[i].toExternalForm());
        }
    }

两种方式都可以打印核心类库

Extension ClassLoader

ext classLoader为扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar。

    @Test
    public void testExt(){
        for(String s:System.getProperty("java.ext.dirs").split(":")){
            System.out.println(s);
        }
    }

打印所有的扩展库的路径

Application ClassLoader

AppClassLoader也称System ClassLoader,可由java.lang.ClassLoader.getSystemClassLoader()获取,实现为sun.misc.Launcher$AppClassLoader由启动类加载器加载,负责搜索加载当前应用classpath下的所有类,它的搜索路径是由java.class.path来指定的。

全盘委托

Java装载类使用“全盘负责委托机制”。“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入。

双亲委托

jvm虚拟机采用双亲委托模型来加载class,除了Ext ClassLoader加载器,都有一个引用指向其父的类加载器(Ext ClassLoader虽然没有父加载器的引用,但是并不代表他没有父加载器)。标准加载过程如下图:

iFK4fJ.md.png

当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。

这种加载模式可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

ClassLoader.loadClass 分析


/**
*  加载类公开方法
*/
 public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

/**
* name    : java类全限定名
* resolve : 是否链接解析类,会导致class进行链接过程,初始化* *           静态属性
*/
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        /**
        * 获取classLoader锁,如果是并行模式,会生成一个object做为monitor,否则是this
        */
        synchronized (getClassLoadingLock(name)) {
            // 首先检查是否已经加载过该类了
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //如果没有parent,则可能是ext classLoader,所以使用bootstrap加载器加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }
                // 如果夫加载器没有加载到该类,由次classLoader进行家在
                if (c == null) {
                    long t1 = System.nanoTime();
                    //必须重写此方法加载class
                    c = findClass(name);
                    // 统计数据
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            //是否链接类
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

流程代码比较简单,有些方法时native代码。

Class.forName和ClassLoader.loadClass() 区别就在于第二个参数,forName默认是true会进行初始化,loadClass不会。

ContextClassLoader

ContextClassLoader(上下文类加载器)是thread的一个属性,提供get/set方法,本身仍是普通的ClassLoader,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是AppClassLoader系统类加载器。

ContextClassLoader主要是与java spi(Service Provider Interface)合作工作。双亲委托模型虽然可以避免类重复加载,保证jdk核心类库唯一。但是由于bootstrap classLoader只能加载核心库不能加载第三方厂商的实现库,所以通过ContextClassLoader解决此问题。

SPI就是jdk提供接口关系,厂商依照此关系实现具体细节。SPI都是基本是通过ServiceLoader类来实现的,ServiceLoader类会使用contextClassLoader读取jar包中的META-INF.services文件所描述的类名,进行加载。JDBC中DriverManager也实现了通过

以JDK1.8和Mysql jdbc Driver为例:

首先,看一下此问题产生的具体原因,以Mysql的jdbc库为例,下面是五种方式

//下面是四种建立链接的方式,均以main函数运行,jdbc的jar存在classPath中

/**
* 一,网上最常用的方式
*/
Class.forName("com.mysql.cj.Driver") //加载此Driverclass文件
DriverManager.getConnection("localhost:3306/test?","scott","tiger");
/**
* 二,其实和第一种一样,com.mysql.cj.Driver的静态代码快就是下面的调用方式
*/
java.sql.DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
DriverManager.getConnection("localhost:3306/test?","scott","tiger");
/**
* 三,直接调用
*/
DriverManager.getConnection("localhost:3306/test?","scott","tiger");

/**
* 四,ClassLoaderTest为main函数的启动类,在连接前,设置一下contextClassLoader为ExtClassLoader,
*    ClassLoaderTest的ClassLoader是AppContextClassLoader,所以parent就是ExtClassLoader
*/
Thread.currentThread().setContextClassLoader(ClassLoaderTest.class.getClassLoader().getParent());
DriverManager.getConnection("localhost:3306/test?","scott","tiger");
/**
* 五,在第四种前提前加载com.mysql.cj.jdbc.Driver
*/
java.sql.DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
Thread.currentThread().setContextClassLoader(ClassLoaderTest.class.getClassLoader().getParent());
DriverManager.getConnection("localhost:3306/test?","scott","tiger");

运行结果是:除了第四种,其余的都可以正常连接,也就是说jdbc的Driver都正常加载了。

为什么第四种不可以第三种可以呢?这就是涉及到我们所说的contextClassLoader了,com.mysql.cj.jdbc.Driver这个class想要加载到jvm中,要么我们自己手动加载进来,要么通过ServiceLoader。 我们手动加载的时候,由于是在自己的main的线程,所以使用当前的AppClassLoader进行加载,当然可以找到这个class了。而通过ServiceLoader加载Driver时,它通过使用ContextClassLoader进行加载,而默认的ContextClassLoader就是AppClassLoader,所以也没有问题,但是当我们改变ContextClassLoader为ExtClassLoader的话,ServiceLoader将无法找到对应的jar也就无法加载驱动类Driver了。

ServiceLoader是jdk核心库位于rt.jar中,所以它的ClassLoader是bootStrap ClassLoader

贴一下ServiceLoader和DriverMnager部分源代码


public class DriverManager {
    private static void loadInitialDrivers() {
        ....
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ...
                /**
                * 通过ServiceLoader加载drvier class。
                */
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                }
                return null;
            }
        });
        ...
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                //这里我感觉是一个防范措施,使用SystemClassLoader再次加载,确保Class初始化
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }
    ....
}


public final class ServiceLoader<S> implements Iterable<S>{
...
private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        //如果cl为null,就是直接获取AppClassLoader
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }
...
public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
        return new ServiceLoader<>(service, loader);
}
public static <S> ServiceLoader<S> load(Class<S> service) {
    //默认使用ContextClassLoader进行加载
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

}

现在可以知道,为什么会出现ContextClassLoader了吧,就是因为核心库使用BootstrapClassLoader,而由于全盘委托机制和双亲委托除非我们手动加载所需要的class,否则会导致BootstrapClassLoader找不到class。所以才出现ContextClassLoader这个东西,必要时使用这个ClassLoader加载所需要类

结尾

本文只是针对JDBC做了比较详细的分析,可能其他的SPI使用方式不同,也可能相同,需要针对具体的问题具体分析,我们只需要知道一点,ServiceLoader读取的文件内容只是一个类全限定名,这个时候如果ServceLoader需要通过全限定名进行加载时,必须要设置正确的ContextClassLoader确保第三方的class能够加载。不过,如果我们要加载的class已经在内存了,那么ServiceLoader找到找不到已经没有关系了~~

后面会加入些字节码的内容~