Java Classloader基础

背景

最近在引入一款开源项目投产时,在生产环境出现了一个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上亲测可用的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值