大致介绍
我们都知道Java代码会被Java编译器编译成class文件,在class文件中描述了该类的各种信息,class类最终需要被加载到JVM中才能运行和使用(由Java解释器执行)。
类的生命周期包括:加载、验证、准备、解析、初始化、使用、卸载。
加载的来源有以下部分:
1、本地磁盘
2、网络下载的.class文件
3、war、jar下加载.class文件
4、从专门的数据库中读取.class文件(少见)
5、将java源文件动态编译成class文件,典型的就是动态代理,通过运行时生成class文件
加载的过程就是通过类加载器 ClassLoader 实现的。ClassLoader 是Java的核心组件,所有的Class都是由 ClassLoader 进行加载的,ClassLoader 负责将class文件中的二进制数据流读入JVM内部,转换为一个与目标类对应的 java.lang.Class 对象实例。然后交给JVM进行后续的生命周期操作。
我们在代码中使用Class.forName(name)时就是使用了类加载器。
3个类加载器
系统定义的类加载器有3个,按下面顺序前一个是后一个的父加载器:
一、启动类加载器(底层使用C++实现)
它用来加载Java的核心库(JVM系统参数sun.boot.class.path 路径下的内容),用于提供JVM自身需要的类,只加载包名为java、javax、sun等开头的类
用户未自己主动设置该属性时,用代码System.getProperties("sun.boot.class.path")可以得到它的默认值:
1  | D:\Development\Java\jdk1.8.0_131/jre/lib/resources.jar  | 
二、扩展类加载器(底层使用java实现,是ClassLoader的子类,位置sun.misc.Launcher$ExtClassLoader)
从 JVM系统参数java.ext.dirs 所指定的目录中加载类库,或从JDK的安装目录的 jre/lib/ext 下加载类库。如果用户创建的JAR放在此目录下,也会自动由该类加载器加载
三、应用程序类加载器(底层使用java实现,是ClassLoader的子类,位置sun.misc.Launcher$AppClassLoader)
它负责加载 环境变量classpath 或 JVM系统参数java.class.path 指定路径下的类库。该类加载器默认是“系统类加载器”,通过ClassLoader.getSystemClassLoader()方法可以获取到该类加载器。它也是用户自定义类加载器的默认父加载器
参考博客:根据配置 CLASSPATH 彻底弄懂 AppCLassLoader(应用程序类加载器)的加载路径问题: https://blog.csdn.net/romantic_jie/article/details/107859901
在 sun.misc.Launcher 类中有初始化扩展类加载器和应用程序类加载器的源码。博客:https://www.pudn.com/news/6286507cb305d84a4f6e8596.html 搜索关键字Launcher
类加载器的其他概念
双亲委派模型,就是指一个类接收到类加载请求后,会把这个请求依次传递给父类加载器(如果还有的话),如果顶层的父类加载器可以加载,就成功返回,如果无法加载,再依次给子加载器去加载。
线程上下文的类加载器,用Thread.currentThread().getContextClassLoader()方法获得,在应用程序类加载器的初始化过程中,将该应用程序类加载器设置为线程上下文的类加载器。
线程上下文的类加载器的作用:
双亲委派模型并不是绝对的,spi机制就可以打破双亲委派模型。
什么是spi?spi(Service Provider Interface)是一种服务发现机制,Java在核心库中定义了许多接口,并且针对这些接口给出调用逻辑,但是并未给出具体的实现。开发者要做的就是定制一个实现类,在 META-INF/services 中注册实现类信息,以供核心类库使用。
最典型的例子就是 JDBC 的实现方式。Java 提供了一个 Driver 接口用于驱动各个厂商的数据库连接,Driver 接口位于 JAVA_HOME 中 jre/lib/rt.jar 中,应该由启动类加载器进行加载。根据类加载机制,当被加载的类引用了另外一个类的时候,虚拟机就会使用加载该类的类加载器加载被引用的类,因此如果其他数据库厂商定制了 Driver 的实现类之后,按理说也得把这个实现类放到启动类加载器加载的目录下,这显然是很不合理的。
于是Java提供了spi机制,即使 Driver 由启动类加载器去加载,但是他可以让线程上下文类加载器(默认是应用程序类加载器)去请求加载子类的类加载器去完成加载。但是这确实破坏了双亲委派模型。
classLoader.getResources 方法
1  | public Enumeration<URL> getResources(String name) throws IOException {  | 
这个方法被用于在 classpath 下查找一个或多个,由方法参数String name指定名字的文件的 URL。该方法返回一个枚举类型 java.util.Enumeration 对象,包含了在 classpath 下所有匹配指定名称的资源的 URL。
例如下面这段代码,正是 Spring 核心类SpringFactoriesLoader查找“spring.factories”文件路径时调用了 classLoader.getResources 方法来查找:
1  | Enumeration<URL> urls = (classLoader != null ?  | 
注意:这个方法返回的CompoundEnumeration对象中,还看不出文件所在的绝对路径,只能看出 ClassLoader 准备在哪些路径下查找目标文件。
CompoundEnumeration类的特点:CompoundEnumeration 是一个用于处理多个 Enumeration 对象的类。CompoundEnumeration 的构造器接受一个 Enumeration 数组,并且将它们合并成一个新的 Enumeration。在 nextElement() 方法中依次返回每个 Enumeration 对象中的每个枚举元素,直到所有Enumeration对象以及它们中的元素遍历结束。
在调用 nextElement() 方法时,如果当前 Enumeration 还有元素,则返回该元素。如果当前 Enumeration 没有元素,则将指针移到下一个 Enumeration,并返回该 Enumeration 的第一个元素。
PS: 其实可以用 List
那么是哪些路径呢?从这个方法可以看出,tmp[0]是当前加载器的父加载器返回的,tmp[1]才是当前加载器返回的,也就是说这个方法也是按照“双亲委派模型”编写的,返回的路径中既包含父加载器的路径,也包含当前加载器的路径。
当前加载器在一般情况下就是应用程序类加载器,所以它的这个方法返回的路径,就是 启动类加载器+扩展类加载器+应用程序类加载器 3个类加载器负责的路径的并集,即 “JVM系统参数sun.boot.class.path” + “JVM系统参数java.ext.dirs” + “环境变量classpath” 包含的路径的并集!
在这个方法中,涉及到几个内部类对象的创建:
一、ClassLoader.getBootstrapResources方法返回的Enumeration<URL>内部类对象,即启动类加载器返回的路径
1  | private static Enumeration<URL> getBootstrapResources(String name)  | 
这是个 static 方法,这个内部类对象不依赖于外部类对象,它还使用到了上面一行代码创建的另一个内部类对象e(见第二点)
二、URLClassPath.getResources方法返回的Enumeration<Resource>内部类对象
上面第一点源码中getBootstrapClassPath返回的是URLClassPath对象,所以getBootstrapClassPath().getResources(name)就是调用 URLClassPath.getResources 方法。
1  | public Enumeration<Resource> getResources(final String var1, final boolean var2) {  | 
这是个非 static 方法,所以这个内部类对象的属性之一就是外部类 URLClassPath 对象,在外部类 URLClassPath 对象中的属性 ArrayList
三、URLClassLoader.findResources方法返回的Enumeration<URL>内部类对象
扩展类加载器 和 应用程序类加载器,它们都继承自URLClassLoader,所以”tmp[1] = findResources(name);”代码就是在调用 URLClassLoader.findResources 方法。
1  | public Enumeration<URL> findResources(final String name)  | 
在这个内部类中,使用到了第一行声明的变量”EnumerationURLClassPath.findResources方法获取的:
(这个方法和上面第二点的方法的区别仅仅在于返回的是 URL 还是 Resource)
1  | public Enumeration<URL> findResources(final String var1, final boolean var2) {  | 
所以这个URLClassLoader.findResources方法中其实创建了两个内部类对象,一个是成员 URLClassPath 创建的,一个是 URLClassLoader 自己创建的。在 URLClassPath 自己创建的内部类对象中,使用到了 URLClassPath 创建的内部类对象,所以真正起作用的还是 URLClassPath 创建的内部类对象。
同时,这两个方法都非 static,所以外部类对象(扩展类加载器/应用程序类加载器)自身实例,和 URLClassPath 实例,都是其内部类对象的属性之一。扩展类加载器/应用程序类加载器 负责加载的路径,就保存在其成员 URLClassPath 中的属性 ArrayList