类加载机制深入

本文深入探讨了Java类加载机制,包括Tomcat的类加载器架构,如何破坏双亲委派模型,如SPI和OSGI,以及字节码生成技术的应用。详细介绍了类加载的实操场景,如自定义类加载器的实现,类加载的多版本共存问题,以及通过SPI服务发现机制加载第三方类。内容涵盖类加载的各个层面,包括热替换、线程上下文类加载器的使用以及代码动态加载等。
摘要由CSDN通过智能技术生成

Tomcat-正统的类加载器架构

  • 对于一个web服务器如tomcat 需要实现如下功能:

  • 1 不同web应用所使用的类库需要相互隔离,不能互相影响,比如webapp1使用spring3.1依赖库,webapp2使用了spring3.2的依赖库,那么这两个应用依赖类库必须隔离。

  • 2 不同web应用所使用的基础类库可以共享,比如两个webapp都要使用java.lang的类库,那么需要进行共享,如果每个web应用都加载一份基础类库太浪费内存空间。

  • 3 web应用程序的类库不能影响到tomcat本身的代码。tomcat本身的代码类库也不能影响到web应用程序的类库。

  • 4 支持热替换,比如修改jsp文件代码,不需要重启应用就可以在浏览器看到变化。

  • 为了支持以上功能,单独一个classpath路径无法满足要求,需要提供多个classpath供用户存放第三方类库。每一个classpath路径会对应一个自定义类加载器去加载放置在里面的类库。

  • tomcat的存放类库的目录有
    /common目录:tomcat服务器和web应用共同使用的类库。
    /server目录:只能由tomcat服务器使用,对于web应用不可见。
    /shared目录:被所有web应用程序共同使用,对tomcat服务器不可见。
    /webapp/web-info:当前web应用私有,对tomcat和其他web应用不可见。
    为了支持这套规则,tomcat自定义了很多类加载器,这些类加载器使用经典双亲委派模型来实现。

  • tomcat用到的类加载器以及委派关系如下
    在这里插入图片描述

  • 前三个是JDK默认的类加载器,后面几个是jdk自定义的类加载器。

  • common类加载器用来加载/common目录的类,cataline类加载器用来加载/server目录的类,shared类加载器用来加载/shared目录的类,webapp类加载器用来加载/webapp/web-info目录的类。除此之外,每一个jsp文件对应一个jsp类加载器。

  • 根据这种委派关系,common类加载器加载的类库可以被整个tomcat环境使用,cataline类加载器对应类库只能被tomcat程序本身使用,share类加载器对应类库可以被所有web应用访问,webapp类加载器是每个web应用对应一个,可以被当前web应用访问。每个jsp类加载器只能对应一个jsp文件,可以实现jsp文件热替换。

  • tomcat默认没有实现cataline和share两个目录,即只有 common — webapp — jsp 这个委派流程,如果需要更复杂委派流程,可以修改配置文件,使用更复杂的委派机制。

  • 通过这种机制,如果common类库的代码需要访问webapp目录类库代码,可以使用线程上下文类加载器来进行类加载访问。

  • 但是在tomcat7好像破坏了双亲委派机制,先加载webapp的类库,再加载tomcat的类库,tomcat往上的类库还是双亲委派。
    另外springboot对类加载行为也有一些变化,总之在web应用中的线程上下文类加载器还是webappclassloader。

破坏双亲委派模型的情况1 SPI机制

  • 根据双亲委派模型,只能用户类调用核心类库如果反向调用就会出现错误,比如核心类库的类a调用classpath目录下的类b时,由于类a是由启动类加载器加载的,那么根据托管机制类a调用类b时类b只能由启动类加载器进行加载,但是启动类加载器在lib目录下无法加载到类b就会报错。
  • 但是在一些情况下可能出现核心类库代码调用用户自定义类的情况,此时必须要破坏双亲委派模型。比如SPI机制,在该机制下模块的接口类在核心类库编写,而模块的实现类放在用户的classpath目录下,那么就必然存在核心类库的接口类调用用户目录的实现类这种情况。
  • 比如接口类Persion在核心类库,实现类Persionimpl在用户类库,那么当我们在核心类库代码中加载Persionimpl类时就会出现问题,因为用户类库对于核心类库是不可见的,核心类库委托启动类加载器无法加载用户类库的实现类。为了解决这个问题,jvm提供了一个工具类ServiceLoader,根据这个工具类就可以实现从接口类在用户目录寻找实现类的功能,但是ServiceLoader类本身也是核心类库,仅仅在ServiceLoader类中通过反射加载Persionimpl也是无法加载到的。其实ServiceLoader类内部使用的是线程上下文类加载器来加载实现类的。如下,如果没有传入自定义类加载器就会默认使用线程上下文类加载器。
public static <S> ServiceLoader<S> load(Class<S> service) {
   
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
  • 线程上下文类加载器可以通过Thread类的一个方法进行设置,如果当前线程没有设置就会继承父线程的线程上下文类加载器,如果全局环境都没有设置,那么线程上下文类加载器默认就是应用程序类加载器。这样我们就可以在高层类获取线程上下文类加载器来加载用户类,相当于启动类加载器向下委托线程上下文类加载器来加载用户类。这样就破坏了双亲委派机制向上委托的原则。
  • 可以看出线程上下文类加载器的本质是,一个类加载器对象可以在一个线程环境中共享,这样只要在一个线程环境中,无论是哪个层级的类库都可以访问到线程上下文类加载器,我们也可以把自己定义的类加载器设置为线程上下文类加载器。
    ServiceLoader通过线程上下文类加载器可以访问用户类库了,但是ServiceLoader怎么知道一个接口类的实现类的类名是什么呢?其实这里使用了一个配置约定,默认ServiceLoader会从classpath目录的 META-INFO/services目录寻找接口和实现类的对应关系配置,通过这个配置文件就知道应该加载接口的哪些实现类。
public final class ServiceLoader<S> implements Iterable<S>
    //配置文件的路径
    private static final String PREFIX = "META-INF/services/";
    //加载的服务类或接口
    private final Class<S> service;
    //已加载的服务类集合
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    //类加载器
    private final ClassLoader loader;
    //内部类,真正加载服务类
    private LazyIterator lookupIterator;
}
  • 总之高层实例化底层的类时,必须通过线程上下文类加载器以及接口-实现类配置文件帮助高层的Classloader找到并加载该类。

  • 典型应用就是JDBC机制,DirverManager属于核心类由启动类加载器加载,具体的mysql连接jar包属于用户类存放在用户类库,由应用程序类加载器加载。mysql连接类会实现Driver接口创建 com.mysql.jdbc.Driver 实现类。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
   
    //...
}
  • 在mysql连接jar包中有 META-INF/services目录配置了 java.sql.Driver接口 和 com.mysql.jdbc.Driver实现类对应关系
  • 获取mysql连接对象的代码如下
// Class.forName("com.mysql.jdbc.Driver");
Connection connection  = DriverManager.getConnection(
        "jdbc:mysql://localhost:3306/test?&useSSL=false&characterEncoding=utf-8",
        "root",
        "123456");
  • 在DriverManager类的静态代码块调用了loadInitialDrivers方法
static {
   
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}
  • 在loadInitialDrivers方法中调用了ServiceLoader类实现了SPI机制进行服务实现类加载。
private static void loadInitialDrivers() {
   
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
   
        public Void run() {
   
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
           while(driversIterator.hasNext()) {
   
                driversIterator.next();
            }

        }
    });
}
  • 上一步已经找到了MySQL中的com.mysql.cj.jdbc.Driver全限定类名,当调用next方法时,就会创建这个类的实例。它就完成了一件事,向DriverManager注册自身的实例。
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
   
    static {
   
        try {
   
            //注册
            //调用DriverManager类的注册方法
            //往registeredDrivers集合中加入实例
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
   
            throw new RuntimeException("Can't register driver!");
        }
    }
    public Driver() throws SQLException {
   
        // Required for Class.forName().newInstance()
    }
}
  • 紧接着就可以创建connect连接对象进行使用了。
  • 整体流程就是DriverManager类调用ServiceLoader类,ServiceLoader类内部使用了线程上下文类加载器加载用户目录Driver实现类进行使用。
  • 其实只要自己遵循JDBC规范和SPI规范,自己也可以实现出类似的功能。
  • 既然这些jdbc驱动jar包扮演核心类的功能,那干嘛不直接把它放在核心类库中,而是要作为用户代码存在呢?我们可以想想,jdbc只是一个接口,它的实现类有很多很多,每种jdbc又有不同的版本,如果放在核心类库太麻烦了,每一次变动都要修改核心类库的代码。

破坏双亲委派模型的情况2 OSGI的类加载架构

  • OSGI是一个基于Java的动态模块化规范,可以进行模块热替换,热部署,比如程序升级时不需要重启整个应用,只需要替换指定模块代码即可。基于OSGI标准的实现技术有 jBoss。

  • 在程序运行期间当我们对一个模块的代码进行热替换时,相当于要把旧的jar包替换为新的jar包,但是由于类加载器的缓存机制,即使换了新的jar包也不会生效,因为程序已经把老的jar包的类缓存了。因此,为了实现热替换,我们必须为每一个模块绑定一个自定义类加载器,再每一次替换模块代码时把类加载器也替换掉,这样新的jar包所有代码才会被系统重新加载。

  • OSGI标准为了实现模块之间的灵活依赖与调用,把原来树状的类加载委派关系改进成为了网状的类加载委派关系,与SPI机制通过线程上下文类加载器打破双亲委派不同,它直接从代码层面打破了双亲委派模型。

  • 比如有 A B C 三个模块,模块B依赖 模块 A C 和基础类库,模块C依赖了模块A。那么这三个模块类加载委派关系如下
    在这里插入图片描述

  • OSGI的类加载规则如下
    在这里插入图片描述

  • 但是OSGI标准也为程序带来了更大的复杂度,可能产生线程死锁和内存泄漏的风险。我们知道进行类加载时,为了防止多个线程同时加载一个类,会对类进行加锁。

  • 如果模块A依赖模块B,而模块B又依赖模块A,那么当a加载b同时b加载a时,很容易产生死锁问题,a的类加载器加载b的类时首先会将a的类加载器锁定,然后委派b的类加载器会去锁定b的类加载器,此时b的类加载器刚好加载a的类,那么会出现两个线程相互持有锁,互相等待对方释放锁的死锁情况。

字节码生成技术

  • 字节码生成技术就是在程序运行时手动生成class二进制字节码,比如 CGlib,web服务器中jsp编译器,动态代理技术等。
  • 动态代理的优势不在于省去了编写代理类那一点工作量,而是实现了可以在原始类和接口还未知的时候,就确定了代理类的代理行为。将原始类和代理类脱离直接联系就可以灵活应用于不同应用场景中。

类加载机制实操

  • 从指定目录加载class文件
URLClassLoader myClassLoader1 = new URLClassLoader(new URL[] {
    new URL("file:D:/test1/") });
Class<?> myClass1 = myClassLoader1.loadClass("com.demo.UDF");
System.out.println(myClass1.newInstance());
  • 从指定jar包加载class文件
URLClassLoader myClassLoader1 = new URLClassLoader(new URL[] {
    new URL("file:D:/test/1.jar") });
Class<?> myClass1 = myClassLoader1.loadClass("com.demo.Persion");
  • 如果用同一个类加载器(必须是自定义类加载器这一层级)加载两次persion,实际只会加载一次。
URLClassLoader myClassLoader1 = new URLClassLoader(new URL[] {
    new URL(
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值