对很多Java开发者来说,classloader是一个底层并且经常被忽略的知识面。在zeroTurnaround团队,我们的开发人员必须经常与classloader打交道,为的就是诞生JRebel技术,这是一项与classloader打交道,提供了在运行时可以做到热装载,避免长时间的重新编译、重新打包、重新发布的循环。
以下是我们学习classloader的一些总结,包括一些debug的技巧希望在将来可以有效得节省你的时间和解决你的潜在烦恼。
一、classloader 就是一个普通的java object
是的,除了JVM的系统classloader,classloader没有什么复杂的,它就是一个java object,一个抽象类,用户可自行实现成一个具体的类,以下是API
public abstract class ClassLoader { public Class loadClass(String name); protected Class defineClass(byte[] b); public URL getResource(String name); public Enumeration getResources(String name); public ClassLoader getParent() }
看起来很简单吧,让我们一个方法一个方法了解。上述方法中主要的方法是loadClass,该方法接受一个String类型的name参数并返回实际的类对象。如果你以前使用过classloader,那估计这是你日常编码中最熟悉也最常用的方法。defineClass 是一个final方法,接受一个从文件或者网络上一个资源读取的字节数组,也是同样返回实际的类对象。
classloader也可以从classpath中find resources,作用与loadClass相似。正好有一对方法,getResource 和 getResources,一个返回是一个URL,一个返回的是URLs的枚举,枚举指向参入的name参数对应的resource。
每一个classloader都有一个parent,getParent方法返回parent classloader,我们将会在下面讨论得更深次。
classloader是延迟加载,所以classes在运行期间只会在请求时才会加载。在运行期间,一个class将有可能被多个classloader加载,主要依赖于class的被那个classloader引用,说起来可能有点绕,下面我们用代码来说明吧。
public class A { public void doSmth() { B b = new B(); b.doSmthElse(); } }
上面代码里在类A的doSmth函数里将调用类B的构造函数,在幕后发生如下A.class.getClassLoader().loadClass("B");
原先需要加载调用类 A的classloader将会加载类 B。
二、classloaders是分层结构的,但是就像孩子一样,他们并不会什么事情都请教他们的父母
每一个classloader都有一个父节点classloader,当一个classloader被请求一个class时,如果发现该class没有被加载过,那么它会直接向它的父classloader请求,父classloader加载也是遵循这样的流程。如果两个classloader同时需要加载同一个class,而且他们有相同的父classloader时,父classloader只会加载一次。当两个classloader分别加载同个class时,这是件麻烦的事情,接下来我们将会看看其中会出现的问题。当J2EE的标准设计时,web classloader确实完全按照相反的方向设计的,比如下面的例子。
war1 模块更情愿使用自己的classloader去加载class,而不是委托父classloader,就是上图app1.ear的classloader。这意味不同的war模块彼此是不知道彼此的存在的,当war1和war2的classloader需要分层次委托时(比如class不在war的classloader范围内),app1.ear的classloader将会被使用。ear classloader的父节点是container classloader, ear classloader将会委托请求到container,这点与war classloader是不同,ear classloader 会委托双亲而不是记载本地class,所以J2EE的设计标准与JSE是不同的。
三、扁平的classpath
现在谈论系统classloader是怎么根据classpath查找classes的,classpath可以是目录或者直接jar文件,至于查找的先后顺序,这是由JVM的版本决定的。有可能在classpath里面有多个不同版本的class,但是你最终都只是获取到第一个实例而已。本质上来说它仅仅是一个资源列表,那也是为什么可以被扁平的引用.所以当查找一个资源时,迭代查找类路径列表通常是相对缓慢的。
四、怎么debug class loading errors
1. NoClassDefFoundError/ClassNotFoundException/ClassNoDefFoundException
如果你碰到以上的error/exception,上述类真的存在么?不用烦于在你的IDE查找对应的class,它们一定存在,否则在编译期就会报错了。上述的错误都是runtime 异常错误,表示在runtime阶段找不到它们。但是,你是从哪里开始的,考虑下面的代码:上述代码将会返回classpath中Test类所需要的所有jar文件和目录。所以我们可以检查对应的jar文件或者class是否存在在classpath,如果不存在,把它们加入classpath中,如果存在,确认目录是否正确。一般都是这两种典型的问题导致以上错误。Arrays.toString((((URLClassLoader) Test.class.getClassLoader()).getURLs()));
2. NoSuchMethodError/NoSuchFieldError/AbstractMethodError/IllegalAccessError
上面的这些错误比较有趣了。它们都是IncompatibleClassChangeError 的子类,表示classloader根据名字已经找到对应的class,但显然找不到正确的版本。我们举个例子,Test类将会调用Util类,但不好意思,在运行时我们碰到一个异常。下面的代码例子将会给我们有效的debug信息。
Test.class.getClassLoader().getResource(Util.class.getName().replace('.', '/') + ".class");
我们将会调用Test类的classloader的getResource方法。这将会返回Util类的URL。注意上面代码将'.'替换成'/'并且在后面新增'.class'。这将对应类的包名表述方式改成了文件系统路径的表述方法。这可以找出classloader真正加载的文件,然后开发者就可以确定它是否是正确的版本。我们可以使用 javap -private(译者注:没有用过),这也可以帮忙我们去查看字节码来确认加载的类是否是正确版本。
3. LinkageError/ClassCastException/IllegalAccessError
(译者注:比较少出现的异常,有兴趣的查看原文,因为译者对这段知识也不熟悉,本着负责的态度,不敢随便翻译)