JVM十:虚拟机类加载机制(2)

1. 类加载器作用

          类加载器虽然只用于实现类的加载动作,但它在Java程序起到的作用却远大于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。通俗而言:比较两个类是否“相等”,只有在这两个类时由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等


2. 类加载器 与 instanceof 关键字

     上文所说的“相等”,包括代表类的对象的 equals() 方法、isAssignableFrom()方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。

如果没有注意到类加载器的影响,在某些情况下可能会产生具有迷惑性的结果,以下代码演示了不同的类加载器对 instanceof 关键字运算结果的影响:

/**
 * 类加载器与instanceof关键字演示
 * 
 * @author zzm
 */
public class ClassLoaderTest {

    public static void main(String[] args) throws Exception {

        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };

        Object obj = myLoader.loadClass("org.fenixsoft.classloading.ClassLoaderTest").newInstance();

        System.out.println(obj.getClass());
        System.out.println(obj instanceof org.fenixsoft.classloading.ClassLoaderTest);
    }
}

 

运行结果:

这里写图片描述

分析

     以上代码中构造了一个简单的类加载器,它可以加载与自己在同一路径下的Class文件。使用这个类加载器去加载一个名为“org.fenixsoft.classloading.ClassLoaderTest”的类,并实例化这个类的对象。

从输出结果的第一行可以看出,此对象确实是类“org.fenixsoft.classloading.ClassLoaderTest”实例化出的对象,但从第二句看出,此对象与类“org.fenixsoft.classloading.ClassLoaderTest”做所属类型检查的时候却返回了false,这是因为虚拟机中存在了两个 ClassLoaderTest 类,一个是由系统应用程序类加载器加载的,另外一个是由我们自定义的类加载器加载的,虽然都来自同一个Class文件,但依然是两个独立的类,做对象所属类型检查时结果自然返回 false




二. 双亲委派模式

1. 虚拟机角度中的类加载器

虚拟机的角度来讲,只存在两种不同的类加载器:

  • 启动类加载器(Bootstrap ClassLoader):此类加载器使用C++实现,是虚拟机自身的一部分。
  • 所有其他类加载器:由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader

2. Java开发人员角度中的类加载器

Java开发人员的角度来看,类加载器还可以划分得更细致一些,绝大部分Java程序都会使用到以下3种系统提供的类加载器:

扫描二维码关注公众号,回复: 4722247 查看本文章

(1) 启动类加载器(Bootstrap ClassLoader)

       此类加载器负责将存放在 <JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在lib 目录中也不会被加载)类库加载到虚拟机内存中。

启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,直接使用null代替即可。如下代码java.lang.ClassLoader.getClassLoader() 方法的代码片段:

    【ClassLoader.getClassLoader() 方法的代码片段】
   /**
     * Returns the class loader for the class.  Some implementations may use null to represent the bootstrap class loader. This method will return null in such implementations if this class was loaded by the bootstrap class loader.
     */
    public ClassLoader getClassLoader() {
        ClassLoader cl = getClassLoader0();
        if (cl == null)
            return null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ClassLoader cc1 = ClassLoader.getCallerClassLoader();
            if(cc1 != null && cc1 != c1 && !c1.isAncestor(cc1)){
                sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);
            }
        }
        return cl;
    }

(2)扩展类加载器(Extension ClassLoader)

ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将Java_Home /lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中,开发者可以直接使用标准扩展类加载器。

(3)应用程序类加载器(Application ClassLoader)

AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,因此一般称为系统类加载器

它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。


3. 双亲委派模型

(1)概念

我们的应用程序都是由这3 种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。这些类加载器之间的关系如下图:

这里写图片描述

      上图展示的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,而这种父子关系一般通过组合关系来实现,而不是通过继承(代码中体现)。

(2)工作过程

        如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器,每一个层次的加载器都是如此,依次递归,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成此加载请求(它搜索范围中没有找到所需类)时,子加载器才会尝试自己加载。

(3)模式优点

      使用双亲委派模型来组织类加载器之间的关系,好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。

例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,用户编写了一个java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但是永远无法被加载运行。

(4)双亲委派模型的系统实现

双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现却很简单,实现集中在java.lang.ClassLoader的loadClass()
方法中,在其方法中,主要判断逻辑如下:先检查是否已经被加载过,

  • 若没有被加载过,则接着判断父加载器是否为空。
    • 若不为空,则调用父类加载器loadClass()方法。
    • 若父加载器为空,则默认使用启动类加载器作为父加载器(递归调用)。
  • 如果父加载失败,则抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

这里写图片描述

【双亲委派模型的实现】

protected synchronized Class<?> loadClass(String name,boolean resolve)throws ClassNotFoundException{
    //check the class has been loaded or not
    Class c = findLoadedClass(name);
    if(c == null){
        try{
            if(parent != null){
                c = parent.loadClass(name,false);
            }else{
                c = findBootstrapClassOrNull(name);
            }
        }catch(ClassNotFoundException e){
            //if throws the exception ,the father can not complete the load
        }
        if(c == null){
            c = findClass(name);
        }
    }
    if(resolve){
        resolveClass(c);
    }
    return c;
}

(5)注意(findClass方法)

在查看学习以上ClassLoader的实现后,注意一个地方,即“如果父加载失败,则抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载”这一步逻辑,进一步查看findClass()方法:

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

    没错!此方法没有具体实现,只是抛了一个异常,而且访问权限是protected这充分证明了:这个方法就是给开发者重写用的,即自定义类加载器时需实现此方法!

三. 实例验证双亲委派模式

在了解了以上多种类加载器和双亲委派模式的理论实现后,对这个“双亲委派模式”运作,自我感觉还是有点迷糊,所以接下来自定义一个类加载器来亲自测试验证“双亲委派模式”的运行实现,从实践角度出发来一探究竟。

在了解完以上双亲委派模型破坏双亲委派模型后,我们得知自定义类加载器有以下两种方式:

  • 采用双亲委派模型:只需要重写ClassLoaderfindClass()方法即可

  • 破坏双亲委派模型:重写ClassLoader的整个loadClass()方法(因为双亲委派模型的逻辑主要实现就在此方法中,若我们重写即可破坏掉。)

不过此次实践就是为了来探究验证双亲委派模型,多以我们当然是采取第一种方法来自定义类加载器。


1. 自定义类加载器

首先第一步我们需要自定义一个简单实现的类加载器,通过第二大点最后的讲解后对自定义类加载器的过程稍有了解,构建重点:自定义的MyClassLoader继承自java.lang.ClassLoader,就像上面说的,只需实现findClass()方法即可。

注意:此类里面主要是一些IO和NIO操作,其中defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class,只要二进制字节流的内容符合Class文件规范即可。

/*
 * 自定义类加载器
 */
public class MyClassLoader extends ClassLoader{  

    public MyClassLoader(){   
    }

    public MyClassLoader(ClassLoader parent){
        super(parent);
    }

     @Override  
        public Class<?> findClass(String name) throws ClassNotFoundException {  
            //打印日志,表示使用的是自定义的类加载器
            System.out.println("Use myclassloader findClass method.");  
            //获取的fileName为: EasyTest.class
            String fileName = name.substring(name.lastIndexOf(".")+1)+".class";  

            byte[] bytes = loadClassData("E:\\test_eclipse\\JvmProject\\"+fileName); 
            return defineClass(name, bytes, 0, bytes.length);  
        }  


        public byte[] loadClassData(String name) { 
            //这里参数name路径为:E:\test_eclipse\JvmProject\EasyTest.class  
            FileInputStream fileInput = null;  
            ByteArrayOutputStream bytesOutput = null;  
            try {  
                fileInput = new FileInputStream(new File(name));  
                bytesOutput = new ByteArrayOutputStream();  
                int b = 0;  
                while ((b = fileInput.read()) != -1) {  
                    bytesOutput.write(b);  
                }  
                return bytesOutput.toByteArray();  
            } catch (Exception e) {  
                e.printStackTrace();  
            } finally {  
                try {  
                    if(fileInput != null)  
                        fileInput.close();  
                } catch (IOException e) {  
                    e.printStackTrace();  
                }  
            }  
            return null;  
        }  
}

 

注意:此时是使用MyClassLoader 自定义加载器来加载 EasyTest类(空实现,只是一个空壳),需要将该类编译后生成的EasyTest.class文件放到E:\test_eclipse\JvmProject\ 路径中,而测试的main 方法如下:

    /*
     * 测试自定义加载器的Main方法
     */
    public static void main(String[] args){  
        MyClassLoader myClassLoader = new MyClassLoader();  
        try {  
            Class<? extends Object> testClass = myClassLoader.loadClass("org.fenixsoft.classloading.EasyTest");  
            Object obj = testClass.newInstance();  
            System.out.println(obj.getClass().getClassLoader().toString());  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  

输出结果:

这里写图片描述

结果分析

查看上图红框显示结果可得,虽然程序的确使用自定义类加载器加载的,可是显示并非是 MyClassLoader而是应用程序类加载器 AppClassLoader 加载的,而这种结果的造成原因正是因为 双亲委派模式。后面通过实例来探究双亲委派模式的源码实现。


2. 源码探究

回顾一下在讲解双亲委派模式中的相关知识点及层次图,启动类加载器只是JVM的一个类加载工具,处于层次图的最上层,严格来说它并无遵守此模式,所以我们从它的下层 —— 扩展类加载器应用程序类加载器开始分析。

查看其输出结果可知,其中涉及到了 sun.misc.Launcher类,查看此类结构图:

这里写图片描述

(1)sun.misc.Launcher类构造方法

点进去 sun.misc.Launcher类进行查看,你会发现在它的构造方法中创建了扩展类加载器 ExtClassLoader的实例,并用该实例创建应用程序类加载器 AppClassLoader的实例:

public Launcher() {  
   // 创建扩展类加载器  
   ClassLoader extcl;  
   try {  
       extcl = ExtClassLoader.getExtClassLoader();  
   } catch (IOException e) {  
       throw new InternalError("Could not create extension class loader");  
   }  

   // 通过扩展类加载器来创建应用程序类加载器  
   try {  
       loader = AppClassLoader.getAppClassLoader(extcl);  
   } catch (IOException e) {  
       throw new InternalError("Could not create application class loader");  
   }
   ...... 
  }

(2)扩展类加载器的实例化

由上可知,在Launcher类构造方法中先实例化了扩展类加载器,查看其实现过程:

static class ExtClassLoader extends URLClassLoader {  
    public static ExtClassLoader getExtClassLoader() throws IOException{  
        final File[] dirs = getExtDirs();  
        ......  
        return new ExtClassLoader(dirs);  
        ......  
    }  

    private static File[] getExtDirs() {  
        // 加载路径  
        String s = System.getProperty("java.ext.dirs");  
        ......  
    }  
    public ExtClassLoader(File[] dirs) throws IOException {  
        // 这里的第二个参数含义是指定上级类加载器
        super(getExtURLs(dirs), null, factory);  
        this.dirs = dirs;  
    }  
    ......  
}  

注意:查看ExtClassLoader 的构造方法中,调用了父类构造方法,其中传入的第二个参数为null,代表扩展类加载器没有上级类加载器!

(3)应用程序类加载器的实例化

紧接着来看应用程序类加载器的实例化过程:

static class AppClassLoader extends URLClassLoader {  
    // extcl是ExtClassLoader类的实例  
    public static ClassLoader getAppClassLoader(final ClassLoader extcl)  
            throws IOException{  
        final String s = System.getProperty("java.class.path");  
        final File[] path = (s == null) ? new File[0] : getClassPath(s);  
        ......  
        return new AppClassLoader(urls, extcl);  
        ......  
    }  

    AppClassLoader(URL[] urls, ClassLoader parent) {  
        // 应用程序类加载器的上级是扩展类加载器  
        super(urls, parent, factory);  
    }  
    ......  
}  

注意:查看AppClassLoader 的构造方法中,调用了父类构造方法,其中传入的第二个参数为parent,也就是扩展类加载器extcl,直接从代码的角度证明 扩展类加载器是应用程序加载器的上级!

扩展类加载器是应用程序加载器的上级(已证明)

(4)ClassLoader的构造方法

在我们创建自定义类加载器时,继承了ClassLoader类,所以程序在运行时会先调用父类——ClassLoader的构造函数,来查看其实现:

private ClassLoader parent;  
private static ClassLoader scl;  

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

private ClassLoader(Void unused, ClassLoader parent) {  
    this.parent = parent;  
}  

public static ClassLoader getSystemClassLoader() {  
    // scl在改方法中创建  
    initSystemClassLoader();  
    .....,  
    return scl;  
}  

private static synchronized void initSystemClassLoader() {  
    if (!sclSet) {  
        if (scl != null)  
            throw new IllegalStateException("recursive invocation");  
        // 获取Launcher类的实例  
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();  
        if (l != null) {  
            Throwable oops = null;  
            // 参照前面Launcher类的构造函数,scl就是应用程序类加载器  
            scl = l.getClassLoader();  
            ......  
            sclSet = true;  
        }  
    }  
}  

 

由以上可证明一个级别关系:

应用程序类加载器是自定义类加载器的上级。

我们在之前介绍过双亲委派模式的工作原理,通过前面一系列的分析后,再次叙述一遍,感受更深:如果一个类加载器收到了加载类的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给上级类加载器去完成,每一个层次的类加载器都是如此,所有加载器的加载请求最终都应该传送至最顶层的类加载器中(扩展类加载器),只有当上级类加载器反馈自己无法完成这个加载请求(它的类加载范围中没有找到所需的类)时,下级类加载器才会去尝试自己加载这个类。

由以上一系列源码探究可窥得这双亲委派模式的工作原理,并且清楚了为何最终加载 EasyTest类的是应用程序类加载器而并非是我们自定义的类加载器


3. 使用自定义类加载器 加载

如今以上的道理算是理解了,可现在偏偏需要使用自定义的类加载器加载,应该如何修改呢?

在测试main() 方法中创建自定义类加载器的代码:

MyClassLoader myClassLoader = new MyClassLoader();  

修改成:

MyClassLoader myClassLoader = new MyClassLoader(ClassLoader.getSystemClassLoader().getParent());

即把自定义ClassLoader的父加载器设置为扩展类加载器,这样父加载器加载不到EasyTest.class文件,就交由子加载器MyClassLoader来加载了(别忘了在自定义类加载器中要写对应的构造方法)。

运行结果如下:

这里写图片描述


4. .class和getClass()

这两者看起来类似,但其实有很大区别,如下:

  • .class用于类名,getClass()是一个final native的方法,因此用于类实例。
  • .class在编译期间就确定了一个类的java.lang.Class对象,但是getClass()方法在运行期间确定一个类实例的java.lang.Class对象

5. ClassLoader.getResourceAsStream(String name)

       不知道细心的朋友有没有注意到此方法,在本博文的第一个例子中出现过。在我们自定义的类加载器中所占篇幅最大的就是一个loadClassData 方法,将文件数据转换为字节数组,但是还有第二种方法,就是采用系统提供的 ClassLoader.getResourceAsStream(String name) 方法,根据此方法获取到数据输入流,通过此输入流获取字节数组,最终传入defineClass 方法即可,代码如下

【自定义类加载器中的 findClass方法】
@Override  
        public Class<?> findClass(String name) throws ClassNotFoundException {  
         try {
             String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
             InputStream is = getClass().getResourceAsStream(fileName);
             if (is == null) {
                 return super.loadClass(name);
             }
             byte[] b = new byte[is.available()];
             is.read(b);
             return defineClass(name, b, 0, b.length);
         } catch (IOException e) {
             throw new ClassNotFoundException(name);
         }
        }  

此方法是用来读入指定的资源的输入流,并将该输入流返回给用户用的,资源可以是图像、声音、.properties文件等,资源名称是以”/”分隔的标识资源名称的路径名称。有兴趣可查看其源码,实现也很简单。


四. 总结

文章总结

本篇博文的内容并不少,首先第一点初步介绍了类加载器的概念、作用,用一个简单实现的类加载器来证明两个类之间的“相等”比较。接着在第二点中详细介绍了三种不同的类加载器,并学习了双亲委派模式,将启动、扩展、应用程序类加载器的层次归纳到双亲委派模型中,理解了此模型的优点、工作原理、源码逻辑等。

如果说以上两点比较偏理论的话,那么在第三点中采用自定义类加载器实例来探究双亲委派模式的工作原理,从源码的角度来分析ClassLoader的实现及类加载情况,此部分尤为重要。

猜你喜欢

转载自blog.csdn.net/weixin_40234548/article/details/81586611