类加载器

类加载器是Java虚拟机中一个很重要的概念,之前在学习《深入理解Java虚拟机》这本书时,对类加载器有了一些粗浅的感性认识。最近对类加载器重新学习了一番,写这篇文章来重新对其进行梳理。

类加载器

在Java语言里,类的加载、连接和初始化过程都是在程序运行时按需完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是为Java应用程序提供了高度的灵活性。同时不要求将全部的类(包括程序中用到的以及用不到的类)一次性全部加载完成,也在一定程度上节约了系统资源。

虚拟机设计团队把类加载阶段中的”通过一个类的全限定名来获取此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”

Java存在以下几种类加载器:

  • 启动类加载器(Bootstrap ClassLoader):这个类加载器是使用C++语言实现的,是虚拟机自身的一部分。这个类加载器会加载系统变量sun.boot.class.path指定的类库。默认是<JAVA_HOME>/lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中不会被加载)类库加载到虚拟机内存中。
  • 扩展类加载器(Extension ClassLoader):这个类加载器由sun.misc.Launcher$ExtClassLoader实现,它加载系统变量java.ext.dirs指定位置下的类库。默认是<JAVA_HOME>/lib/ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
  • 各种各样用户自定义的类加载器:除了上面虚拟机自带或者JDK提供的类加载器外,用户还可以自定义类加载器,用于加载上面的类加载器无法加载的类。

Extension ClassLoader与Application ClassLoader的创建过程

了解Extension ClassLoaderApplication ClassLoader的创建过程,有助于理解这两个类加载器之间的关系以及双亲委派模型。

Extension ClassLoaderApplication ClassLoader的创建过程在sun.misc.Launcher类中完成,这个类在rt.jar中,jdk并没有提供源码,因此这里以openjdk提供的代码为准。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public Launcher() {
// Create the extension class loader
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader", e);
}

// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}

// Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader);

// Finally, install a security manager if requested
...
}

Launcher类在初始化时会依次创建Extension ClassLoaderApplication ClassLoader

下面是Extension ClassLoader的创建过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public static ExtClassLoader getExtClassLoader() throws IOException {
final File[] dirs = getExtDirs();

try {
return AccessController.doPrivileged(
new PrivilegedExceptionAction<ExtClassLoader>() {
public ExtClassLoader run() throws IOException {
int len = dirs.length;
for (int i = 0; i < len; i++) {
MetaIndex.registerDirectory(dirs[i]);
}
return new ExtClassLoader(dirs);
}
});
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}

public ExtClassLoader(File[] dirs) throws IOException {
super(getExtURLs(dirs), null, factory);
}

public URLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
super(parent);
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
acc = AccessController.getContext();
ucp = new URLClassPath(urls, factory, acc);
}
  1. 调用getExtDirs()方法,根据java.ext.dirs环境变量获取Extension ClassLoader扫描的目录。
  2. 调用ExtClassLoader的构造函数,它直接调用了父类URLClassLoader的构造函数。

注意,这里parent参数传入的是nullparent参数表示的是当前类加载器的父类加载器。传入null表示当前类加载器的父类加载器为Bootstrap ClassLoader

下面是Application ClassLoader的创建过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static ClassLoader getAppClassLoader(final ClassLoader extcl) throws IOException {
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);

return AccessController.doPrivileged(
new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}

AppClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent, factory);
}

Application ClassLoader的创建过程与Extension ClassLoader类似。

  1. 根据java.class.path环境变量获取Application ClassLoader扫描的目录。
  2. 调用构造函数。这里parent参数传入的是之前创建的Extension ClassLoader,所以Application ClassLoader的父类加载器是Extension ClassLoader

双亲委派模型

前面我们看到Extension ClassLoader的父类加载器是Bootstrap ClassLoaderApplication ClassLoader的父类加载器是Extension ClassLoader,它们之间有一个层次关系。

类加载器之间的关系可以如图所示:

双亲委派模型

类加载器之间的这种层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。除了顶层的启动类加载器之外,其余的类加载器都应当有自己的父类加载器。其工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

双亲委派机制的实现非常简单,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

第一步,调用findLoadedClass方法检查请求的类是否已经被加载过了,如果已经加载过了则不再重复加载,直接返回。

第二步,如果父加载器存在,则委托给父加载器来加载类。如果父加载器不存在,则委托给启动类加载器来加载类。

第三步,如果上层的加载器成功加载到类,则返回加载完的类。否则,调用findClass方法自己来尝试加载类。

比如前面说的Extension ClassLoaderApplication ClassLoader都继承了URLClassLoader,因此都会调用URLClassLoader中的findClass方法,从URLClassPath变量中获得相应类所在的路径,再根据类所在的路径调用defineClass去读取并加载类。

使用双亲委派模型来组织类加载器之间的关系,对于保证Java程序的稳定运作很重要。比如java.lang.Object类,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序将会变得一片混乱。

自定义类加载器

除了上面提到三种类加载器外,Java还允许用户自定义类加载器,这样我们就可以加载任意位置的类并控制类的加载过程。

前面在双亲委派机制部分,分析了loadClass方法是如何工作的。我们自定义的类加载器也应该遵循双亲委派机制。因此自定义类加载器非常简单,通常只需要3步:

  1. 继承java.lang.ClassLoader
  2. 重写findClass方法。当上层的类加载器无法加载指定的类时,就会调用当前类加载器的findClass方法来寻找指定的.class文件。
  3. 调用defineClass方法让JVM加载读取到的.class文件内容。

如下是一个简单的自定义类加载器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MyClassLoader extends ClassLoader {
private String path;

public MyClassLoader(String path) {
this.path = path;
}

@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
String classPath = path + "/" + name.replaceAll("\\.", "/") + ".class";
Path path = Paths.get(classPath);
if (!Files.exists(path)) {
throw new ClassNotFoundException();
}
byte[] bytes;
try {
bytes = Files.readAllBytes(path);
} catch (IOException e) {
throw new ClassNotFoundException();
}
return defineClass(name, bytes, 0, bytes.length);
}
}

使用MyClassLoader可以指定.class文件所在的路径,当需要加载类时,就去指定的路径下去寻找并读取.class文件,最后调用defineClass方法完成类的加载。

使用这个自定义类加载器,我们就可以加载任意目录的类,示例如下:

1
2
3
4
5
6
MyClassLoader classLoader = new MyClassLoader("/Users/wangqi/IdeaProjects/classloader-test1/target/classes");
Class<?> wolfClass = classLoader.loadClass("love.wangqi.animal.Wolf");
System.out.println(wolfClass);
Object wolfObject = wolfClass.newInstance();
Method getNameMethod = wolfClass.getDeclaredMethod("getName", null);
System.out.println(getNameMethod.invoke(wolfObject, null));

双亲委派模型的破坏

双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器实现方式。因此,双亲委派模型存在被破坏的情况,主要有以下3种情况:

  1. 双亲委派模型出现之前就已经存在的自定义类加载器
  2. 基础类需要调用回用户的代码,比如JNDI服务
  3. OSGi模块化机制

JNDI,全称是Java Naming and Directory Interface。JNDI是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,由独立厂商为JNDI接口提供实现。

JNDI的代码在rt.jar中,由启动类加载器去加载,但是它需要调用接口实现提供者的代码,而启动类加载器是找不到这些代码的。

为了解决这个问题,Java设计团队引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

JNDI服务使用线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用了这种方式。

JDBC

下面以JDBC为例分析一下它如何使用SPI机制破坏了双亲委派模型。

以下是一段使用JDBC来连接数据库的代码:

1
2
String url = "jdbc:mysql://xxx";
Connection connection = DriverManager.getConnection(url, "username", "password");

DriverManager是在rt.jar中的类,我们在使用过程中并没有指定具体数据库的Driver,它是如何找到Driver的具体实现的呢?

原因就在DriverManager类在初始化时在静态代码块中执行了加载JDBC驱动的代码:

1
2
3
4
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}

静态代码块中调用loadInitialDrivers()方法加载JDBC的驱动,其主要代码如下:

1
2
3
4
5
6
7
8
9
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}

这段代码主要的目的就是使用SPI机制来找到并加载Driver的实现类。

SPI机制是一种为某个接口寻找实现的机制,它通过约定来实现:当服务的提供者提供了服务的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件就是实现该服务接口的具体实现类。当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要在代码里指定。

ServiceLoader类是实现SPI机制的工具类。ServiceLoader的初始化过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}

public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}

第一步:调用Thread.currentThread().getContextClassLoader()方法获取上下文类加载器,默认是Application ClassLoader。使用该类加载器加载service指定的类,这里是java.sql.Driver。这个操作便是对双亲委派模型的破坏,因为这里用到了子类加载器去加载实现类。

第二步:根据serviceclassLoader新建ServiceLoader类。

第三步:根据serviceclassLoader新建LazyIterator类。LazyIterator是寻找并加载具体实现类的工具类。

ServiceLoader初始化完成之后,调用iterator()方法新建一个迭代器,在遍历迭代器的过程中去加载实现类。这个遍历过程最终调用的是刚刚新建的LazyIterator中的方法。LazyIterator有两个重要的方法:hasNextServicenextService

hasNextService方法负责寻找实现类。它的执行步骤如下:

  1. 加载实现类的配置文件,名为META-INF/services/java.sql.Driver
  2. 遍历配置文件中的实现类名称,分别为com.mysql.jdbc.Drivercom.mysql.fabric.jdbc.FabricMySQLDriver

nextService方法负责方法加载实现类。执行步骤如下:

  1. 调用Class.forName使用上下文类加载器加载实现类
  2. 调用newInstance()方法实例化实现类

在mysql的实现类实例化时会调用DriverManager.registerDriver方法将自身的实例注册到DriverManager类中。DriverManager类在调用getConnection方法去连接数据库时会遍历注册的所有实现类,调用实现类的connect方法去真正地连接数据库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}

private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if(driver != null) {
Class<?> aClass = null;
try {
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
result = false;
}
result = ( aClass == driver.getClass() ) ? true : false;
}
return result;
}

其中isDriverAllowed方法判断调用getConnection方法的类加载器与加载Driver的类加载器是否相同。

每一个类加载器都拥有一个独立的类名称空间,比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

两个不相等的类是不能相互调用的,因此调用getConnection方法的类加载器要确保和加载Driver的类加载器相同。使用我们的自定义加载器可以验证这个说法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
MyClassLoader classLoader = new MyClassLoader("/Users/wangqi/IdeaProjects/classloader-test1/target/classes");
Class<?> wolfClass = classLoader.loadClass("love.wangqi.animal.Wolf");
System.out.println(wolfClass);
Object wolfObject = wolfClass.newInstance();
Method getNameMethod = wolfClass.getDeclaredMethod("getName", null);
System.out.println(getNameMethod.invoke(wolfObject, null));

MyClassLoader classLoader2 = new MyClassLoader("/Users/wangqi/IdeaProjects/classloader-test1/target/classes");
Class<?> wolfClass2 = classLoader2.loadClass("love.wangqi.animal.Wolf");
System.out.println(wolfClass);
Object wolfObject2 = wolfClass2.newInstance();
System.out.println(wolfClass == wolfClass2);
System.out.println(wolfClass.isInstance(wolfObject2));
System.out.println(getNameMethod.invoke(wolfObject2, null));

输出结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class love.wangqi.animal.Wolf

class love.wangqi.animal.Wolf
false
false

java.lang.IllegalArgumentException: object is not an instance of declaring class

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at classloader.custom.MyClassLoaderTest.test1(MyClassLoaderTest.java:27)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:220)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53)

可以看到,wolfClasswolfClass2虽然是从同一个类文件加载的,但是因为使用不同的类加载器加载,因此他们并不相等。wolfObject2不是wolfClass的实例,wolfObject2也不能调用wolfClass类反射获得的getNameMethod方法。

深入理解Java虚拟机——JVM高级特性与最佳实践
https://juejin.im/post/6844903780031397902
https://juejin.im/post/6844904065311178759
https://blog.csdn.net/yangcheng33/article/details/52631940
https://www.jianshu.com/p/946df4276662
https://zhuanlan.zhihu.com/p/70600378