聊聊ClassLoader

一、什么是类加载器

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

周志明. 深刻理解Java虚拟机:JVM高级特性与最佳实践(第2版) 机械工业出版社.spring

二、须要注意的点

两个类是“相等”(包括equals、isAssignableFrom、isInstanceOf)的前提条件是这两个类的类加载器相等。sql

public static void main(String[] args) throws ClassNotFoundException, MalformedURLException {

    URL jar = new URL("file:\\G:\\code\\demo\\demo-0.0.1-SNAPSHOT.jar");
    URL[] urls = new URL[]{jar};

    //类加载器1
    URLClassLoader classLoader1 = new URLClassLoader(urls,null);
    Class userClass1 = classLoader1.loadClass("com.demo.User");

    //类加载器2
    URLClassLoader classLoader2 = new URLClassLoader(urls,null);
    Class userClass2 = classLoader2.loadClass("com.demo.User");

    //输出false,缘由:userClass来自不一样的类加载器
    System.out.println(userClass1.equals(userClass2));
}

三、类加载器的分类

  • 启动类加载器(BootstrapClassLoader):前面已经介绍过,这个类将器负责将存放在<JAVA_HOME>\lib目录中的,或 者被-Xbootclasspath参数所指定的路径中的,而且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即便放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器没法被Java程序直接引用,用户在编写自定义类加载器时,若是须要把加载请求委派给引导类加载器,那直接使用null代替便可。
  • 扩展类加载器(ExtensionClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的全部类库,开发者能够直接使用扩展类加载器。
  • 应用程序类加载器(ApplicationClassLoader):这个类加载器由sun.misc.Launcher$App-ClassLoader实现。因为这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,因此通常也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者能够直接使用这个类加载器,若是应用程序中没有自定义过本身的类加载器,通常状况下这个就是程序中默认的类加载器。数据库

    周志明. 深刻理解Java虚拟机:JVM高级特性与最佳实践(第2版) 机械工业出版社tomcat

四、类加载器的双亲委托加载

ClassLoader的结构中有一个重要的成员变量parent,也就是咱们所说的ClassLoader的双亲。微信

// java.lang.ClassLoader
public abstract class ClassLoader {

    private static native void registerNatives();
    static {
        registerNatives();
    }

    // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;
    ...

委派ClassLoader进行类加载的过程应该是:oracle

  • 首先判断类是否已经加载,若是已经加载直接返回已加载的类
  • 若是没有加载交给parent进行加载,若是加载成功返回类
  • 若是parent加载失败,本身尝试加载
    JDK中loadClass的过程以下:
// java.lang.ClassLoader
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 {
                // 若是没有加载交给parent进行加载,若是加载成功返回类
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 若是parent=null时,认为parent=启动类加载器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            
            // 若是parent加载失败,本身尝试加载
            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;
    }

检查与加载过程如图所示:
图1app

五、双亲委托模式的弊端

要说明弊端,必须引入SPI。框架

什么是SPI

SPI ,全称为 Service Provider Interface,是一种服务发现机制。JAVA中定义的SPI通常是要第三方进行实现,咱们比较常见的如:java.sql.Driver,JDK中只定义了Driver接口,并无去实现,Driver的实现由数据库厂商来实现。
oralce数据库驱动的实现以下:(来自:ojdbc6-11.2.0.4.0.jar)ide

public class OracleDriver implements Driver {
    ...
}

同时第三方jar必须增长配置文件:图2.png
java.sql.Driver文件内容:oracle.jdbc.OracleDriver
java虚拟机经过扫描jar包下的配置文件信息加载对应接口的实现类。

SPI小示例

定义SayHello接口

package com.demo;
public interface SayHello {
    void hello();
}

实现SayHello接口

package com.demo;
public class SayHelloImpl implements SayHello {
    @Override
    public void hello() {
        System.out.println("hello");
    }
}

在META-INF/services目录下增长com.demo.SayHello文件,文件内容为:com.demo.SayHelloImpl
主函数

public class ClassLoaderApplication {
    public static void main(String[] args) {
        ServiceLoader<SayHello> sayHellos = ServiceLoader.load(SayHello.class);
        for (SayHello s : sayHellos) {
            s.hello();
        }
    }
}

SPI引入给双亲委托模式带来的冲击

以java.sql.Driver为例,java.sql.Driver接口定义在rt.jar中,而rt.jar由BootstrapClassLoader负责加载,Driver最终由同在rt.jar包中的DriverManager类所使用,代码以下:

// class : DriverManager
private static void loadInitialDrivers() {
    ...
     ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    ...

因为DriverManager类在rt.jar中,因此能够认定DriverManager类最终由BootstrapClassLoader加载器负责加载,而咱们的Driver实现类(OracleDriver)通常都是由应用程序类加载器(ApplicationClassLoader)或自定义类加载器负责加载,因此Driver的实现对BootstrapClassLoader是不可见的,这样一定会致使DriverManager的loadInitialDrivers失败。

解决方案

为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(ThreadContextClassLoader)。这个类加载器能够经过java.lang.Thread类的setContextClassLoaser()方法进行设置,若是建立线程时还未设置,它将会从父线程中继承一个,若是在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
看下ServiceLoader的相关源码:

//class : ServiceLoader
public static <S> ServiceLoader<S> load(Class<S> service) {
    //ServiceLoader就是经过Thread.currentThread().getContextClassLoader()获取类加载器的
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

因此解决DriverManager类中能够加载OracleDriver的问题,能够经过将应用程序类加载器(ApplicationClassLoader)设置到java.lang.Thread类的setContextClassLoaser()方法来解决。
其实这一过程咱们基本不用本身来敲代码实现,由于咱们用的容器都已经帮咱们考虑到了。以tomcat(9.0.24)的源码为例:

//class : WebappLoader
@Override
public void backgroundProcess() {
        if (reloadable && modified()) {
            try {
                Thread.currentThread().setContextClassLoader
                    (WebappLoader.class.getClassLoader());
                if (context != null) {
                    context.reload();
                }
            } finally {
                if (context != null && context.getLoader() != null) {
                    Thread.currentThread().setContextClassLoader
                        (context.getLoader().getClassLoader());
                }
            }
        }
  }

六、再来聊聊Spring中的ClassLoader

咱们定义的JavaBean在spring的getBean方法的建立过程其实与DriverManager建立Driver实例的过程是同样的。咱们的JavaBean是通常都是由应用程序类加载器(ApplicationClassLoader)或自定义类加载器负责加载,而Spring作为一款开源框架多是有更高层类加载器负责加载,因此Spring获取JavaBean的Class时第一优先级是经过Thread.currentThread().getContextClassLoader()来获取JavaBean的Class的类加载器。如代码所示:

//org.springframework.util.ClassUtils
public static ClassLoader getDefaultClassLoader() {
    ClassLoader cl = null;

    try {
        cl = Thread.currentThread().getContextClassLoader();
    } catch (Throwable var3) {
    }

    if (cl == null) {
        cl = ClassUtils.class.getClassLoader();
        if (cl == null) {
            try {
                cl = ClassLoader.getSystemClassLoader();
            } catch (Throwable var2) {
            }
        }
    }

    return cl;
}

一、什么是类加载器

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

周志明. 深刻理解Java虚拟机:JVM高级特性与最佳实践(第2版) 机械工业出版社.

二、须要注意的点

两个类是“相等”(包括equals、isAssignableFrom、isInstanceOf)的前提条件是这两个类的类加载器相等。

public static void main(String[] args) throws ClassNotFoundException, MalformedURLException {

    URL jar = new URL("file:\\G:\\code\\demo\\demo-0.0.1-SNAPSHOT.jar");
    URL[] urls = new URL[]{jar};

    //类加载器1
    URLClassLoader classLoader1 = new URLClassLoader(urls,null);
    Class userClass1 = classLoader1.loadClass("com.demo.User");

    //类加载器2
    URLClassLoader classLoader2 = new URLClassLoader(urls,null);
    Class userClass2 = classLoader2.loadClass("com.demo.User");

    //输出false,缘由:userClass来自不一样的类加载器
    System.out.println(userClass1.equals(userClass2));
}

三、类加载器的分类

  • 启动类加载器(BootstrapClassLoader):前面已经介绍过,这个类将器负责将存放在<JAVA_HOME>\lib目录中的,或 者被-Xbootclasspath参数所指定的路径中的,而且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即便放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器没法被Java程序直接引用,用户在编写自定义类加载器时,若是须要把加载请求委派给引导类加载器,那直接使用null代替便可。
  • 扩展类加载器(ExtensionClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的全部类库,开发者能够直接使用扩展类加载器。
  • 应用程序类加载器(ApplicationClassLoader):这个类加载器由sun.misc.Launcher$App-ClassLoader实现。因为这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,因此通常也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者能够直接使用这个类加载器,若是应用程序中没有自定义过本身的类加载器,通常状况下这个就是程序中默认的类加载器。

    周志明. 深刻理解Java虚拟机:JVM高级特性与最佳实践(第2版) 机械工业出版社

四、类加载器的双亲委托加载

ClassLoader的结构中有一个重要的成员变量parent,也就是咱们所说的ClassLoader的双亲。

// java.lang.ClassLoader
public abstract class ClassLoader {

    private static native void registerNatives();
    static {
        registerNatives();
    }

    // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;
    ...

委派ClassLoader进行类加载的过程应该是:

  • 首先判断类是否已经加载,若是已经加载直接返回已加载的类
  • 若是没有加载交给parent进行加载,若是加载成功返回类
  • 若是parent加载失败,本身尝试加载
    JDK中loadClass的过程以下:
// java.lang.ClassLoader
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 {
                // 若是没有加载交给parent进行加载,若是加载成功返回类
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 若是parent=null时,认为parent=启动类加载器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            
            // 若是parent加载失败,本身尝试加载
            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;
    }

检查与加载过程如图所示:
图1

五、双亲委托模式的弊端

要说明弊端,必须引入SPI。

什么是SPI

SPI ,全称为 Service Provider Interface,是一种服务发现机制。JAVA中定义的SPI通常是要第三方进行实现,咱们比较常见的如:java.sql.Driver,JDK中只定义了Driver接口,并无去实现,Driver的实现由数据库厂商来实现。
oralce数据库驱动的实现以下:(来自:ojdbc6-11.2.0.4.0.jar)

public class OracleDriver implements Driver {
    ...
}

同时第三方jar必须增长配置文件:图2.png
java.sql.Driver文件内容:oracle.jdbc.OracleDriver
java虚拟机经过扫描jar包下的配置文件信息加载对应接口的实现类。

SPI小示例

定义SayHello接口

package com.demo;
public interface SayHello {
    void hello();
}

实现SayHello接口

package com.demo;
public class SayHelloImpl implements SayHello {
    @Override
    public void hello() {
        System.out.println("hello");
    }
}

在META-INF/services目录下增长com.demo.SayHello文件,文件内容为:com.demo.SayHelloImpl
主函数

public class ClassLoaderApplication {
    public static void main(String[] args) {
        ServiceLoader<SayHello> sayHellos = ServiceLoader.load(SayHello.class);
        for (SayHello s : sayHellos) {
            s.hello();
        }
    }
}

SPI引入给双亲委托模式带来的冲击

以java.sql.Driver为例,java.sql.Driver接口定义在rt.jar中,而rt.jar由BootstrapClassLoader负责加载,Driver最终由同在rt.jar包中的DriverManager类所使用,代码以下:

// class : DriverManager
private static void loadInitialDrivers() {
    ...
     ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    ...

因为DriverManager类在rt.jar中,因此能够认定DriverManager类最终由BootstrapClassLoader加载器负责加载,而咱们的Driver实现类(OracleDriver)通常都是由应用程序类加载器(ApplicationClassLoader)或自定义类加载器负责加载,因此Driver的实现对BootstrapClassLoader是不可见的,这样一定会致使DriverManager的loadInitialDrivers失败。

解决方案

为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(ThreadContextClassLoader)。这个类加载器能够经过java.lang.Thread类的setContextClassLoaser()方法进行设置,若是建立线程时还未设置,它将会从父线程中继承一个,若是在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
看下ServiceLoader的相关源码:

//class : ServiceLoader
public static <S> ServiceLoader<S> load(Class<S> service) {
    //ServiceLoader就是经过Thread.currentThread().getContextClassLoader()获取类加载器的
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

因此解决DriverManager类中能够加载OracleDriver的问题,能够经过将应用程序类加载器(ApplicationClassLoader)设置到java.lang.Thread类的setContextClassLoaser()方法来解决。
其实这一过程咱们基本不用本身来敲代码实现,由于咱们用的容器都已经帮咱们考虑到了。以tomcat(9.0.24)的源码为例:

//class : WebappLoader
@Override
public void backgroundProcess() {
        if (reloadable && modified()) {
            try {
                Thread.currentThread().setContextClassLoader
                    (WebappLoader.class.getClassLoader());
                if (context != null) {
                    context.reload();
                }
            } finally {
                if (context != null && context.getLoader() != null) {
                    Thread.currentThread().setContextClassLoader
                        (context.getLoader().getClassLoader());
                }
            }
        }
  }

六、再来聊聊Spring中的ClassLoader

咱们定义的JavaBean在spring的getBean方法的建立过程其实与DriverManager建立Driver实例的过程是同样的。咱们的JavaBean是通常都是由应用程序类加载器(ApplicationClassLoader)或自定义类加载器负责加载,而Spring作为一款开源框架多是有更高层类加载器负责加载,因此Spring获取JavaBean的Class时第一优先级是经过Thread.currentThread().getContextClassLoader()来获取JavaBean的Class的类加载器。如代码所示:

//org.springframework.util.ClassUtils
public static ClassLoader getDefaultClassLoader() {
    ClassLoader cl = null;

    try {
        cl = Thread.currentThread().getContextClassLoader();
    } catch (Throwable var3) {
    }

    if (cl == null) {
        cl = ClassUtils.class.getClassLoader();
        if (cl == null) {
            try {
                cl = ClassLoader.getSystemClassLoader();
            } catch (Throwable var2) {
            }
        }
    }

    return cl;
}

更多spring源码相关知识点击
《超哥spring源码解析之核心容器篇》免费视频学习
也能够关注超哥微信公众号:
超哥spring源码解析.jpg

相关文章
相关标签/搜索