classLoader双亲委托与类加载隔离

96
行径行
0.4 2018.10.30 15:42 字数 3779

虽然前面把class文件的产生到加载使用流程说了一遍,但是还是想具体看看classLoader的双亲委托具体是如何运行的,有什么利弊。

还有想看看不同类加载器的不同命名空间带来那些好处和实际有那些应用?并且想对ClassLoader加载类这个过程进行更加底层的了解,通过阅读源代码和自定义类加载器方式实践。

双亲委托机制?

还是先看看JVM中的类加载器层次结构如下:

Bootstrap classLoader
             /\
            /||\
        Extenssion ClassLoader
             /\
            /||\
      Application ClassLoader
        /|         |\
User ClassLoader    User ClassLoader(自定义类加载器)

我们应用程序中的所有类,都是这几种类加载器加载的。当JVM从某个二进制字节流中读取类的时候,具体这几个类加载器是如何工作的呢?它们之间除了看上去的父子层次关系,在具体的加载类的时候,又有怎样的关系?

什么是双亲委托,原理机制是什么?

JVM中的classLoader在搜索某个类的时候,是使用双亲委托模型机制工作的。

该模型的前提条件就是:除了JVM自带的BootstrapClassLoader引导类加载器外,其他的类加载器都必须属于自己的父加载器(不是以继承方式实现父子关系,而是使用组合包含关系)。所以,这种父子层级之间的关系,就将某个类加载过程给规定好了。
我们也可以从java.lang.ClassLoader.class中看到这种组合模式形成的类加载器父子层级关系:

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;   //父类加载器s
...
}

所以,基于这种组合模式形成的类加载器父子层级关系背景下,双亲委托其工作过程:(假设C加载器是最顶层的Bootstrap引导类加载器)

如果一个类加载器(classLoader)A收到了加载类的请求,那么A首先不会自己去尝试加载这个类,而是把这个”请求”委托给A的父加载器B去完成,调用getParent()方法可以得到自己的父加载(若方法返回null表示该加载器为引导类加载器)。加载器B也会把该请求委派给自己的父加载器C。在加载该类的每一层都会进行如此父类处理委托。

因此所有的加载请求最终都会发送到顶层的启动类加载器(BootStrap ClassLoader)中,只有当父加载器反馈无法完成这个加载请求(在它的搜索范围i额没有找到所需要的类),子加载器才会尝试自己去加载。

eg:
请求加载类tclass,根据当先系统上下文中得到当前的类加载器A,判断A是否有父加载器,若是有,则将该请求发送给父加载器B,在请求发送给C。

若是C无法加载该tclass(Bootstrap加载/lib下的类),就让B加载,若是B成功加载则完成该类tclass的加载,成功生成对应的java.lang.Class实例,A不需要在进行加载。若是B也无法加载,则最后交给A加载。

若是A加载成功,返回Class实例,若是加载失败,抛出class相关的异常,程序结束。

同时,可以查看JDK的源代码中是如何表达该过程的:
java.lang.ClassLoader中对于该方法的解释是这样的:(英文翻译有点渣哈,╮(╯▽╰)╭…)
若是要加载name指定的二进制字节码流文件,类加载器默认会按照以下步骤来搜索类:
a. 调用findLoadedClass方法来检查该类是否已经被类加载器加载了。
b. 如果当前上下文的类加载器的父加载器不为空,则调用父加载器的loadClass方法来查找类;若是没有父加载器,则使用JVM内建的Bootstrap加载器来查找类。
c.最后,才是调用findClass方法去查找该类。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        //A. First, check if the class has already been loaded
        Class c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {   //B.有父加载器,则委托父加载器调用loadClass方法查找加载类
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name); //B.无父加载器,则委托JVM内建启动类加载器加载该类
                }
            } 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);    //C.若是所有父加载器都无法加载该类字节码,则调用findClass方法去查找类。
                // 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);
        }
        return c;
    }
}

我们可以看到第三步骤C中,当所有父加载器都无法加载该类的时候,需要调用findClass方法。

而源代码中该类仅仅只有一个protected访问权限的方法声明,根本没有实现内容,侧面就说明了,这个方法是提供给开发人员自定义拓展用的

用于定义自定义的ClassLoader,覆盖该findClass方法。那么当在程序运行中,在双亲委托无法加载该类的最后一步,就是需要使用我们自定义的类加载器调用自定义的findClass来覆盖如何实现加载该类的细节。

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

具体如何自定义类加载器,在后面会说啦。。下图是双亲委托模型的流程图:


load_class.png

为什么要使用这种双亲委托模型?

使用双亲委托模型好处有以下几点:

  • 可以避免java类的重复加载,当父类加载器已经加载了,那么子加载器就没有必要重新加载一遍。而java类的双亲委托模型的搜索过程总比类加载器再加载一次耗时少,节省资源把。

  • 可以避免用户自定义同路径类对于J2SE平台自己定义的核心API的破坏。例如java.lang.Object类,它存放在rt.jar包中,无论哪一个类加载器需要加载这个类,最终都是委派给处于最顶端的启动类加载器(Bootstrap ClassLoader)进行加载,因此,Object类在所有程序各种类加载器环境中(也相当于类加载器命名空间中)都是同一个类(没办法,因为引导类加载器是JVM启动自带首先启动的,是第一次加载所有基础类库的类加载器)。
    相反,若是不使用双亲委托机制,由各个类加载器去自行加载的话,如果用户自定义了一个java.lang.Object类,放在Classpath中,那么系统将会出现多个不同的Object类,导致java类型体系中最基础的行为也就无法保证正常运行。双亲委托模型保证了java程序运行的稳定。

  • 基于JVM标识每个类的唯一性需要与类加载器一同来判断,那么,通过我们自定义的类加载器加载的类,就能很灵活和方便的与其他甚至同名的类区分开来,进行隔离使用。大大增强了我们对类的使用。

这种委托机制的利弊,如何理解双亲委派模型的被破化?

JVM规范中,双亲委托模型并不是一个强制性的约束,而是java设计者推荐给开发者的类加载实现方式。在java的世界中大部分的类加载器都遵循这个模型。但到目前为止,该模型主要出现以下几次大规模”被破坏”的情况:(参考《深入理解JVM虚拟机》)

a. 双亲委托模型是在JDK1.2之后出现的。在此之前类加载器和抽象类java.lang.ClassLoader就已经存在了。所以为了向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一个新的protected方法findClass(),(在上面的源代码中也可以看见)。
而在双亲委托模型未设计出前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为JVM在进行类加载的时候,会调用加载器私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()方法。
参考源码(java.lang.ClassLoader.java)
私有只是用于虚拟机调用来加载类的入口:

// This method is invoked by the virtual machine to load a class.
    private Class loadClassInternal(String name)
        throws ClassNotFoundException
    {
        // For backward compatibility, explicitly lock on 'this' when
        // the current class loader is not parallel capable.
        if (parallelLockMap == null) {
            synchronized (this) {
                 return loadClass(name);
            }
        } else {
            return loadClass(name);
        }
    }

而在JDK1.2双亲委托出现之后,不提倡用户再去覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()方法完成加载,在loadClass()方法父类加载失败,则会调用自己的findClass方法完成加载,这样就保证写出来的类加载器是符合双亲委托规则的。

b. 二次破环是该双亲委托模弊端引起的。该模型能很好的解决各个类加载器对基础类的统一问题。因为它们总是作为被用户代码调用的API,但是问题就来了。如果基础类又要调用回用户的代码,该如何实现?。

典型例子:JNDI服务,它的代码由启动类加载器加载(在rt.jar中),但JNDI目的就是对整个程序的资源进行几种管理和查找,需要调用由每个不同独立厂商实现并且部署在应用程序的ClassPath下的JNDI接口提供者的代码。但是在应用启动时候读取rt.jar包时候,是不认识这些三方厂商定义的类的,那么如何解决?

java设计团队引入了一个新设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时候,还未设置,将会从父线程中继承一个。如果在应用程序全局范围都没有设置,默认是appClassLoader类加载器。

class Thread implements Runnable {
     ...
    /* The context ClassLoader for this thread */
    private ClassLoader contextClassLoader;
    
@CallerSensitive
    public ClassLoader getContextClassLoader() {
        if (contextClassLoader == null)
            return null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ClassLoader.checkClassLoaderPermission(contextClassLoader,
                                                   Reflection.getCallerClass());
        }
        return contextClassLoader;
    }
    //
     public void setContextClassLoader(ClassLoader cl) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("setContextClassLoader"));
        }
        contextClassLoader = cl;
    }
    /**
     * Returns a reference to the currently executing thread object.
     *
     * @return  the currently executing thread.
     */
    public static native Thread currentThread(); //用于获取当前现在运行程序的线程
...
}

如何自定义类加载器

那么我们如何实现自定义类加载呢,其实很简单,只要继承抽象的java.lang.ClassLoader类即可。然后重写findClass()方法,实现自己类加载器加载类的具体规则和实现。那我们为什么还要自己定义类加载器呢?好处多啊,在上文也说到了,具体下文说到。

定义自已的类加载器分为两步:(其实也可以重写loadClass方法的,不过也就破化了双亲委托模型)

  1. 继承java.lang.ClassLoader
  2. 重写父类的findClass方法

父类有那么多方法,为什么偏偏只重写findClass方法?
因为JDK已经在loadClass方法中帮我们实现了ClassLoader搜索类的算法,当在loadClass方法中搜索不到类时,loadClass方法就会调用findClass方法来搜索类,所以我们只需重写该方法即可。

来具体实现一个实践实践看看:

  1. 在c盘桌面放置clazz文件夹,里面由ClassTest.class文件。(可以加载本地,网络上的,数据库等等位置的类文件)
public class ClassLoaderTest extends ClassLoader {
    
@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File file = getClassFile(name);
        try {
            byte[] bytes = getClassBytes(file);
            return defineClass(name,bytes, 0, bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return super.findClass(name);
    }
    private File getClassFile(String name) {
        // return new File(name);
        return new File("C:\\Users\\XianSky\\Desktop\\clazz\\ClassTest.class");
    }
    private static byte[] getClassBytes(File file) throws Exception {
         FileInputStream fis = new FileInputStream(file);
         ByteArrayOutputStream aos = new ByteArrayOutputStream(fis.available());
         byte[] bytes = new byte[fis.available()];  //使用fis.avaliable()方法确保整个字节数组没有多余数据
         fis.read(bytes);
         aos.write(bytes);
         fis.close();
         return aos.toByteArray();
    }
    public static void main(String[] args) throws Exception {
        ClassLoaderTest ct = new ClassLoaderTest();
        Class c = Class.forName("clazz.ClassTest", true, ct);
        System.out.println(c.getClassLoader());
    }
}
    //输出
    clazz.ClassLoaderTest@15db9742

可以看到获取本地file路径下的class文件的二进制字节码,然后使用自定义的类加载器进行加载,输出的就是自定义的类加载器。这种形式的类字节码加载过程很显然易见,是因为当前类加载器的所有父类加载器都查找不到该类字节流,所以最后是使用我们自定义的类加载实现加载的。

2.可以使用findClass方法加载同样环境下的同一个类文件,也能达到不能类加载加载相同类也是不同的结果。
在eclipse中项目中存在以下目录结构文件,与上述A不同的是,我们自定义的加载器的父类加载器AppClassLoader其实是可以加载到clazz.ClassTest类的,根据双亲委托加载想,那父类都能加载了,为什么第一个clazz.ClassTest的类加载器还是我们自定义的ClassLoaderTest呢?

--projectName
    -src
        -clazz
            -ClassLoaderTest.java
            -ClassTest.java

我们也可以加载同一个路径下的java类,重点看看程序运行输出的对象示例的内存地址。(当前开发工具eclipse编译后的类字节通过appClassLoader是可以加载得到的)

public class ClassLoaderTest extends ClassLoader {
    
@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
//      File file = getClassFile(name);
//      try {
//          byte[] bytes = getClassBytes(file);
//          return defineClass(name,bytes, 0, bytes.length);
//      } catch (Exception e) {
//          e.printStackTrace();
//      }
//      return super.findClass(name);
        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 (Exception e){
            throw new ClassNotFoundException(name);
        }
    }
    private File getClassFile(String name) {
        // return new File(name);
        return new File(name);
    }
    private static byte[] getClassBytes(File file) throws Exception {
         FileInputStream fis = new FileInputStream(file);
         ByteArrayOutputStream aos = new ByteArrayOutputStream(fis.available());
         byte[] bytes = new byte[fis.available()];  //使用fis.avaliable()方法确保整个字节数组没有多余数据
         fis.read(bytes);
         aos.write(bytes);
         fis.close();
         return aos.toByteArray();
    }
    public static void main(String[] args) throws Exception {
        ClassLoaderTest ct = new ClassLoaderTest();
        Object obj = ct.findClass("clazz.ClassTest").newInstance();
        System.out.println(obj.getClass());
        System.out.println(obj.getClass().getClassLoader());
        Class c = Class.forName("clazz.ClassTest", true, ct);
        System.out.println(c.getClassLoader());
        System.out.println(clazz.ClassLoaderTest.class.getClassLoader());
    }
}
//输出
class clazz.ClassTest
clazz.ClassLoaderTest@6d06d69c
clazz.ClassLoaderTest@6d06d69c
sun.misc.Launcher$AppClassLoader@73d16e93

可以看到,第一次使用自定义类加载findClass加载的class.ClassTest是我们自定义加载器的加载,最后那个clazz.ClassLoaderTest.class是appClassLoader加载,这就说明了我们自定义的类加载的父类加载器是可以访问查找到这个类,至于为什么会这样,我就根据自己的理解将加载class.ClassTest类时序图过程画出来:

myloader_process.png

这里要注意的一个是:在双亲委托过程中,真正完成类加载工作的类加载器和启动这个加载过程的类加载器,可能不是同一个。真正完成类加载工作是通过defineClass来实现的;而启动类的加载过程是通过调用loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initating loader)。在JVM中,那个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器

所以这里的clazz.ClassTest中,引导类加载器相当于类的初始加载器,而自定义的类加载是定义加载器。所以clazz.ClassTest.class.getClassLoader方法返回的是我们自定义的类加载。(这也仅仅是我自己的理解,仅供参考,也有可能不对啊,还需努力…)

  1. 通过重写loadClass方法来自定义类加载器
public class ClassLoaderTest2 {
    public static void main(String[] args) throws Exception{
        //通过重写loadClass方法来自定义类加载器
        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 (Exception e){
                    throw new ClassNotFoundException(name);
                }
            };
            
        };
        Object obj = myLoader.loadClass("clazz.ClassLoaderTest2").newInstance();
        System.out.println(obj.getClass());
        System.out.println(obj.getClass().getClassLoader());
        System.out.println(clazz.ClassLoaderTest2.class.getClassLoader());
        System.out.println(obj instanceof clazz.ClassLoaderTest2);
        
    }
    //output:
    class clazz.ClassLoaderTest2
    clazz.ClassLoaderTest2$1@7852e922
    sun.misc.Launcher$AppClassLoader@73d16e93
    false
}

可以看见,不同的类加载器加载相同的一个类,会对instanceof造成一定的影响。虽然存在了两个一样的ClassLoaderTest2,但是是两个不同类加载器加载的。一个是应用类加载器加载,另外一个是我们自定义类加载器加载的,所以是不同的两个类。

类加载器与类标识的命名空间?

也就是说,在JVM搜索类的时候,如何判断两个相同全限定名的类是否是同一个类?也就会对后面判断该类是否已经加载的流程有重要影响了。

JVM在判定两个class是否相同时候,不仅要判断两个类是否相同(equals,hashCode,全限定名..),还要判断该类是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才会认为这两个class是相同

关于类加载器和类共同标识命名空间和类加载的共享和隔离问题,可以拿tomcat来作为例子说明。tomcat内部由自己定义的类加载器,还可以通过tomcat安装目录下的catalina.properties文件灵活配置应用共享类库,和应用工之间单独使用的类库。这个,就放在下次说了,我也好好看看,思考思考。。。

参考: [深入理解Java虚拟机:JVM高级特性与最佳实践].周志明
深入探讨 Java 类加载器-成富

Java
Gupao