类加载器与双亲委派模型

JVM类加载笔记之二

Posted by     "Eric" on Tuesday, December 10, 2019

在类加载过程的加载阶段,“通过一个类的全限定名来获取描述此类的二进制字节流”是通过“类加载器”来实现的。类加载器虽然只用于实现类的加载动作,但在Java程序中起到的作用不限于类加载阶段。比较连个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。 在JDK1.8中有四种类加载器,如下图所示

3JTeuF.png

注:启动类加载器是用c++编写的,无法查看源码;扩展的类加载器只能加载jar文件包

一、双亲委派模式

所谓双亲委派,就是指用类加载器的loadClass方法时查找类的规则:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中(Bootstrap),只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。 使用双亲委派模型来组织类加载器之间的关系,使Java类随它的类加载器一起具备了带有优先级的层次关系。(例如java.lang.Object,无论哪一个类加载器加载这个类,最终都是由启动类加载器进行加载,因此Object类在程序的各种类加载环境中都是同一个类)

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 {
                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
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

二、线程上下文类加载器

双亲委派很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API,但世事往往没有绝对的完美,如果基础类又要调用回用户的代码,那该怎么办?而且这种情况并不少见,其经常发生在SPI中。SPI是一种服务发现机制,它通过ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。(关于SPI,详见这篇博文

我们在使用JDBC时,都需要加载Driver驱动,但是如果不写

Class.forName("com.mysql.jdbc.Driver")

也可以让com.mysql.jdbc.Driver正确加载,这是如何做到的呢?我们追踪一下源码

public class DriverManager{
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
    
    static{
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
}

显然,DriverManager在JAVA_HOME/jre/lib目录下,所以加载器是Bootstrap ClassLoader,但是在该目录下显然没有mysql-connector-5.1.47.jar包,在DriverManager的静态代码块中,怎么能正确加载com.mysql.jdbc.Driver呢,继续看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;
    }
    //1)使用ServiceLoader机制加载驱动,即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;
		}
    });
	println("DriverManager.initialize: jdbc.drivers = " + drivers);
    
	// 2)使用 jdbc.drivers 定义的驱动名加载驱动
    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);
			// 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器
			Class.forName(aDriver, true,ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
			println("DriverManager.Initialize: load failed: " + ex);
		}
	}
}

先看2) 它最后使用Class.forName完成类的加载和初始化,关联的是应用程序类加载器,因此可以顺利完成类加载

再看1)它是大名鼎鼎的Service Provider interface(SPI),约定如下,在jar包的META-INF/services包下,以接口全限定名为文件,文件内容是实现类名称

3JTsv8.png

这样可以使用

ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
while(iter.hasNext()) {
	iter.next();
}

来得到实现类,体现面向接口编程+解耦的思想。

接着看ServiceLoader.load方法:

public static <S> ServiceLoader<S> load(Class<S> service){
    //获取线程上下文类加载器
    ClassLoader c1=Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service,c1);
}

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器。换句话说,一个由Bootstrap ClassLoader加载的代码中包含有需要下层类加载加载的对象。使用java.lang.Thread类的getContextClassLoader()方法调用应用程序类加载器。

三、自定义类加载器

为什么要用?

  • 想加载非classpath随意路径中的类文件
  • 都是通过接口来使用实现,希望解耦时,常用在框架设计
  • 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于tomcat容器 步骤:
    • 继承ClassLoader父类
    • 重写findClass方法(如果重写loadClass会破坏双亲委派机制)
    • 读取类文件的字节码
    • 调用父类的defineClass方法加载类
    • 使用者调用该类加载器的loadClass方法
class MyClassLoader extends ClassLoader {
    @Override // name 就是类名称
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String path = "e:\\myclasspath\\" + name + ".class";

        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            Files.copy(Paths.get(path), os);

            // 得到字节数组
            byte[] bytes = os.toByteArray();

            // byte[] -> *.class
            return defineClass(name, bytes, 0, bytes.length);

        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException("类文件未找到", e);
        }
    }
}