掀起JVM的引擎盖——classloader

原文地址 


对很多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阶段找不到它们。但是,你是从哪里开始的,考虑下面的代码:
Arrays.toString((((URLClassLoader) Test.class.getClassLoader()).getURLs()));


上述代码将会返回classpath中Test类所需要的所有jar文件和目录。所以我们可以检查对应的jar文件或者class是否存在在classpath,如果不存在,把它们加入classpath中,如果存在,确认目录是否正确。一般都是这两种典型的问题导致以上错误。
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
(译者注:比较少出现的异常,有兴趣的查看原文,因为译者对这段知识也不熟悉,本着负责的态度,不敢随便翻译)

   

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值