Java编译原理--类加载器

Java语言在刚刚诞生的时候提出过一句著名的口号“一次编写,到处运行”,这句话充分的表达了开发人员对于冲破平台界限的渴望,也解释了Java语言跟平台无关的设定。

一、概述

类加载过程包括加载、连接和初始化,连接又可以细化为验证、准备和解析,除了加载过程可以由程序自定义处理外,其他的过程都是由虚拟机自动处理的;在这个过程中,类是如何被加载到内存中的呢?加载的时候需要加载哪些类呢?这就是本文要讨论的内容了--类加载器。

根据虚拟机规范,类加载器的作用包括根据类的全限定名找到定义此类的二进制字节流;将字节流代表的静态存储类型转化为方法区运行时数据结构;在方法区生成class对象,作为方法区类的入口。虚拟机规范只是规定了整个加载的过程,但是没有规定具体从什么文件中加载字节流,也没有规定如何加载,什么时候加载等等,每个类加载器可以根据实际情况具体实现。

 二、类与类加载器

类加载器虽然只是用于类的加载动作,但是类加载器在程序中的作用远不止加载过程。在Java语言中,如果要定义一个类,不仅需要类本身信息,还需要有类加载器的信息,比如我们要判断两个类是否相等,不仅要判断这两个类是否来源于同一个class文件,还要判断是否是同一个类加载器加载的,因为每个类加载器都会有一个独立的命名空间,不同的类加载器加载的类也是不同的。

上文所指的"相等",不仅包括Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象的关系所属判定等,如果没有注意类加载器的影响,那么结果可能会是错误的。举例说明:

package com.jvm.classloader;

import java.io.IOException;
import java.io.InputStream;

public class ClassLoaderTest {

	public static void main(String[] args) throws Exception {
		ClassLoader classLoader = new ClassLoader() {
			@Override
			public Class<?> loadClass(String name) throws ClassNotFoundException {
				try {
					String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
					InputStream is = getClass().getResourceAsStream(fileName);
					if (is == null) {
						return super.loadClass(name);
					}
					byte[] b = new byte[is.available()];
					is.read(b);
					return defineClass(name, b, 0, b.length);
				} catch (IOException e) {
					throw new ClassNotFoundException(name);
				}
			}
		};
		Object o = classLoader.loadClass("com.jvm.classloader.ClassLoaderTest").newInstance();
		System.out.println(o.getClass());
		System.out.println(o instanceof com.jvm.classloader.ClassLoaderTest);
	}
}

输出结果:

上述代码构造了一个简单的类加载器,它可以加载统一路径下的class文件,我们使用这个类加载器加载类,然后将这个类实例化,打印这个类,可以看到确实是目前所在类,但是判断这个类和class对象关系,却发现是false,这是为什么呢?因为main方法再启动的时候已经使用了类加载器了,这个类加载器是应用类加载器,它已经将class文件加载进虚拟机,自定义类加载器生成的类跟应用类加载器生成的类是不相等的,所以返回false。

三、类加载机制

 1、类加载器分类

从Java虚拟机的角度来考虑,类加载器总共有两种,一种是启动类加载器,这个类加载器用C++实现,是虚拟机自身的一部分;另一种是其他类加载器,这些类加载器由Java语言实现,独立于虚拟机,并且全部继承自抽象泪java.lang.ClassLoader。

从开发人员的角度来考虑,Java虚拟机还可以划分的更加详细,Java虚拟机在功能上可以分为:启动类加载器、扩展类加载器、应用类加载器、自定义类加载器。

启动类加载器负责将存放在jre/lib目录中的jar加载到虚拟机中,或者被-Xbootclasspath参数所指定的路径中的jar,并且这些jar是需要被虚拟机识别的(按照文件名识别,例如rt.jar,名字不一致不会被加载)。启动类加载器无法被程序直接引用,用户自定义类加载器如果需要引用启动类加载器,那么直接将父类加载器设置为null即可。

扩展类加载器负责将存放在jre/lib/ext目录下的jar加载到虚拟机中,或者被java.ext.dirs系统变量所指定的目录下的路径中的类库,开发者可以直接使用这个类加载器。

应用类加载器负责将classpath路径下的类库,开发者可以直接使用这个类加载器,这个类加载器也是用户程序默认的类加载器。这个类加载器是ClassLoader的getSystemClassLoader()方法返回的类加载器,所以也被称为系统类加载器。

Java虚拟机提供了这么多的类加载器,我们在程序中改怎么选择类加载器呢?如果一个类被多个类加载器加载如何处理呢?如何保证一个类在程序中只能被一个类加载器加载一次呢?这个问题就是虚拟机的类加载机制--双亲委派模型。

2、双亲委派模型

Java虚拟机的类加载采用父类委托机制,这种类加载器之间的层次关系,称为双亲委派模型。双亲委派模型要求除了顶层的类加载器之外,其他的类加载器都要有父类加载器,这里的类加载器的父子关系不是普通Java语言的继承(Inheritance)关系,而是通过组合(Composition)关系实现的,双亲委派模型如下图所示:


    双亲委派模型的工作机制:
    1) 如果一个类加载器接到了加载类的请求,它首先不会自己去尝试加载这个类,而是将加载类的请求委托给父类加载器,父类加载器再次向上委托,一直委托到启动类加载器;
    2) 如果启动类加载器可以加载这个类,那么加载结束;如果启动类加载器不能加载这个类,则将加载请求转交给子类加载器,也就是扩展类加载器;
    3) 如果一直到最底层的类加载器依然没有将这个类加载到内存,则抛出ClassNotFoundException异常,加载过程结束。

3、双亲委派模型的好处

使用双亲委派模型来组织加载类,有着显而易见的好处,上文说过,Java类的确定需要类本身信息和类加载器的信息,双亲委派模型可以让类具有了一种优先级的层次关系,例如object这个类,他存放在rt.jar包中,不管哪个类加载器要加载这个类,都需要向上委托,一直委托到启动类加载器,由启动类加载器加载,因此,在所有程序中,这个类总是唯一的。如果不适用双亲委派模型,各个类加载器都可以任意加载类的话,那么用户就可以自定义一个java.lang.object类和一个类加载器,这时候加载到虚拟机的object会有多个,并且各不相同,Java类型体系就无法保证准确性,应用程序就会出现不可预知的错误。

双亲委派模型对于类加载过程和虚拟机来说非常重要,但是它的实现却很简单,Java虚拟机为我们提供了一个抽象类java.lang.ClassLoader,这个类定义了类加载器的基本方法,其中loadClass()方法就是类加载器的入口,这个入口实现了类加载器的双亲委派过程,代码如下:

protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
  {
      synchronized (getClassLoadingLock(name)) {
          // 先从缓存查找该class对象,找到就不用重新加载
          Class<?> c = findLoadedClass(name);
          if (c == null) {
              long t0 = System.nanoTime();
              try {
                  if (parent != null) {
                      //如果找不到,则委托给父类加载器去加载
                      c = parent.loadClass(name, false);
                  } else {
                  //如果没有父类,则委托给启动加载器去加载
                      c = findBootstrapClassOrNull(name);
                  }
              } catch (ClassNotFoundException e) {
                  // ClassNotFoundException thrown if class not found
                  // from the non-null parent class loader
              }

              if (c == null) {
                  // If still not found, then invoke findClass in order
                  // 如果都没有找到,则通过自定义实现的findClass去查找并加载
                  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) 首先检查要加载的类是否已在内存中存在,如果已存在,即已经加载过则不再重复加载;
    2) 如果没有加载过则判断当前的类加载器是否有父类加载器,如果有父类加载器,则尝试使用父类加载器加载,如果没有父类加载器,则尝试使用启动类加载器加载;
    3) 如果父类加载器和启动类加载器都没有加载成功,则尝试使用此类加载器进行加载;
    4) 如果需要进行解析,则进行类解析,加载过程结束。

四、Tomcat类加载器

Tomcat目录结构中,有四类目录需要类加载器进行加载,分别是"/common/*"、"/server/*"、"/shared/*"、"/WEB-INF/*",这四类目录分别存放Java类库及各种组件等,Java类库存放的含义分别是:
    1) 放置在common目录下的jar包,可以被Tomcat和所有web应用程序共享;
    2) 放置在server目录下的jar包,可以被Tomcat本身使用,但是不能被其他web应用程序使用;
    3) 放置在shared目录下的jar包,可以被web应用程序使用,但是不能被Tomcat本身使用;
    4) 防止在webapp/WEB-INF/目录下的jar包,仅仅可以被当前目录下的web应用程序使用,不能被其他WEB-INF目录下的web应用程序使用。
    为了支持这些目录结构,并且对目录结构中的类进行加载和隔离,Tomcat自定义了多种类加载器,这些类加载器按照经典的双亲委派模型来实现,他们之间的关系如下图所示:
    

上层三个类加载器是Java虚拟机提供的类加载器,这里不再赘述。而CommonClassLoader、CatalinaClassLoader、SharedClassLoad和WebAppClassLoader则是Tomcat提供的类加载器,他们分别加载commmon、server、shared和webapp目录下的类库,其中,WebApp类加载器和JasperLoader会存在多个实例,每个应用程序会生成一个类加载器,每个jsp文件会对应一个类加载器。WebAppClassLoader的作用范围是当前的应用程序,而JasperClassLoader的作用范围仅仅是当前的jsp文件,当服务器检测到有新的jsp文件存在或者当前jsp文件更新时,会重新生成一个JasperClassLoader,这样就可以保证jsp文件更新之后总是可以及时加载到内存中,从而实现文件热部署。
    当应用检测到需要加载某个类时,Tomcat会以以下规则加载类:
    1) 使用启动类加载器尝试进行进行加载,如果加载成功,则返回,加载过程结束;
    2) 如果启动类加载器加载失败,则使用扩展类加载器尝试进行加载,加载成功则返回,加载过程结束;
    3) 如果加载失败,则尝试使用应用类加载器从WEB-INF/classes目录下加载,加载成功则直接返回,加载过程结束;
    4) 如果加载失败,则使用应用类加载器在目录WEB-INF/lib目录下加载,加载成功则直接返回,加载过程结束;
    5) 如果加载使用,则尝试使用Common类加载器在目录CATALINA_HOME/lib目录下加载,加载成功则直接返回,加载过程结束;
    6) 如果加载失败,则抛出异常,加载过程结束。
    Tomcat类加载顺序如下图所示:

基于以上规则,如果想要在web应用程序间共享一个jar包,那么不仅需要将共享的jar包放置在/common/目录下,还需要删除tomcat/lib和WEB-INF/lib目录下的jar包;

也有可能出现以下问题:
    1) 如果用户在CATALINA_HOME/lib和WEB-INF/lib目录中放置了不同版本的jar包,此时可能会导致加载不到类的错误,因为类加载有先后顺序;
    2) 如果多个应用使用了同一个jar包,当在应用中放置多个jar包时,有可能导致多个应用间类加载出现问题。

 五、线程上下文类加载器

双亲委派模型并不是强制性的约束模型,而是Java设计者推荐给开发者的类加载器的实现方式,在Java程序世界里,大部分的类加载器都遵循这个规律,比如Tomcat类加载器,但也有例外,比如下文要提到的线程上下文类加载器。

双亲委派模型非常好的解决了各个类加载器对于基础类的加载问题,基础类之所以称为基础类,是因为他们总是被用户程序调用(例如java.lang.Object),但是也有例外,如果基础类要回调应用类呢?例如JDBC服务,JDBC服务是Java的基础服务,Java只是提供了基本的接口,具体实现需要各个厂商来完成。JDBC接口由启动类加载器来加载,但是它的实现由各个提供商来实现,JDBC的目的就是完成对数据库驱动的发现和管理,所以需要在程序启动时加载这些实现,但是这些实现一定会在用户程序中,也就是会存放在classpath目录下,启动类加载器不能加载这个目录下的类,这怎么办呢?线程上下文类加载器应运而生。

转自CSDN
    作者:小杨Vita
    链接:https://blog.csdn.net/yangcheng33/article/details/52631940
    来源:CSDN
    著作权归作者所有,转载请联系作者获得授权

 

前言

此前我对线程上下文类加载器(ThreadContextClassLoader,下文使用TCCL表示)的理解仅仅局限于下面这段话:

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。

这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System ClassLoader)来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。

而线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。

一直困恼我的问题就是,它是如何打破了双亲委派模型?又是如何逆向使用类加载器了?直到今天看了jdbc的驱动加载过程才茅塞顿开,其实并不复杂,只是一直没去看代码导致理解不够到位。

JDBC案例分析

我们先来看平时是如何使用mysql获取数据库连接的:

// 加载Class到AppClassLoader(系统类加载器),然后注册驱动类
// Class.forName("com.mysql.jdbc.Driver").newInstance(); 
String url = "jdbc:mysql://localhost:3306/testdb";    
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password"); 

上就是mysql注册驱动及获取connection的过程,各位可以发现经常写的Class.forName被注释掉了,但依然可以正常运行,这是为什么呢?这是因为从Java1.6开始自带的jdbc4.0版本已支持SPI服务加载机制,只要mysql的jar包在类路径中,就可以注册mysql驱动。

那到底是在哪一步自动注册了mysql driver的呢?重点就在DriverManager.getConnection()中。我们都是知道调用类的静态方法会初始化该类,进而执行其静态代码块,DriverManager的静态代码块就是:

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

始化方法loadInitialDrivers()的代码如下:

private static void loadInitialDrivers() {
    String drivers;
    try {
        // 先读取系统属性
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }
    // 通过SPI加载驱动类
    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;
        }
    });
    // 继续加载系统属性中的驱动类
    if (drivers == null || drivers.equals("")) {
        return;
    }

    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            // 使用AppClassloader加载
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}

从上面可以看出JDBC中的DriverManager的加载Driver的步骤顺序依次是: 
1. 通过SPI方式,读取 META-INF/services 下文件中的类名,使用TCCL加载; 
2. 通过System.getProperty("jdbc.drivers")获取设置,然后通过系统类加载器加载。 
下面详细分析SPI加载的那段代码。

JDBC中的SPI

先来看看什么是SP机制,引用一段博文中的介绍:

SPI机制简介 
SPI的全名为Service Provider Interface,主要是应用于厂商自定义组件或插件中。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。 
SPI具体约定 
Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader

知道SPI的机制后,我们来看刚才的代码:

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

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

注意driversIterator.next()最终就是调用Class.forName(DriverName, false, loader)方法,也就是最开始我们注释掉的那一句代码。好,那句因SPI而省略的代码现在解释清楚了,那我们继续看给这个方法传的loader是怎么来的。

因为这句Class.forName(DriverName, false, loader)代码所在的类在java.util.ServiceLoader类中,而ServiceLoader.class又加载在BootrapLoader中,因此传给 forName 的 loader 必然不能是BootrapLoader,复习双亲委派加载机制请看:java类加载器不完整分析 。这时候只能使用TCCL了,也就是说把自己加载不了的类加载到TCCL中(通过Thread.currentThread()获取,简直作弊啊!)。上面那篇文章末尾也讲到了TCCL默认使用当前执行的是代码所在应用的系统类加载器AppClassLoader。

再看下看ServiceLoader.load(Class)的代码,的确如此:

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

ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作。

到这儿差不多把SPI机制解释清楚了。直白一点说就是,我(JDK)提供了一种帮你(第三方实现者)加载服务(如数据库驱动、日志库)的便捷方式,只要你遵循约定(把类名写在/META-INF里),那当我启动时我会去扫描所有jar包里符合约定的类名,再调用forName加载,但我的ClassLoader是没法加载的,那就把它加载到当前执行线程的TCCL里,后续你想怎么操作(驱动实现类的static代码块)就是你的事了。

好,刚才说的驱动实现类就是com.mysql.jdbc.Driver.Class,它的静态代码块里头又写了什么呢?是否又用到了TCCL呢?我们继续看下一个例子。

使用TCCL校验实例的归属

com.mysql.jdbc.Driver加载后运行的静态代码块:

static {
    try {
        // Driver已经加载到TCCL中了,此时可以直接实例化
        java.sql.DriverManager.registerDriver(new com.mysql.jdbc.Driver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
}

registerDriver方法将driver实例注册到系统的java.sql.DriverManager类中,其实就是add到它的一个名为registeredDrivers的静态成员CopyOnWriteArrayList中 。

到此驱动注册基本完成,接下来我们回到最开始的那段样例代码:java.sql.DriverManager.getConnection()。它最终调用了以下方法:

private static Connection getConnection(
     String url, java.util.Properties info, Class<?> caller) throws SQLException {
     /* 传入的caller由Reflection.getCallerClass()得到,该方法
      * 可获取到调用本方法的Class类,这儿调用者是java.sql.DriverManager(位于/lib/rt.jar中),
      * 也就是说caller.getClassLoader()本应得到Bootstrap启动类加载器
      * 但是在上篇文章[java类加载器不完整分析]中讲到过启动类加载器无法被程序获取,所以只会得到null
      */
     ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
     synchronized(DriverManager.class) {
         // 此处再次获取线程上下文类加载器,用于后续校验
         if (callerCL == null) {
             callerCL = Thread.currentThread().getContextClassLoader();
         }
     }

     if(url == null) {
         throw new SQLException("The url cannot be null", "08001");
     }

     SQLException reason = null;
     // 遍历注册到registeredDrivers里的Driver类
     for(DriverInfo aDriver : registeredDrivers) {
         // 使用线程上下文类加载器检查Driver类有效性,重点在isDriverAllowed中,方法内容在后面
         if(isDriverAllowed(aDriver.driver, callerCL)) {
             try {
                 println("    trying " + aDriver.driver.getClass().getName());
                 // 调用com.mysql.jdbc.Driver.connect方法获取连接
                 Connection con = aDriver.driver.connect(url, info);
                 if (con != null) {
                     // Success!
                     return (con);
                 }
             } catch (SQLException ex) {
                 if (reason == null) {
                     reason = ex;
                 }
             }

         } else {
             println("    skipping: " + aDriver.getClass().getName());
         }

     }
     throw new SQLException("No suitable driver found for "+ url, "08001");
 }
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
    boolean result = false;
    if(driver != null) {
        Class<?> aClass = null;
        try {
        // 传入的classLoader为调用getConnetction的线程上下文类加载器,从中寻找driver的class对象
            aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
        } catch (Exception ex) {
            result = false;
        }
    // 注意,只有同一个类加载器中的Class使用==比较时才会相等,此处就是校验用户注册Driver时该Driver所属的类加载器与调用时的是否同一个
    // driver.getClass()拿到就是当初执行Class.forName("com.mysql.jdbc.Driver")时的应用AppClassLoader
        result = ( aClass == driver.getClass() ) ? true : false;
    }

    return result;
}

可以看到这儿TCCL的作用主要用于校验存放的driver是否属于调用线程的Classloader。例如在下文中的tomcat里,多个webapp都有自己的Classloader,如果它们都自带 mysql-connect.jar包,那底层Classloader的DriverManager里将注册多个不同类加载器的Driver实例,想要区分只能靠TCCL了。


     

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章