引言

从事Java开发的小伙伴,在工作中一定遇到过如ClassNotFoundExceptionNoClassDefFoundErrorNoSuchMethodError等等之类的异常或错误,大家肯定会想到类缺失了、Jar包有冲突了等原因。知其然更要知其所以然,本文将从Java类加载机制的原理层面来分析出现这些问题的原因,同时以一些基础框架为例来说明它们是如何规避的。以此来加深我们对Java类加载机制的了解,达到快速解决问题和开发基础工具时如何善用Java类加载器。

类加载过程

一个Class的生命周期包括加载连接(验证、准备、解析)初始化使用卸载。其中,系统寻找Class文件的过程涉及加载、连接和初始化。

加载:对应loadClass

主要是根据类路径读取二进制流,将静态信息加载到方法区,堆内存中创建Class对象来访问方法区的信息。这个里面在java实现里,涉及的调用链:

1
2
3
4
this.parent.loadClass->
this.findBootstrapClassOrNull->
this.findClass->
this.resolveClass

连接:对应resolveClass

  • 验证阶段是验证文件格式、元数据、字节码、符号引用,比如魔数校验、主次版本号、常量池常量类型、是否符合Java语言规范;
  • 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配,比如静态基本数据类型的默认值;
  • 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。也就是得到类或者字段、方法在内存中的指针或者偏移量

初始化

执行初始化方法 <clinit>()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。<clinit>()方法是编译之后自动生成的,它是带锁线程安全(单例模式)。比如new一个类,读取一个静态字段(未被final修饰)、或调用一个类的静态方法时。

卸载

卸载类即该类的 Class 对象被 GC。由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。

双亲委派模型

AppClassLoader的父类加载器为ExtClassLoaderExtClassLoader的父类加载器为null,null并不代表ExtClassLoader没有父类加载器,而是BootstrapClassLoader

加载的流程源码如下:

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
36
37
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;
}
}

加载的过程如下:

  • 首先,在当前类加载中调用loadClass方法,通过findLoadedClass来判断类是否已经被加载过。
  • 如果还没有被记载:
    • 若父类加载器存在,则通过调用父加载器loadClass()方法处理;
    • 否则,通过findBootstrapClassOrNull使用启动类加载器 BootstrapClassLoader 加载。
  • 如果无法成功记载对应class文件,就通过当前类加载器的findClass方法,否则就会抛出ClassNotFoundException
  • 如果已经加载过,则调用resolveClass执行连接操作。

自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。在findClass一般自定义找到class文件二进制流的方式,同时还要调用defineClass。部分源码如下:

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
  /**
* Finds the class with the specified <a href="#name">binary name</a>.
* This method should be overridden by class loader implementations that
* follow the delegation model for loading classes, and will be invoked by
* the {@link #loadClass <tt>loadClass</tt>} method after checking the
* parent class loader for the requested class. The default implementation
* throws a <tt>ClassNotFoundException</tt>.
*
* @param name
* The <a href="#name">binary name</a> of the class
*
* @return The resulting <tt>Class</tt> object
*
* @throws ClassNotFoundException
* If the class could not be found
*
* @since 1.2
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}

类加载异常分析

针对ClassNotFoundException,从双亲委派模型中,我们可以看到在调用loadClass方法过程中,如果一直无法成功加载class信息,就会抛出ClassNotFoundException。这个时候我们就要检查class的类路径是否正确以及对应的类加载器是否设置正确,一般是出现在反射或类加载器loadClass方法上,classpath中找不到类。

针对NoClassDefFoundError,一般出现在new一个对象实例时,class文件中出现该代码说明已经通过了编译,但是在实际运行时发现该类在初始化时发生了错误,比如自身静态代码块初始化异常、依赖的其他类不存在,就会抛出NoClassDefFoundError

针对NoSuchMethodError,一般是对象实例调用了某个方法,在编译时classpath中的类存在该方法,但在实际运行中,加载进来该类没有该方法说明了存在编译时和运行时该类的版本不同或者是存在多个版本的JAR冲突问题。

为了避免上述的一些异常,我们在编写业务代码的时候,要注意设置好classpath或者做好不同版本的class隔离。为此,我们一般会通过自定义类加载器,比如扩展URLClassLoader重写findClass来加载指定目录下的class文件,与AppClassLoader读取的路径隔离开来。

线程上下文类加载器

作为基础框架来说,比如Java中提供了一个SPI的机制,SPI接口是由BootstrapClassLoader来加载,SPI实现类需要AppClassLoader来加载,然而按照双亲委派机制这是无法实现的。为此,JAVA当中引入了ThreadContextClassLoader,破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器

这里拿JDBC中的SPI使用来举例说明,代码如下:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class DriverManager {
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}

private static void loadInitialDrivers() {

// 省略部分代码

// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()

AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();

try{
while(driversIterator.hasNext()) {
//对应c = Class.forName(cn, false, loader);
// 此处的loader是由ServiceLoader.load(service, cl);
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});

//省略部分代码
}

public final class ServiceLoader<S> implements Iterable<S> {

private static final String PREFIX = "META-INF/services/";

public static <S> ServiceLoader<S> load(Class<S> service) {
// 如果没有设置contextClassLoader,默认是AppClassLoader
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
}

由上述代码可以看出ServiceLoaderBootstrapClassLoader加载,内部找到所有Driver的实现类要加载需要通过当前线程的上下文加载器才可以识别到。同时,如果我们下游有不同版本的实现的话,可以自定义各自的类加载器,通过Thread.currentThread().setContextClassLoader(ClassLoader cl)设置进去,这样就可以做到隔离。

扩展阅读