背景
最近在引入一款开源项目投产时,在生产环境出现了一个ClassNotFoundException,仔细看完异常信息后才发现其产生于一个NullPointerException。
SomeClass.getClassLoader().getParent(); // => null
奇怪在于,测试环境并未出现过这个异常,生产环境却有。最后经过对比,发现两个环境启动脚本上的差异。
# 存在NullPointer的启动方式
java -Djava.ext.dirs=/app/lib App
# 没有问题的启动方式
java -classpath a.jar:b.jar:c.jar App
估计是一些发开人员为了图方便,临时修改了启动脚本,但并未完全理解以上两种启动命令写法的区别,所以这里稍稍总结一下。
为什么需要classloader?
顾名思义,ClassLoader名为类加载器,可以简单的理解为将.class文件加载到内存成为Class对象。首先可以想到,一个jvm进程至少存在一个ClassLoader,可若仅有一个ClassLoader会存在什么问题呢。Java类的全称是由packge+name组成,假设我们的程序中需要用到两个类,功能稍有区别,但package+name完全相同,或者一个进程中包含多组模块互不相关的模块(例如tomcat)。单个ClassLoader可能就难以满足需求了,其实说白了就是做类隔离。JDK给出了一套基本的ClassLoader体系。
JDK中的classloader代理链
这一部分原理很多文章都讲得相当清楚,这里是IBM developerworks上的一篇文章。这里会一步步做代码验证。
1. ClassLoader默认代理方式
以上抽象类ClassLoader的部分代码,可以看到一个parent属性。那么ClassLoader的代理链就是通过这个属性建立的,也就是说,只要是按照这套代理链的方式创建新的ClassLoader对象,必然需要提供一个parent ClassLoader,一般都是通过构造函数传入。
public abstract class ClassLoader {
private static native void registerNatives();
static {
registerNatives();
}
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent; // 默认会存在一个parent属性
... 省略其他代码 ...
2. 打印ClassLoader的名字
public class Main {
public static void main(String[] args) {
ClassLoader c1 = Main.class.getClassLoader();
ClassLoader c2 = c1.getParent();
ClassLoader c3 = c2.getParent();
System.out.println(c1);
System.out.println(c2);
System.out.println(c3);
}
}
输出结果:
sun.misc.Launcher$AppClassLoader@4633c1aa
sun.misc.Launcher$ExtClassLoader@6fefa3e7
null
直接使用IDE运行以上代码,可以看到,ClassLoader的委托层级:AppClassLoader->ExtClassLoader->null(BootstrapClassLoader)。
3. Launcher类中ClassLoader的定义
public Launcher() {
Launcher.ExtClassLoader var1;
try {
// 创建ExtClassLoader
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader");
}
try {
// 创建AppClassLoader时,传入了ExtClassLoader作为parent
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader");
}
Thread.currentThread().setContextClassLoader(this.loader);
// 省略无关代码
... ...
}
这是sun.misc.Launcher类的构造函数代码片段,展示了AppClassLoader和ExtClassLoader之间的代理链是如何建立的。以上代码中可以看到Launcher.ExtClassLoader.getExtClassLoader()是没有传入参数的,那么他的parent classloader是什么呢。见以下代码:
static class ExtClassLoader extends URLClassLoader {
...省略其他代码...
public ExtClassLoader(File[] var1) throws IOException {
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
}
可以看到,ExtClassLoader继承了URLClassLoader,使用URLClassLoader的构造函数时,在parent参数处,传入的了null。这是因为上面提到的BootstrapClassLoader只是一个为了方便理解虚拟出来的类,其功能由jvm实现,在Java程序中并不存在。
那么以上两组代码片段就解释了jdk的基础ClassLoader链路的形成。
4. 到底加载了哪里类
由于代码篇幅较多,这里方便描述,将代码简化。
static class AppClassLoader extends URLClassLoader {
AppClassLoader() {
super(toURL(System.getProperty("java.class.path")),
null, Launcher.factory);
}
}
static class ExtClassLoader extends URLClassLoader {
ExtClassLoader() {
super(toURL(System.getProperty("java.ext.dirs")),
null, Launcher.factory);
}
}
可以看到,ExtClassLoader加载了由启动命令传入的java.ext.dirs参数(-Djava.ext.dirs=)指向的类或目录,而AppClassLoader加载了由启动命令传入的java.class.path参数(-cp或-classpath)指向的类或目录。
回归到最开始描述的指针问题,这里结论就非常清晰了。如果通过-Djava.ext.dirs方式指定应用jar包的路径,应用类的加载是由ExtClassLoader完成,而ExtClassLoader的parent classloader为null。这种做法虽然大部分场景下没有问题,但会覆盖JVM的默认参数,会导致\jre\lib\ext中的jar包无法加载。
Ps. 可能有些同学觉得classpath无法指定文件夹,图方便就直接用java.ext.dirs参数了。-classpath app/lib/* 这种写法在jdk7上亲测可用的。