Java类加载器的作用就是在运行时加载类。它通过加载class文件、网络上的字节流或其他来源构造Class对象,用于生成对象在程序中运行。
在平时的程序开发中,我们一般不需要操作类加载,因为Java本身的类加载器机制已经帮我们做了很多事情。但后面很多时候,比如说自己开发框架或者排查问题的时候,我们需要理解类加载的机制和如何按自己的需求去自定义类加载器。类加载器知识就像Java开发的一道门,门内外隔离了开发人员对类加载的使用,而了解类加载器就掌握了这道门的钥匙。
类加载器是一个用来加载类文件的类。Java源代码通过编译器编译后,类加载器加载文件中的字节码来执行程序,字节码的来源也可以来自于网络。Java有3中默认的类加载器:Bootstrap类加载器、Extension类加载器和System类加载器(或者叫做Application类加载器)。每个类加载器都设定好从哪里加载类。
Bootstrap类加载器:JRE/lib/rt.jar中的JDK类文件
Bootstrap类加载器是所有类加载器的父类(非类继承关系)。它的大部分由C来写的,通过代码获取不到,比如调用String.class.getClassLoader(),会返回null。
Extension类加载器:JRE/lib/ext或者java.ext.dirs指向的目录
Extension加载器由 sun.misc.Launcher$ExtClassLoader
实现 System类加载器:CLASSPATH环境变量,由 -classpath
或 -cp
选项定义,或者是JAR中的Manifest的classpath属性定义。 System类加载器由 sun.misc.Launcher$AppClassLoader
实现。
Java的类加载性保证了很好的稳定性和拓展性。类加载器的工作原理基于三个机制
某加载器在尝试加载类的时候,都会委托其父类加载器尝试加载类。比如一个应用要加载CLASSPAH下A.class。加载这个类的请求由System类加载器委托父加载器Extension类加载器,Extension类加载器会委托父加载器Bootstrap类加载器。Bootstrap类加载器先从rt.jar中尝试加载这个类。这个类在rt.jar中不存在,所以加载工作回到Extension类加载器。Extension类加载器会查看jre/lib/ext目录下有没有这个类,如果存在那么这个类将被加载,且只加载一次。如果没找到,加载请求有回到了System类加载器。System类加载器从CLASPATH中查看该类,如果存在则加载这个类,如果没找到,则报java.lang.ClassNotFoundException。
子类加载器可以看到父类加载器加载的类,而反之则不行。
父加载器加载过的类不能被子加载器加载第二次,同一个类加载器实例也只能加载一个类一次。
我们可以使用显示调用的方式或者交由JVM隐式加载一个类。在类加载的过程中,一般有三个概念上的类加载器提供使用。
CurrentClassLoader,称之为当前类加载器
SpecificClassLoader, 称之为指定的类加载器。值得是一个特定的ClassLoader示例
ThreadContextClassLoader,称之为线程上下文类加载器。每个线程都会拥有一个ClassLoader引用,而且可以通过Thread.currentThread().setContextClassLoader(ClassLoader classLoader)进行切换
其中CurrentClassLoader的加载过程是JVM运行时候控制的,非显示调用。
Class.forName和ClassLoader.loadClass都可以用来进行类加载。比如
Class.forName("com.xiaoyi.pandora.vo.A"); Class.forName("com.xiaoyi.pandora.vo.A", true, customClassLoader1); customClassLoader1.loadClass("com.xiaoyi.pandora.vo.A"); 复制代码
Class.forName的方式其实是使用了CurrentClassLoader这种方式,本质上还是找到一个类加载器去执行ClassLoader.loadClass动作。
@CallerSensitive public static Class<?> forName(String className) throws ClassNotFoundException { Class<?> caller = Reflection.getCallerClass(); return forName0(className, true, ClassLoader.getClassLoader(caller), caller); } 复制代码
从代码中可以看出,利用反射的机制获取执行方法的类示例caller,从而找到加载caller对应类的类加载器。从而将加载的动作交由这个类加载去执行。
类名,类加载器,类示例这三者的关系是紧密相连的。这里要提到一个数据结构来保存Java加载类过程中这三者的关系。SystemDictionary(系统字典)。
SystemDictionary以类名和类加载器实例作为一个key,Class对象引用为value。Class对象能过找到它的类加载器,类名和类加器实例对应一个唯一的Class对象。
所以,Class.forName的调用方式是先从SystemDictionary获取当前类加载器,然后以ClassLoader.loadClass的方式去加载一个类。
大多数情况下,程序中的类加载都是通过隐式加载的形式。不需要显示调用ClassLoader对象去加载类。 我们看下面一个很普通的场景。为了显示实际效果,这里自定义了一个类加载器去显示类的加载动作。
public class A { B b; public A() { this.b = new B(); } public void show() { System.out.println("b's classLoader is " + b.getClass().getClassLoader()); } } 复制代码
在加载A.class后,如果实例化类A对象,JVM会自动帮我们利用类加载器帮我们加载B类。
String classPath = "/Users/xiaoyi/work"; CustomClassLoader customClassLoader1 = new CustomClassLoader(classPath, "xiao"); Class aClazz = Class.forName("com.xiaoyi.pandora.vo.A", true, customClassLoader1); Object a = aClazz.newInstance(); 复制代码
控制台的输出如下
xiao classLoader start load class :com.xiaoyi.pandora.vo.A xiao classLoader start load class :com.xiaoyi.pandora.vo.B 复制代码
在不执行Object a = aClazz.newInstance();这条语句的时候,控制台不会输出xiao classLoader start load class :com.xiaoyi.pandora.vo.B。
总结:在加载B类时,JVM会隐式获取加载A类的类加器去执行加载B类的工作。
java.lang.ClassLoader中Class<?> loadClass(String name, boolean resolve)方法介绍了详细的类加载过程。类加载中重要的四个方法,loadClass是下面1,2,3方法的入口
1、Class<?> findLoadedClass(String name)
判断类是否已经被加载过。即从SystemDictionary中根据类型和当前类加载器作为key,查看是否能找到对应的Class
2、Class<?> findClass(String name)
交给子类加载器的拓展
3、void resolveClass(Class<?> c)
将类名,当前类加载器,类示例对象关联起来,存储在SystemDictionary中。Java中规范在一个类被使用之前,必须做关联(Before the class can be used it must be resolved)
4、Class<?> defineClass(String name, byte[] b, int off, int len)
根据类文件或者网络上的字节流,转化为一个Class的实例。常用于自定义类加载器。
java.lang.ClassLoader中Class<?> loadClass(String name, boolean resolve)的代码如下(省略了非核心流程的代码)
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 获取锁,类的加载过程是线程安全的 synchronized (getClassLoadingLock(name)) { // 查看是否已经被加载过 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { // 如果存在父加载器,委托父加载器去尝试加载 c = parent.loadClass(name, false); } else { // 委托Bootstrap类加载器去尝试加载 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null) { // 类加载器本身去加载类 c = findClass(name); } } if (resolve) { // 关联类和类加载器的关系 resolveClass(c); } return c; } } 复制代码
自定义类加载器是使用类加载器一个基本场景。关键的方法在前面“类加载过程”已经给出。主要有2点
1、继承ClassLoader
2、重写findClass方法,实现获取类的字节流转化为类实例返回
一个简单的自定义类加载器如下
public class CustomClassLoader extends ClassLoader { private String name; private String classPath; public CustomClassLoader(String classPath, String name) { this.name = name; this.classPath = classPath; } @Override public Class<?> findClass(String name) throws ClassNotFoundException { System.out.println(this.name + " classLoader start load class :" + name); try { byte[] classData = getData(name); if(classData != null) { return defineClass(name, classData, 0, classData.length); } } catch (IOException e) { e.printStackTrace(); } return super.findClass(name); } private byte[] getData(String className) throws IOException { InputStream in = null; ByteArrayOutputStream out = null; String path = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; try { in = new FileInputStream(path); out = new ByteArrayOutputStream(); byte[] buffer = new byte[2048]; int length = 0; while ((length = in.read(buffer)) != -1) { out.write(buffer, 0, length); } return out.toByteArray(); } catch (Exception e) { } finally { if(in != null) { in.close(); } if(out != null) { out.close(); } } return null; } } 复制代码
从SystemDictionary数据结构中,一个类名和类加载器实例,对应一个Class实例。也就是说一个类文件,可以交由不同的类加载器去生成不同的类实例,这些类实例是可以在JVM中并存的。但是如果在对类加载器的访问上做好隔离,这些类实例在JVM中是可以实现隔离的。具体可以参考Tomcat容器如何利用WebappClassLoader实现应用上的隔离的技术相关文档或者Pandora的隔离实现原理。
SPI全称是Service Provider Interface, 是JDK内置的一种服务提供发现机制。是一种动态替换发现机制。举个例子:有个接口想在运行时才发现具体的实现类,那么你只需在程序运行前添加一个实现即可。常见的SPI有JDBC、JCE、JNDI、JAXP等。
这些SPI的接口是由Java核心库来提供,而SPI的实现则是作为第二方jar包被包含进类路径(classpath)中。以JDBC的mysql为例子,调用代码如下所示
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydb", "root", "root"); 复制代码
com.mysql.jdbc.Driver中的代码如下,在类被加载后,向DriverManager中注册mysql的Driver。
public class Driver extends NonRegisteringDriver implements java.sql.Driver { public Driver() throws SQLException { } static { try { DriverManager.registerDriver(new Driver()); } catch (SQLException var1) { throw new RuntimeException("Can/'t register driver!"); } } } 复制代码
下面来分析一下其中的类加载过程
1、隐式方式加载DriverManager类,因为DriverManager在JDK的rt.jar中,这是对应的类加载器是Bootstrap类加载器
2、DriverManager类加载后,执行静态代码块,初始化java.sql.Driver的实现类。按照SPI的规范,ServiceLoader会去/META-INF/services下面查找java.sql.Driver资源
3、ServiceLoader获取ThreadContextClassLoader,构造ServiceLoader实例,并将ThreadContextClassLoader赋给ServiceLoader实例。这时候的类加载器是System类加载器。
public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) { return new ServiceLoader<>(service, loader); } 复制代码
4、DriverManager利用ServiceLoader去获取/META-INF/services下的java.sql.Driver文件,加载对应的实现类,并初始化具体的Driver类。这时候的类加载器是第3步的System类加载器,它能够加载classpath下面的类。
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<?> c = null; try { c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found"); } S p = service.cast(c.newInstance()); providers.put(cn, p); return p; 复制代码
5、Driver类加载实例化后,会执行静态代码将自己注册到DriverManager中
SPI的实现方式其实是破坏了类加载器的委托机制,在类加载的过程中,Bootstrap类加载在获取不到具体的SPI Provider实现类的情况下,委托ThreadContextLoader去加载classpath下的实现类。