聊聊ClassLoader

Posted superyu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了聊聊ClassLoader相关的知识,希望对你有一定的参考价值。

1、什么是类加载器

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

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

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));
}

3、类加载器的分类

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

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

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

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;
    }

检查与加载过程如图所示:
技术图片

5、双亲委托模式的弊端

要说明弊端,必须引入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必须增加配置文件:技术图片
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());
                }
            }
        }
  }

6、再来聊聊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源码解析之核心容器篇》免费视频学习
也可以关注超哥微信公众号:
技术图片

以上是关于聊聊ClassLoader的主要内容,如果未能解决你的问题,请参考以下文章

从Java的类加载机制谈起:聊聊Java中如何实现热部署(热加载)

从Java的类加载机制谈起:聊聊Java中如何实现热部署(热加载)

聊聊类加载器与双亲委派模型

spring-boot-devtools 快速重启的秘密!#yyds干货盘点#

聊聊视频播放那些事1

java反射中,Class.forName和classloader的区别(代码说话)