初探JVM-ClassLoader源码

        之前简单介绍了一下JVM中的ClassLoader (见《JVM类加载机制-ClassLoader》),现在我们再来看看ClassLoader的源码。


ClassLoader 

        首先,我们看看ClassLoader的构造方法,如果不传入参的话,默认父加载为SystemClassLoader默认加载器。

protected ClassLoader() {
        this(checkCreateClassLoader(),getSystemClassLoader());
}
 

        我们再细看一层,发现SystemClassLoader原来要到sun.misc.Launcher里面去。其实JVM用到的3个类加载器,有两个是sun.misc.Launcher里面的AppClassLoader和ExtClassLoader,还有一个BootstrapLoader是C++写的,这里我们就不细究了。

private static synchronized void initSystemClassLoader() {
        if (!sclSet) {
            if (scl !=null)
                throw new IllegalStateException("recursive invocation");
            sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
            if (l !=null) {
                Throwable oops = null;
                scl = l.getClassLoader();
                try {
                    scl = AccessController.doPrivileged(new SystemClassLoaderAction(scl));
                } catch (PrivilegedActionException pae) {
                    oops = pae.getCause();
                }
            }
            sclSet = true;
        }
}

        我们再来看看loadClass(),这个无疑是整个ClassLoader的重点,其处理逻辑为:

1. 调用 findLoadedClass()检查是否已经加载类。

2. 在父类加载器上调用loadClass(),让父类先尝试加载(委派模型)。如果父类加载器为null,就是说到达顶层加载器了,则用内置类加载方法findBootstrapClass0()来加载。

3. 顶层加载器的findBootstrapClass0()也找不到class的话,就会抛出异常,此时再调用当前加载器的findClass()查找。

        通过这个流程,能保证每个class只load一次,避免发生类冲突。而且按照委派模型,每次都从顶层ClassLoader开始加载,确保类加载的安全。

protected synchronized Class<?>loadClass(String name, boolean resolve)throws ClassNotFoundException {     
        Class c = findLoadedClass(name);  // Check if it has already beenloaded
        if (c ==null) {
            try {
                if (parent !=null)
                    c = parent.loadClass(name,false);
                else
                    c = findBootstrapClass0(name);
            }
            catch (ClassNotFoundException e) {             
                c = findClass(name);  // If still not found, then invokefindClass
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
}
private native final Class findLoadedClass0(String name);
private native Class findBootstrapClass(String name);
private native void resolveClass0(Class c);
protected Class<?>findClass(String name)throws ClassNotFoundException
        throw new ClassNotFoundException(name);
}

        以上代码能看出,底层load和find的实现都不是Java写的,交给本地方法实现,由JVM管理。

        另外你也许会疑惑,ClassLoader.findClass()里面竟然直接抛出异常。这是因为JVM一般的类都是由findBootstrapClass0()来查找的,如果找不到,就说明classpath中没有此class了,所以这里只能继续抛出ClassNotFoundException。

(所以我们看到ClassNotFoundException出现的时候,第一反应应该检查一下classpath的设置,检查该类的.class是否存在,检查jar包有没有部署好等等。)

        其实findClass()是留给用户自定义的类加载器实现的。因为findBootstrapClass0()只会在classPath中查找.class文件,如果你通过别的方式(如网络),或者在别的路径(classpath以外的路径)加载,就要自定义ClassLoader,并通过重写findClass()来重新指向你的类。 如URLClassLoader

        这里最好不要重写loadClass(),因为会损坏ClassLoader的委派模型,造成安全隐患。

 

        另外,我们可以用代码来查看JVM加载器的层次结构。

        JVM的ClassLoader是三层结构,分别是:BootstrapLoader加载系统类,ExtClassLoader加载扩展类,AppClassLoader加载应用类。

        我们建一个测试类Inner4Test,然后循环打印出它的类加载器。

        Class<?> c = Class.forName("com.jscai.classloader.Inner4Test");
        ClassLoader loader = c.getClassLoader();
        while (loader !=null) {
            System.out.println(loader.toString());
            loader = loader.getParent();
       }

console

sun.misc.Launcher$AppClassLoader@79b0edb2

sun.misc.Launcher$ExtClassLoader@4ec57f88

        这里清晰地打印出JVM类加载的层次,但只有AppClassLoader和ExtClassLoader,其实BootstrapLoader的缺席,是因为BootstrapLoader是C++写的,Java这边没法显示,所以认为null。

 

 

        接着,我们看看UrlClassLoader是怎么实现自定义加载的。

URLClassLoader

        URLClassLoader加载器用于从 指向JAR文件和目录的URL的搜索路径 来加载类和资源。相对于JVM的加载器只能加载classpath来说,他可以指向任意可达的URL来加载类资源。

 

        从类的继承结构来看,URLClassLoader->SecureClassLoader->ClassLoader,URLClassLoader间接继承了ClassLoader。

public class URLClassLoader extends SecureClassLoaderimplements Closeable
 

        在URLClassLoader自定义的类加载策略中,没有修改loadClass(),而只是重写了findClass()。他会把我们传入的类名"com.jscai.classloader.Ext4Test",根据构造函数中的path,拼装成.class文件名"com/jscai/classloader/Ext4Test.class",然后在defineClass()中把.class文件加载成类对象。

        在执行URLClassLoader.loadClass()的时候,因为是URL指向的类资源在classpath以外,所以在委派模型上面是找不到的。这时候抛出ClassNotFound异常,跳到URLClassLoader.findClass()来执行自定义的加载。

    protected Class<?> findClass(final String name)throws ClassNotFoundException{
        try {
            return (Class) AccessController.doPrivileged(newPrivilegedExceptionAction() {
                public Object run()throws ClassNotFoundException {
                    String path = name.replace('.','/').concat(".class");
                    Resource res = ucp.getResource(path,false);
                    if (res !=null) {
                        try {
                            return defineClass(name,res,true);
                        }
                        catch (IOException e) {
                            thrownewClassNotFoundException(name, e);
                        }
                    }
                    else {
                        thrownewClassNotFoundException(name);
                    }
                }
            }, acc);
        }
        catch (java.security.PrivilegedActionExceptionpae) {
            throw (ClassNotFoundException) pae.getException();
        }
    }

 

        这里写个简单的URLClassLoader-demo,先创建一个简单的测试类Ext4Test,然后写一个测试方法来证明JVM能正常加载:

        System.out.println(Class.forName("com.jscai.classloader.Ext4Test"));

 

        然后到bin目录把Ext4Test.class连同package文件夹(因为URLClassLoader会根据包名解析成目录结构)放到根目录下的newClass目录,然后把Ext4Test类删掉。再运行刚刚的测试方法,会抛出ClassNotFoundException。说明此时Ext4Test类不在classpath,JVM的类加载器已经找不到它了。

        这时候再指定URLClassLoader来加载,这时候能正常运行,说明URLClassLoader能找到classpath以外的Ext4Test.class。

        String url = "file:/" + System.getProperty("user.dir") +"/newClass/";
        URLClassLoader ucl = new URLClassLoader(new URL[]{new URL(url)});
        Class<?> c = ucl.loadClass("com.jscai.classloader.Ext4Test");
        ClassLoader loader = c.getClassLoader();
        while (loader !=null) {
            System.out.println(loader.toString());
            loader = loader.getParent();
        }

console

java.net.URLClassLoader@248523a0

sun.misc.Launcher$AppClassLoader@79b0edb2

sun.misc.Launcher$ExtClassLoader@4ec57f88

 

        而且这里能看出,类加载遵循委派模型,URLClassLoader位于JVM的类加载器之后。

        所以,这里如果只是加载classpath以内的class,那JVM类加载器加载完就结束了,即使用URLClassLoader来加载,也不会经过URLClassLoader的。

        String InnerName = "com.jscai.classloader.Inner4Test";
        URLClassLoader ucl = new URLClassLoader(new URL[]{new URL(url)});
        Class<?> c = ucl.loadClass(InnerName);
        ClassLoader loader = c.getClassLoader();
        while (loader != null) {
            System.out.println(loader.toString());
            loader = loader.getParent();
        }

console

sun.misc.Launcher$AppClassLoader@79b0edb2

sun.misc.Launcher$ExtClassLoader@4ec57f88

 

动态加载--Class的识别

        了解了JVM的ClassLoader和自定义类加载器后,我们谈谈ClassLoader比较火的应用--类的动态加载/装载。

        类的动态加载就是在不重启JVM的前提下,把新的class加载到内存中(新的class对象覆盖旧的class对象)。那为什么ClassLoader能动态加载,JVM中是怎么管理class对象的, JVM中是怎么辨别class对象的新旧的,……等等很多问题都是值得我们去思考和探究,在这里我们先讨论,JVM中是怎么识别一个class对象的。

 

        在JVM中,唯一标识一个class对象需要看两个条件:A类全名(包名+类名),B加载器实例。

        就是说,只要是同一个类加载器实例加载的同一个名字的class对象,JVM都认为是同一个类。

        Class<?> c1 = Class.forName("com.jscai.classloader.Inner4Test");
        Class<?> c2 = Class.forName("com.jscai.classloader.Inner4Test");
        System.out.println("They aresame class : " + (c1 == c2));
console

They are same class : true


        两个Object用==比较,返回true,就是两者的地址相等,就说明是同一个Object。因此我们能看出,同名的class在JVM只会加载一次,所以两者相等。

        但是如果用URLClassLoader加载外面的类Ext4Test,效果却不一样:

        String url = "file:/" + System.getProperty("user.dir") +"/newClass/";
        URLClassLoader ucl1 = new URLClassLoader(new URL[]{new URL(url)});
        URLClassLoader ucl2 = new URLClassLoader(new URL[]{new URL(url)});
        Class<?> c1 = ucl1.loadClass("com.jscai.classloader.Ext4Test");
        Class<?> c2 = ucl2.loadClass("com.jscai.classloader.Ext4Test");
        System.out.println("They aresame class : " + (c1 == c2));

console

They are same class : false


        虽然类名是一样的,但是使用的类加载器实例不同,所以JVM不认为这俩是同一个class,两者就不想等了。而上个例子中,两个Inner4Test的class相等,是因为JVM的类加载实例都是单例,同名和同加载器实例的两个条件都符合了。

        同样我们可以把类加载器实例换成一样的,两个class就相等了:

        URLClassLoader ucl1 = new URLClassLoader(new URL[]{new URL(url)});
        Class<?> c1 = ucl1.loadClass("com.jscai.classloader.Ext4Test");
        Class<?> c2 = ucl1.loadClass("com.jscai.classloader.Ext4Test");
        System.out.println("They aresame class : " + (c1 == c2));

console

They are same class : true

 

        因此,根据这个特点,我们可以自定义类加载器,来实现动态加载类(装载):一方面管理好现有的实例对象,另一方面监听是否有jar更新,如果有更新就重新生成加载器实例去加载这些class,这样新的class就能代替旧的class,就能实现动态加载而避免整体重启了。例如Tomcat的热部署。   (当然,具体实现还是很复杂的,没有我说的那么简单!)

 

---- 源码参考JDK-7

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值