对于类加载的一点理解


当我们写好一个Java程序之后,不是管是CS还是BS应用,都是由若干个.class文件组织而成的一个完整的Java应用程序,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不同的class文件当中,所以经常要从这个class文件中要调用另外一个class文件中的方法,如果另外一个文件不存在的,则会引发系统异常。而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的。

Java默认提供的三个ClassLoader

  • BootStrap ClassLoader:称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等。即 JAVA_HOME/lib目录下的的核心API或-Xbootclasspath选项指定
  • Extension ClassLoader:称为扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar。
  • App ClassLoader:也叫System class loader(系统类加载器),负责加载应用程序classpath目录下的所有jar和class文件。

每个类加载器都有直接的名字空间,对于同一个类加载器实例来说,名字相同的类只能存在一个,并且仅仅加载一次。不管该类有没有变化,下次再需要加载时候,它只是从自己的缓存中直接返回已经加载过的类引用。我们自己编写的类默认情况下都是通过System ClassLoader进行加载的。当我们使用new关键字或者class.forName来加载类时,所有加载的类则调用SystemClassLoader进行加载。

ClassLoader加载类的原理

ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。

为什么需要三个类加载器?

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

判断两个class对象是否是同一个的两个必要条件

  1. 类的完整类名是否一致,包括包名
  2. 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同

换句话说,在JVM中,即使两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不一样,那么这两个对象也是不相同的

怎么打破双亲委派?

  1. 自定义写一个继承ClassLoader 的类加载器
  2. 重写loadClass方法
  3. 重写findClass方法方法

这里最主要的是重写loadclass方法,由于双亲委派机制的实现都是经过这个方法实现的,先找附加在其进行加载,若是父加载器没法加载再由本身来进行加载,源码里会直接找到根加载器,重写了这个方法之后就能本身定义加载的方式了

未破坏双亲委派机制之前

Java有自己的一套 资源管理服务JNDI 等等,是由启动类加载器加载的,说明类是放在rt.jar包中。

双亲委派机制:当前类加载器收到类加载的请求后,先不自己尝试加载类,而是先将请求委派给父类加载器,调用父类的 loadClass() 方法,这是一个递归的过程(因此,所有的类加载请求,都会先被传送到启动类加载器),只有当父类加载器加载失败时,当前类加载器才会尝试自己去自己负责的区域加载。

所以判断是否破坏双亲委派机制的一个重要指标是类加载的请求顺序

java.sql.Driver这个东西。JDK只能提供一个规范接口,而不能提供实现。提供实现的是实际的数据库提供商。提供商的库总不能放JDK目录里吧。

Java从1.6搞出了SPI就是为了优雅的解决这类问题——JDK提供接口,供应商提供服务。编程人员编码时面向接口编程,然后JDK能够自动找到合适的实现,岂不是很爽?

但是便利的同时也带来了困扰。提供商提供的类不能放JDK里的lib目录下,所以也就没法用BootstrapClassLoader加载了。所以当你代码写了

Class clz = Class.forName("java.sql.Driver");
Driver d = (Driver)clz.newInstance();

这个代码会用Bootstrap ClassLoader尝试去加载,问题是java.sql.Driver是个接口,无法真的实例化,就报错了。在没有SPI时,你可以现在classpath里加一个mysql-connector-java.jar包,然后这样写

Class clz = Class.forName("com.mysql.jdbc.Driver");
Driver d = (Driver) clz.newInstance();

这就没问题了,这里用了Application Classloader加载了mysql-connector-java.jarcom.mysql.jdbc.Driver。问题是你硬编码了,一定要加载”com.mysql.jdbc.Driver”,不是很优雅,不能实现“用接口编程,自动实例化真的实现“的这种编码形式。为了提高代码的优雅灵活性,在高版本中引入了SPI机制,同时也打破了双亲委派模型

JDBC破坏双亲委派机制

JDBC4.0之后使用spi机制才会破坏双亲委派机制,使用 SPI 在META-INF/services/java.sql.Driver寻找实现类 com.mysql.cj.jdbc.Driver,如下图

所以我们就不需要自己注册驱动了(需要 SPI 服务帮我们注册),连接的过程变为了

 String url = "jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=CONVERT_TO_NULL";
 String username = "root";
 String password = "root";

 Connection connection = DriverManager.getConnection(url, username, password);

然而, DriverManager 是位于rt.jar包中的,类加载器是启动类加载器,com.mysql.jdbc.Driver肯定不在 <JAVA_HOME>/lib,那就无法加载MySQL的Driver,我们可以用应用程序类加载器来加载。所以java开发者的设计是,添加一个线程上下文类加载器(Thread Context ClassLoader),在启动类加载器中获取应用程序类加载器。 Thread.setContextClassLoaser() 设置线程上下文类加载器,如果创建线程的时候没有设置,会从父类继承一个,默认应用程序类加载器。

线程上下文类加载器是让父类加载器请求子类加载器完成类的加载,打破了双亲委派机制。具体过程,在 DriverManager 的静态代码块对 Driver进行加载:

 public class DriverManager {
     static {
         loadInitialDrivers();
         println("JDBC DriverManager initialized");
     }

     private static void loadInitialDrivers() {

         //..

         AccessController.doPrivileged(new PrivilegedAction<Void>() {
             public Void run() {

                 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                 Iterator<Driver> driversIterator = loadedDrivers.iterator();

                 try{
                     while(driversIterator.hasNext()) {
                         driversIterator.next();
                     }
                 } catch(Throwable t) {
                 // Do nothing
                 }
                 return null;
             }
         });

         //..
     }
 }

其中最重要的是SPI 服务,ServiceLoader 查找META-INF/services/java.sql.Driver文件 ,在下面用driversIterator遍历的时候就会发现文件中的内容,例如,MySQL中的就是一行字符串 com.mysql.cj.jdbc.Driver。点进ServiceLoader.load()方法查看

     public static <S> ServiceLoader<S> load(Class<S> service) {
         ClassLoader cl = Thread.currentThread().getContextClassLoader();
         return ServiceLoader.load(service, cl);
     }

     public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
         ClassLoader cl = ClassLoader.getSystemClassLoader();
         ClassLoader prev = null;
         // 如果创建线程的时候没有设置,会使用父类加载器,上面已经设置,那么这里的类加载器就是线程上下文类加载器
         while (cl != null) {
             prev = cl;
             cl = cl.getParent();
         }
         return ServiceLoader.load(service, prev);
     }

出现了我们前面说的内容,线程上下文类加载器,说明不符合双亲委派机制了,所以ServiceLoader拿到了 拿到了线程上下文类加载器。接着看源码,ServiceLoader 自己实现了 Iterator ,叫 LazyIterator 。

 private class LazyIterator
         implements Iterator<S>
 {
     //..
     private boolean hasNextService() {
         //..
     }

     private S nextService() {
         if (!hasNextService())
             throw new NoSuchElementException();
         // META-INF/services/java.sql.Driver文件中的内容,即 `com.mysql.cj.jdbc.Driver`
         String cn = nextName;
         nextName = null;
         Class<?> c = null;
         try {
             // 使用线程上下文类加载器加载 实现类(com.mysql.cj.jdbc.Driver)
             c = Class.forName(cn, false, loader);
         } catch (ClassNotFoundException x) {
             fail(service,
                  "Provider " + cn + " not found");
         }
         //..
     }
     // hasNext()..调用hasNextService()

     // next()..调用nextService()

 }

Tomcat类加载体系结构

Tomcat同样破坏了双亲委派模型,那么Tomcat是如何完成多个web应用之间相互隔离,又如何保证多个web应用都能加载到基础类库

Tomcat本身也是一个java项目,因此其也需要被JDK的类加载机制加载,也就必然存在引导类加载器、扩展类加载器和应用(系统)类加载器。Tomcat自身定义的类加载器主要由图中下半部分组成,Common ClassLoader作为Catalina ClassLoaderShared ClassLoader的parent,而Shared ClassLoader又可能存在多个children类加载器WebApp ClassLoader,一个WebApp ClassLoader实际上就对应一个Web应用,那Web应用就有可能存在Jsp页面,这些Jsp页面最终会转成class类被加载,因此也需要一个Jsp的类加载器,就是图中的JasperLoder
需要注意的是,在代码层面Catalina ClassLoaderShared ClassLoaderCommon ClassLoader对应的实体类实际上都是URLClassLoader或者SecureClassLoader,一般我们只是根据加载内容的不同和加载父子顺序的关系,在逻辑上划分为这三个类加载器;而WebApp ClassLoaderJasperLoader都是存在对应的类加载器类的

通过磁盘/网络动态加载类

动态加载jar包,在实际开发中经常会需要用到,尤其涉及平台和业务的关系的时候,业务逻辑部分可以独立出去交给业务方管理,业务方只需要提供jar包,就能在平台上运行。

创建一个maven工程,编写一个测试类,然后通过maven打包生成jar包,测试类如下

public class RemoteTest {


    public void sayHello(String say){
        JSONObject object = new JSONObject();
        object.put("name","test");
        System.out.println("随便说说:"+object.toJSONString());
    }
}

生成一个jar包 test-1.0-SNAPSHOT.jar,放在我本地的 F:\test\test-1.0-SNAPSHOT.jar 目录。然后再创建另一个工程,在此工程中加载这个本地的jar包,代码如下。也可通过网络请求加载远程的包,这样就实现了动态加载类

public class MyRemoteTest {

    public final static ClassLoader getMyClassLoader(String path) {
        try {
            ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
            URL url = new URL(path);
            ClassLoader custom =
                    new URLClassLoader( new URL[] { url }, systemClassLoader );
            return custom;
        } catch (Exception exp) {
            exp.printStackTrace();
        }
        return null;
    }

    public final static ClassLoader getNetClassLoader(String url) {
        try {


            ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
            URI uri = new URI(url);
            ClassLoader custom =
                    new URLClassLoader( new URL[] { uri.toURL() }, systemClassLoader );
            return custom;
        } catch (Exception exp) {
            exp.printStackTrace();
        }
        return null;
    }


    public static void main(String[] args) throws Exception {
        // 加载远程服务的包
       //  ClassLoader classLoader = MyRemoteTest.getMyClassLoader("http:/127.0.0.1/test/test-1.0-SNAPSHOT.jar");

        // 加载本地磁盘上的包
        ClassLoader classLoader = MyRemoteTest.getMyClassLoader("file:///F:/test/test-1.0-SNAPSHOT.jar");
        //加载类
        //Class<?> userService =  Class.forName("com.mark.RemoteTest");

        Class< ? > userService = classLoader.loadClass( "com.mark.RemoteTest" );

        //获取方法
        Method printMethod = userService.getMethod("sayHello",String.class);
        //调用
        printMethod.invoke(userService.newInstance()," lisi ");

    }
}

Author: 顺坚
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source 顺坚 !
评论
 Previous
Java的反射与动态编译 Java的反射与动态编译
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。这种技术在一些开源框架中使用非常广泛,
2022-06-05
Next 
LeetCode-最长公共子序列 LeetCode-最长公共子序列
力扣上一道中等题,求两个字符串的最长公共子序列。说实话这道题我完全没有思路,一开始尝试用双指针去遍历两个数组,但是公共子序列可以是不连续的,这样情况就会变得很复杂。在和力扣度过美好的一天后,我最终放弃了。看完题解后,茅塞顿开。故此记录一下思
2022-05-31
  TOC