简介
加载阶段是整个类加载过程中的一个阶段,《Java虚拟机规范》没有指明二进制字节流必须要从哪里获取、如何获取,那也意味着我们的类加载过程中,加载阶段是相对可控的。
1. 类与类加载器
任意类都必须由加载它的加载器和这个类本身共同确立在JVM中的唯一性,每个类加载器都拥有一个独立的类名称空间。
比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等(“相等” 包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括了使用instanceof关键字做对象所属关系判定等各种情况)。
我们从以下示例去进一步了解,验证上述说法的正确性:
package demo2;
import java.io.IOException;
import java.io.InputStream;
public class FirstClassLoader extends 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);
}
}
}
package demo2;
public class Run {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
Object obj = new FirstClassLoader().loadClass("demo2.FirstClassLoader").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof demo2.FirstClassLoader);
}
}
输出:
class demo2.FirstClassLoader
false
结合上述代码,虽然“obj”对象是demo2.FirstClassLoader类实例化出来的,但仍然返回了false
。这是因为JVM中同时存在了两个demo2.FirstClassLoader类,一个是应用程序类加载器加载的,另一个是自定义的类加载器加载的,虽然来自同一个Class文件,但在JVM中仍然属于相互独立的类,所以进行类型检查的时候返回了false
。
2. 类加载器
系统提供的三种类加载器:
- 启动类加载器:通过C++实现的,是JVM的一部分,主要负责加载存放在
<JAVA_HOME>\lib
目录或被-Xbootclasspath
参数指定的路径中存放的,且前提是JVM能够识别的类库(例如:rt.jar、tools.jar,其他即使放置在lib目录下也不会被加载)加载到JVM的内存中。启动类加载器无法被Java程序直接引用,在编写自定义类加载器时,若需要把加载请求委派给引导类加载器处理,可以直接使用null
来代替。 - 拓展类加载器:它是在类
sun.misc.Launcher$ExtClassLoader
中实现的,主要负责加载<JAVA_HOME>\lib\ext
目录中,或被java.ext.dirs系统变量所指定的类库,它是一种Java系统类库的扩展机制,JDK的开发团队允许用户将具有通用性的类库放置在ext目录里以扩展Java SE的功能。 - 应用程序类加载器:这个类加载器由
sun.misc.Launcher$AppClassLoader
来实现。因为应用程序类加载器是ClassLoader类中的getSystemClassLoader()
方法的返回值,所以也称作“系统类加载器”,负责加载用户类路径上所有的类库。应用程序中若没有自定义过自己的类加载器,一般情况下应用程序类加载器就是程序中默认的类加载器。
2.1 双亲委派模型
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。这里类加载器之间的父子关系一般不是以继承关系来实现的,而是通常使用组合关系来复用加载器代码。
双亲委派的工作过程:如果一个类加载器收到类加载的请求,首先不会自己尝试加载这个类,而是将请求委派到父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有父类加载器返回自己无法完成这个加载请求时(他的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
双亲委派模型的优点:最大的好处就是Java中的类随着类加载器一起具备了一种带有优先级的层次关系。例如:java.lang.Object,存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能保证是同一个类。
如果没有使用双亲委派模型,都有各个类加载器自行加载的话,比如自己编写一个java.lang.Object并存放在程序的ClassPath中,那么系统中将会出现多个不同的Object类,应用程序将会变得一片混乱。
双亲委派的部分实现源码如下:
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 {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 若抛出ClassNotFoundException,代表父类加载器无法完成加载请求
}
if (c == null) {
// 如果仍然没有找到,则调用自身findClass以找到该类。
long t1 = System.nanoTime();
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;
}
}
2.2 破坏双亲委派模型
当被加载的类引用了另外一个类的时候,虚拟机就会使用加载第一个类的类加载器加载被引用的类。
如果rt.jar中的类引用外部类的时候,启动类加载器不能加载这些外部类该怎么解决呢?
2.2.1 示例:JDBC
JDBC 4.0规定,JDBC驱动包META-INF/services路径下必须存在java.sql.Driver文件,文件内容为java.sql.Driver接口的实现类全限定名,通过
SPI
(Service Provider Interface)技术实现自动加载。
什么是破坏双亲委派模型?
java.sql.Driver接口定义在rt.jar中,自然由启动类加载器完成加载,但是java.sql.Driver的实现类是各服务商提供的,不属于启动类加载器负责加载的区域,所以只能由子加载器进行加载,这就要打破双亲委派模型的层次结构,来逆向使用类加载器,也就违背了双亲委派模型的一般性原则。
接下来以Mysql的JDBC驱动包为例,pom引用如下:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.39</version>
<scope>runtime</scope>
</dependency>
接下来查看mysql-connector-java-5.1.39.jar中是否包含META-INF/services/java.sql.Driver文件并查看内容:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
查看java.sql.DriverManager部分源码如下:
public class DriverManager {
// 省略
static {
loadInitialDrivers();
}
// 省略
private static void loadInitialDrivers() {
String drivers;
// 省略
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;
}
});
// 省略
}
// 省略
}
ServiceLoader.load()
从services/java.sql.Driver文件中获得所有能被实例化的类的全限定名,通过Class.forName() 载入类对象,并用 instance() 方法将类实例化。
查看java.util.ServiceLoader部分源码
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
可以看到默认使用的类加载器为线程上下文加载器Thread.currentThread().getContextClassLoader(),上下文加载器又是什么类型的加载器呢?
// 摘自sun.misc.Launcher代码片段
public Launcher() {
// 省略
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
// 省略
}
如上所示,在sun.misc.Launcher初始化的时候就会将线程上下文加载器设置为AppClassLoader。
关于ServiceLoader的源码简单解析可以查看另一篇文章《ServiceLoader与SpringFactoriesLoader源码剖析》这里不做赘述。
总结
本文我们了解系统提供的三种类加载器之间的协作关系与双亲委派模型的工作流程,破坏双亲委派模型在我看来也是一种“突破”,在合理的场景下即是创新。