Java ClassLoader机制

JDK默认ClassLoader

JDK 默认提供了如下几种ClassLoader

BootstrpLoader

Bootstrp加载器是用C++语言写的,它是在Java虚拟机启动后初始化的,它主要负责加载%JAVA_HOME%/jre/lib,-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类。

ExtClassLoader

Bootstrploader加载ExtClassLoader,并且将ExtClassLoader的父加载器设置为Bootstrploader.ExtClassLoader是用Java写的,具体来说就是 sun.misc.Launcher$ExtClassLoader,ExtClassLoader主要加载%JAVA_HOME%/jre/lib/ext,此路径下的所有classes目录以及java.ext.dirs系统变量指定的路径中类库。

AppClassLoader

Bootstrploader加载完ExtClassLoader后,就会加载AppClassLoader,并且将AppClassLoader的父加载器指定为ExtClassLoader。AppClassLoader也是用Java写成的,它的实现类是 sun.misc.Launcher$AppClassLoader,另外我们知道ClassLoader中有个getSystemClassLoader方法,此方法返回的正是AppclassLoader.AppClassLoader主要负责加载classpath所指定的位置的类或者是jar文档,它也是Java程序默认的类加载器

同时ExtClassLoader和AppClassLoader都继承自URLClassLoader加载器

综上所述,它们之间的关系可以通过下图形象的描述:

双亲委托模型

首先我们知道ClassLoader加载类的入口是loadClass方法
Java中ClassLoader的加载采用了双亲委托机制,采用双亲委托机制加载类的时候采用如下的几个步骤:

  1. 当前ClassLoader首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。
  2. 当前classLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到bootstrp ClassLoader.
  3. 当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。

说到这里大家可能会想,Java为什么要采用这样的委托机制?理解这个问题,我们引入另外一个关于Classloader的概念“命名空间”, 它是指要确定某一个类,需要类的全限定名以及加载此类的ClassLoader来共同确定。也就是说即使两个类的全限定名是相同的,但是因为不同的 ClassLoader加载了此类,那么在JVM中它是不同的类。明白了命名空间以后,我们再来看看委托模型。采用了委托模型以后加大了不同的 ClassLoader的交互能力,比如上面说的,我们JDK本生提供的类库,比如hashmap,linkedlist等等,这些类由bootstrp类加载器加载了以后,无论你程序中有多少个类加载器,那么这些类其实都是可以共享的,这样就避免了不同的类加载器加载了同样名字的不同类以后造成混乱

如何自定义ClassLoader
Java除了上面所说的默认提供的classloader以外,它还容许应用程序可以自定义classloader,那么要想自定义classloader我们需要通过继承java.lang.ClassLoader来实现,接下来我们就来看看再自定义Classloader的时候,我们需要注意的几个重要的方法:

loadClass方法

public Class<?> loadClass(String name)  throws ClassNotFoundException

上面是loadClass方法的原型声明,上面所说的双亲委托机制的实现其实就实在此方法中实现的。下面我们就来看看此方法的代码来看看它到底如何实现双亲委托的。

    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); //步骤1,首先从自己当前classloader已经加载的类中查询是否此类已经加载
            if (c == null) { //没有的话
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false); //委托父类加载器去查找
                    } else {
                        c = findBootstrapClassOrNull(name); //如果没有父加载器那么使用BootstrpLoader去查找
                    }
                } 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); //默认null实现

                    // 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); //resolve为true的时候,这个方法的作用暂时未知,默认为resolve为false
            }
            return c;
        }
    }

上面的代码,我加了注释通过注释可以清晰看出loadClass的双亲委托机制是如何工作的。 这里我们需要注意一点就是public Class<?> loadClass(String name) throws ClassNotFoundException没有被标记为final,也就意味着我们是可以override这个方法的,也就是说双亲委托机制是可以打破的。另外上面注意到有个findClass方法,接下来我们就来说说这个方法到底是搞末子的。

findClass方法

我们查看java.lang.ClassLoader的源代码,我们发现findClass的实现如下:

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

我们可以看出此方法默认的实现是直接抛出异常,其实这个方法就是留给我们应用程序来override的。那么具体的实现就看你的实现逻辑了,你可以从磁盘读取,也可以从网络上获取class文件的字节流,获取class二进制了以后就可以交给defineClass来实现进一步的加载。defineClass我们再下面再来描述。
ExtClassLoader和AppClassLoader都继承自URLClassLoader加载器
再来看看URLClassLoader中的findClass方法的实现

    protected Class<?> More ...findClass(final String name)
        throws ClassNotFoundException
    {
        ..........
        String path = name.replace('.', '/').concat(".class");
        Resource res = ucp.getResource(path, false);
        ..........
        return defineClass(name, res); //最终调用了defineClass方法
        ..........
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }

自定义ClassLoader就可以重写findClass方法啦,参考Android中的BaseDexClassLoader实现

defineClass方法

我们首先还是来看看defineClass的源码:

    protected final Class<?> defineClass(byte[] b, int off, int len)
        throws ClassFormatError
    {
        return defineClass(null, b, off, len, null);
    }

从上面的代码我们看出此方法被定义为了final,这也就意味着此方法不能被Override,其实这也是jvm留给我们的唯一的入口,通过这个唯 一的入口,jvm保证了类文件必须符合Java虚拟机规范规定的类的定义。此方法最后会调用native的方法来实现真正的类的加载工作
defineClass这个方法很简单就是将class文件的字节数组编程一个class对象,这个方法肯定不能重写,内部实现是在C/C++代码中实现的

Ok,通过上面的描述,我们来思考下面一个问题:
假如我们自己写了一个java.lang.String的类,我们是否可以替换调JDK本身的类?
答案是否定的。我们不能实现。为什么呢?我看很多网上解释是说双亲委托机制解决这个问题,其实不是非常的准确。因为双亲委托机制是可以打破的,你完全可以自己写一个classLoader来加载自己写的java.lang.String类,但是你会发现也不会加载成功,具体就是因为针对java.*开头的类,jvm的实现中已经保证了必须由bootstrp来加载。

代码示例

public class Test {
    public static void main(String[] args) {
        System.out.println("----------------------------");
        // 取得应用(系统)类加载器
        URLClassLoader appClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
        System.out.println(appClassLoader);
        System.out.println("应用(系统)类加载器 的加载路径: ");
        URL[] urls = appClassLoader.getURLs();
        for (URL url : urls)
            System.out.println(url);
        System.out.println("----------------------------");

        // 取得扩展类加载器
        URLClassLoader extClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader().getParent(); //因为AppClassLoader的parent就是ExtClassLoader
        System.out.println(extClassLoader);
        System.out.println("扩展类加载器 的加载路径: ");
        urls = extClassLoader.getURLs();
        for (URL url : urls)
            System.out.println(url);
        System.out.println("----------------------------");

        // 在我的机器上,使用jdk8,是打印不出来的...
        /*
         * System.out.println("BootstrapClassLoader 的加载路径: "); urls =
         * sun.misc.Launcher.getBootstrapClassPath().getURLs(); for(URL url :
         * urls) System.out.println(url);
         * System.out.println("----------------------------");
         */
        //ExtClassLoader的parent应该是BootstrapClassLoader??? 实际是有问题的,因为此时可以发现打印null,但是双亲委托机制中可以看到parent为null,loadclass会
        //先交给BootstrapClassLoader,所以也可以会所ExtClassLoader的parent是BootstrapClassLoader吧,实际上BootstrapClassLoader是一个C/C++代码实现,封装在JVM内核中
        ClassLoader bootClassLoader = ClassLoader.getSystemClassLoader().getParent().getParent();
        System.out.println(bootClassLoader);

        //此时没有打印BootstrapClassLoader,而是打印null。jdk其实发现根本就没有BootstrapClassLoader这个类,c/c++实现
        System.out.println("String ClassLoader:" + String.class.getClassLoader()); //String在jre/lib/rt.jar中所以使用的是BootstrapClassLoader
        System.out.println("Test ClassLoader:" + Test.class.getClassLoader());
    }
}

类加载器也是Java类,因为Java类的类加载器本身也是要被类加载器加载的,显然必须有第一个类加载器不是Java类,这个正是BootStrap,使用C/C++代码写的,已经封装到JVM内核中了,而ExtClassLoader和AppClassLoader是Java类。

打印结果:

sun.misc.Launcher$AppClassLoader@4e0e2f2a
应用(系统)类加载器 的加载路径:
file:/C:/Users/liaobinbin/workspace/Test/bin/

sun.misc.Launcher$ExtClassLoader@2a139a55
扩展类加载器 的加载路径:
file:/C:/Program%20Files/Java/jre1.8.0_65/lib/ext/access-bridge-64.jar
file:/C:/Program%20Files/Java/jre1.8.0_65/lib/ext/cldrdata.jar
file:/C:/Program%20Files/Java/jre1.8.0_65/lib/ext/dnsns.jar
file:/C:/Program%20Files/Java/jre1.8.0_65/lib/ext/jaccess.jar
file:/C:/Program%20Files/Java/jre1.8.0_65/lib/ext/jfxrt.jar
file:/C:/Program%20Files/Java/jre1.8.0_65/lib/ext/localedata.jar
file:/C:/Program%20Files/Java/jre1.8.0_65/lib/ext/nashorn.jar
file:/C:/Program%20Files/Java/jre1.8.0_65/lib/ext/sunec.jar
file:/C:/Program%20Files/Java/jre1.8.0_65/lib/ext/sunjce_provider.jar
file:/C:/Program%20Files/Java/jre1.8.0_65/lib/ext/sunmscapi.jar
file:/C:/Program%20Files/Java/jre1.8.0_65/lib/ext/sunpkcs11.jar
file:/C:/Program%20Files/Java/jre1.8.0_65/lib/ext/zipfs.jar

BootstrapClassLoader: null
String ClassLoader:null
Test ClassLoader:sun.misc.Launcher$AppClassLoader@4e0e2f2a

总结:
BootstrapClassLoader —加载jre/lib目录下的核心库
ExtClassLoader扩展类加载器 —加载/jre/lib/ext/目录下的扩展包
AppClassLoader应用(系统)类加载器 —加载应用程序编译出来的class

自定义ClassLoader

参考这里的实现吧
Java高新技术第一篇:类加载器详解

参考

  1. Java Classloader机制解析
  2. 分析BootstrapClassLoader/ExtClassLoader/AppClassLoader的加载路径 及”父委托机制”
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值