类加载器

一、类加载过程

一个非数组类的加载阶段(加载阶段通过一个类的全限定名来获取描述此类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 loadClass() 方法),这个动作放到Java虚拟机外部去实现数组类型不通过类加载器创建,它由 Java 虚拟机直接创建

类加载器用于实现类的加载动作。

二、

package JvmTest;

import java.io.IOException;
import java.io.InputStream;

/*
 * 类加载器与instanceof关键字演示
 *   (1)Java序列化就是指把Java对象转换为字节序列的过程
 *   (2)Java反序列化就是指把字节序列恢复为Java对象的过程。
 */
public class Nine {
    public static void main(String[] args) throws Exception{
        //自己重写的类加载器
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try{
                    //加载在同一路径下Class文件
                    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);   // 把自己字节序列恢复成Java对象

                }catch (IOException e){
                    throw new ClassNotFoundException(name);
                }
            }
        };
        //使用myloader类加载去加载名为JvmTest.Nine的类,并实例化对象obj
        Object obj = myLoader.loadClass("JvmTest.Nine").newInstance();

        System.out.println(obj.getClass()); //对象obj的确是类JvmTest.Nine实例化的对象
     //instanceof Java中的一个双目运算符,用来测试一个对象是否为一个类的实例
System.out.println(obj instanceof JvmTest.Nine); //所属类检查,判断obj是否属于类 } }
class JvmTest.Nine
false

上述代码重写了一个类加载器,使用这个类加载器去加载一个名为JvmTest.Nine的类,并创建了一个对象obj。结果发现obj的确是JvmTest.Nine的一个实例,但是做对象所属类检查的时候,却输出false。原因如下:

因为虚拟机出现了两个JvmTest.Nine类,一个是系统应用程序类加载器加载的,另一个是自定义myLoader类加载器加载的。虽然都来自同一个Class文件,但确实不同的独立的类。再次验证那句话:

即使两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类就不相等。

 

 三、双亲委派模型

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

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

类加载器可以划分得更细致一些,主要分为以下三种:

  1. 启动类加载器(Bootstrap ClassLoader),最顶层的类加载器,由C++实现,无法被Java程序直接引用。负责加载<JAVA_HOME>\lib目录下的jar包的类或者被-Xbootclasspath参数所指定的路径中的所有类。
  2. 扩展类加载器(Extension ClassLoader),主要负责加载<JAVA_HOME>\lib\ext目录下的jar包和类,或被java.ext.dirs系统变量所指定的路径中所有类库,开发者是可以直接使用的。
  3. 应用程序类加载器(Application ClassLoader),面向我们用户的加载器,负责加载当前应用classpath(用户类路径)下的所有jar包和类。应用程序类加载器也称为系统类加载器。

 

类加载器的双亲委派模型

每一个类都有一个对应它的类加载器。系统中的ClassLoader在协同工作的时候会默认使用双亲委派模型:在类加载的时候,系统会首先判断当前类是否被加载过,已经被加载的类会直接返回,否则才会尝试加载。首先会把该请求委派父类加载器的loadClass()处理,因此所有的请求最终都应该传送到顶层的启动类加载器BootstrapClassLoader中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器BootstrapClassLoader作为父类加载器。流程图如下所示:

 由此可见检查类是否被加载是从下往上的,而尝试加载类是从上往下的。

 

 

 

 

 

 

 

 

 

 

                        

 

 

 

 

 

每个类加载都有一个父类加载器,可以通过下面的程序来验证。

package JvmTest;
/*
 *每一个类都有一个父类加载器
 */
public class Eleven {
    public static void main(String[] args) {
        System.out.println("Eleven 's ClassLoader is:" + Eleven.class.getClassLoader());
        System.out.println("The Parent of Eleven's ClassLoader is:" + Eleven.class.getClassLoader().getParent());
        System.out.println("The GrandParent of Eleven's ClassLoader is:" + Eleven.class.getClassLoader().getParent().getParent());
    }
}
ClassLoaderDemo's ClassLoader is:sun.misc.Launcher$AppClassLoader@18b4aac2
The Parent of ClassLoaderDemo's ClassLoader is:sun.misc.Launcher$ExtClassLoader@1540e19d
The GrandParent of ClassLoaderDemo's ClassLoader is:null

从控制台输出可以看出,应用程序类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器为null,注意null并不表示为空,而是启动类加载器。

这里解释一下双类委派模型名字的缘来:这里的双亲更多表达的是“父母这一辈儿”,并不是真的有父/母类加载器。另外,类加载器之间的“父子”关系也不是通过继承来实现的,使用组合关系来复用父类加载器,是由“优先级”来决定。

以下是双亲委派模型的大致解析:

package JvmTest;
/*
 * 双亲委派模型的源码分析(大体框架)
 * 有一个递归的思想
 */
public class Eleven {
    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
        //首先,检查请求的类是否已经被加载过了
        Class c = findLoadedClass(name);
        if(c == null){
            try{
                if(parent != null){
                    //调用父类的类加载器的loadClass()方法
                    c = parent.loadClass(name, false);
                }else{
                    //若父类加载器为空,则使用启动类加载器作为父类加载器
                    c = findBootstrapClassOrNull(name);
                }
            }catch (ClassNotFoundException e){
                //如果父类加载器抛出ClassNotFoundException,说明父类加载器无法完成加载请求
                throw new ClassNotFoundException(name);
            }

            if(c == null){
                //在父类加载器无法加载的时候
                //再调用本身的findClass方法来进行类加载
                c = findClass(name);
            }
        }
        //如果加载过了直接返回(resolve取false)
        if (resolve){
            resolveClass(c);
        }
        return c;
    }
}
若类加载过了直接返回(默认resolve取false);若没有被加载,则调用父类加载器的loadClass方法,若父类加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,则在抛出 ClassNotFoundException 异常后,再调用自己的 findClass 方法进行加载。

 

四、双亲委派模型的好处

双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载(JVM区分不同类的方式不仅仅根据类名相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了Java的核心API不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为java.lang.Object类的话,那么程序运行的时候,系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证。(而使用双亲委派模型,java.lang.Object存放在rt.jar之中,无论哪一个类加载器加载这个类,最终都是为委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一类)。

 

五、自定义类加载

一般类加载器有三种就够了,那为什么双亲委派模型中还要使用自定义类加载器呢?有以下原因:

  1. 加密:因为Java字节码能进行反编译,但在某些安全性较高的场景中,不允许这种情况发生。我们可以将编译后的代码用加密算法进行加密,加密后的.class文件就不能使用常规的类加载器去加载类了,而要使用我们自定义类加载器先解密,然后再加载。
  2. 动态创建:eg.动态代理
  3. 从非标准的来源加载代码:我们不用非要从class文件中获取定义此类的二进制流,还可以从数据库,网络中或者zip包中获取。

那我们如何定义自定义类加载器呢?

由于除了BootstrapClassLoader,其他类加载器均由Java实现并且全部继承自java.lang.ClassLoader,类加载时根据双亲委派模型会先一层层找到父加载器,如果加载失败,则会调用当前加载器的findClass()方法来完成加载。如果我们要自定义自己的类加载器,首先继承ClassLoader,然后覆盖findClass()方法

 

六、如果我们不想用双亲委派模型怎么办?

如果我们想打破双亲委派模型就需要重写loadClass()方法

如果我们想自定义类加载器,不想打破双亲委派模型,就重写ClassLoader类中的findClass()方法

 

posted @ 2020-12-12 21:47  Peterxiazhen  阅读(160)  评论(0编辑  收藏  举报