zoukankan      html  css  js  c++  java
  • 类装载器

      类加载就是虚拟机将java的Class文件加载到内存,并对数据进行验证,准备,解析,初始化的一个过程。将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

    一、类的生命周期

      首先附上一张类的生命周期的一张图:

      类的生命周期分为 加载、链接、初始化、使用、和卸载五个阶段。其中链接阶段又分为验证、准备和解析,使用阶段分为对象初始化、垃圾搜集和对象终结。在这些阶段中发生的顺序基本都是确定的,唯有解析阶段不确定,有可能发生在初始化之前,也有可能发生在初始化之后,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

    • 加载

      加载为类装载的第一阶段,它会获取类的的二进制流,它会将calss文件中的类的信息转换为方法区的数据结构,在java堆中生成相对应的java.lang.Class对象,作为对方法区中这些数据的访问入口。相对于类加载的其他阶段而来说,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。
      获取类的二进制流的方法:
        ①. 读取本地系统中直接加载
        ②. 将java源文件动态编译为.class文件
        ③. 读取jar文件中的.class文件
        ④. 从网络上下载.class文件

    • 链接
      ①. 验证(为了保证class流的格式是正确的)

      验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。其中包含文件格式验证(是否以0xCAFEBABE开头、版本号是否合理);元数据验证(是否有父类、是否继承了final类、非抽象类是否实现了所有的抽象方法);字节码验证(运行检查、栈数据类型和操作码数据参数是否吻合、跳转指令是否指定到合适的位置);符号引用验证(常量池中描述的类是否存在、访问的方法或字段是否存在并且有足够的权限)。

       ②. 准备(分配内存,并为类设置初始值)

        在方法区中分配内存,并为类设置初始值。例如:public static int v = 1 这段代码在准备阶段v会被设置为默认值0,而不是1,只有在初始化阶段v才会被置为1,而常量则会在初始化阶段直接置为设置的值

       ③. 解析(把类中的符号引用转换为直接引用)

        符号引用是一组符号来描述目标,可以是任何字面量。直接引用是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

    • 初始化

       ①. 执行类的构造器 static变量赋值,执行static语句 
       ②. 子类的调用前要保证父类被调用
       ③. 是线程安全的

      下面简单看一个例子:

    /**
     * 
     * @author Herrt灬凌夜
     *
     */
    class SuperClass {
        static {
            System.out.println("SuperClass init!");
        }
        public static int value=123;
    }
    class SubClass extends SuperClass{
        static {
            System.out.println("SubClass init!");
        }
    }
    public class NotInitialization {
        public static void main(String[] args) {
            System.out.println(SubClass.value);
        }
    }

       执行结果为:

    SuperClass init!
    123

      从上例可以看出,在调用SubClass.value时首先初始化了SubClass的父类SuperClass,并且执行了SuperClass类中的静态代码块,初始化了value的值,但是并没有去触发SubClass类的初始化。只有当程序”首次”并且是”主动使用”类的时候,才会执行初始化。

      主动使用:
        ①. 创建类的实例
        ②. 访问类的静态变量、或给该类的静态变量赋值
        ③. 调用类的静态方法
        ④. 反射调用类的静态方法、或反射创建类实例
        ⑤. JVM启动时被标明为启动类
        ⑥. 初始化一个类的子类

    • 使用

        ①. 对象实例化:当我们去实例化一个对象时,首先虚拟机会去常量池定位这个类的符号引用,并检查这个类是否被加载,解析和初始化过,如果没有的话就需要首先执行这几个过程,如果已经执行过这几个过程,那么虚拟机就会为新生对象分配内存(一般是指针碰撞或者空闲列表方式),将内存空间进行对象初始化(初始值全部为对应0值),设置对象头信息,将对象引入栈,执行构造器
        ②. 垃圾收集:当对象不再被引用的时候,就会被虚拟机标上特别的垃圾记号,在堆中等待GC回收(在前面的文章中有写到GC回收算法)
        ③. 对象的终结:对象被GC回收后,对象就不再存在,对象的生命也就走到了尽头

    • 卸载

      即类的生命周期走到了最后一步,程序中不再有该类的引用,该类也就会被JVM执行垃圾回收,从此生命结束…(满足下面三个条件时该类就会被回收)
        ①. 当一个类在java堆中没有任何实例时
        ②. 该类的ClassLoader已经被回收
        ③. 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

     

    二、什么是类装载器 ClassLoader

      Java类加载器(Java Classloader)是Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类到Java虚拟机的内存空间中。类通常是按需加载,即第一次使用该类时才加载。由于有了类加载器,Java运行时系统不需要知道文件与文件系统。Classloader是一个抽象类,它的实例将读入的java字节码装载到JVM中,可以单独定制,满足不同的字节码流获取方式,主要负责类装载过程中的加载阶段。
    大部分java程序会使用以下3中系统提供的类加载器:

    • 启动类加载器(Bootstrap ClassLoader): 这个类加载器负责将存放在<java_home>lib目录中的,或者被 -Xbootclasspath参数所指定的路径中的,并且在巡检识别的(仅仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录下也不会被加载)类库加载到虚拟机中。此类加载器并不继承于java.lang.ClassLoader,由原生代码(如C语言)编写,不能被java程序直接调用。
    • 扩展类加载器(Extendsion ClassLoader):此类负责加载<java_home>libext目录中的,或者被java.ext.dirs系统变量所指定的路径的所有类库,开发者可以直接使用扩展类加载器。
    • 应用程序类加载器(Application ClassLoader): 这个类加载器负责加载用户类路径(CLASSPATH)下的类库,一般我们编写的java类都是由这个类加载器加载,这个类加载器是CLassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器.一般情况下这就是系统默认的类加载器.

    三、JDK中 ClassLoader默认设计模式(双亲委派模式)

      双亲委派模式:一个类加载器接受到加载类的请求时,首先会去看自己是否加载过这个类,如果加载过则直接返回,如果没有加载过,不会立即去尝试加载这个类,而是把请求委托给父加载器,直到Bootstrap ClassLoader。因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

      我们在开发过程中偶尔会出现ClassNotFoundException的异常,那么在什么情况下才会出现这样的异常呢?

    • 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
    • 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
    • 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
    • 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
      自底向上检查类是否加载,自顶向上尝试加载类。

      在classLoader中比较重要的几个方法:

    方法名入参出参描述
    loadClass String name Class 根据Class的名字去装载这个class,并且返回这个Class
    defineClass byte[]b,int off, int len final Class 定义一个class, 给定一个byte数组,偏移量和长度,就是将class的文件以流信息传入,然后转换为一个class
    findClass String name Class 在loadClass方法中做调用,查找class,自定义classLoad时推荐是重载这个方法
    findLoadedClass String name final Class 查找已经被加载的class,如果查找不到再去加载这个类,如果找到则不去做二次加载

      在loadClass中,首先去找这个class,如果找到则返回,确保一个类只会被加载1次,否则则委托父级类加载器去加载。

       注意:此处的 parent 不是该类的父类,而是父加载器。

     

      我们看一个简单的例子:

    package com.wyx.service;
    
    public class FindClassOrder {
    
        public static void main(String[] args) {
            HelloLoder loder = new HelloLoder();
            loder.print();
        }
    }

     

    package com.wyx.service;
    
    public class HelloLoder {
    
        public void print () {
            System.out.println("I am apploader");
        }
    }

        我们直接执行main方法,发现输出的是 I am apploader

     

      然后我们将在在其他地方创建相同的类,注意包名也要相同。将此类的class文件放到E盘的tmp目录下,注意包也要一级一级创建,执行时在 VM arguments 加上-Xbootclasspath/a:E: mp

    package com.wyx.service;
    
    public class HelloLoder {
    
        public void print () {
            System.out.println("I am bootloader");
        }
    }

     

      此时再次执行,我们发现执行的结果不再是 I am apploader  而是 I am bootloader 了。

     

       上面例子中,首先我们执行时首先去 AppClassLoader 中看这个类是否在这个类加载器中被加载,没有找到就去ExtClassLoader中找,ExtClassLoader中也没有找到,于是去BootStrapClassLoader 中找,此时也没有找到就开始加载,此时没有指定BootStrapClassLoader加载的位置,于是没有找到 HelloLoder.class,所以继续交给ExtClassLoader去加载,ExtClassLoader也没有找到 HelloLoder.class,于是交给AppClassLoader加载,在AppClassLoader中找到的 HelloLoder.class中输出的是 I am apploader 。 当我们指定 BootStrapClassLoader的加载位置为 -Xbootclasspath/a:E: mp 时,在BootStrapClassLoader加载时就找到了 HelloLoder.class 文件,所以BootStrapClassLoader就直接加载了这个类,而不是等到AppClassLoader去加载。而此时这个类中的输出为 I am bootloader 。这个例子说明了类的加载是从上向下的。

       我们再来看一个例子,在这个例子中我们同样指定BootStrapClassLoader加载的位置 :-Xbootclasspath/a:E: mp

    public class FindClassOrder {
    
        public static void main(String[] args) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
            //获取类加载器
            ClassLoader cl = FindClassOrder.class.getClassLoader();
            
            //获取要加载的类
            byte[] buffer = null;  
            try {  
                File file = new File("D:\Users\wuyouxin\eclipse-workspace\springBootTest\target\classes\com\wyx\service\HelloLoder.class");
                FileInputStream fis = new FileInputStream(file);  
                ByteArrayOutputStream bos = new ByteArrayOutputStream(1000);  
                byte[] b = new byte[1000];  
                int n;  
                while ((n = fis.read(b)) != -1) {  
                    bos.write(b, 0, n);  
                }  
                fis.close();  
                bos.close();  
                buffer = bos.toByteArray();  
            } catch (FileNotFoundException e) {  
                e.printStackTrace();  
            } catch (IOException e) {  
                e.printStackTrace();  
            }  
            Method md_defineClass = ClassLoader.class.getDeclaredMethod("defineClass", 
                    byte[].class,int.class,int.class);
            
            //因为 defineClass 是protected final的,如果不设置则不可调用
            md_defineClass.setAccessible(true);
            //强制使用AppClassLoader加载 HelloLoder.class
            md_defineClass.invoke(cl, buffer, 0, buffer.length);
            md_defineClass.setAccessible(false);
            
            HelloLoder loder = new HelloLoder();
            System.out.println(loder.getClass().getClassLoader());
            loder.print();
        }
    }

       不同的是我们使用 AppClassLoader 强制加载了 第一个 HelloLoder.class,所以此时的执行结果为:

      我们可以看出加载的类加载器为AppClassLoader ,执行的语句也为 I am apploader,所以当系统需要使用HelloLoder类时,直接去AppClassLoader 找类有没有被加载,现在找到了就不继续向父级类加载器发起委派了。此例说明,类的检查是否被加载是自底向上的。

     四、上下文加载器(Thread.SetContextClassLoader())

       但是,这种双亲委派模式存在一个问题,就是查看类加载都是从下往上的,所以,在顶层的classLoder中无法加载底层classLoder的类,也就是说在 BootStrapClassLoader 中无法加载 应用层的类,而在rt.jar中有些类中的方法可能在我们应用中被重写了,但是正在 BootStrapClassLoader 中是无法加载到重写的方法的。 但是有时候就需要在 BootStrapClassLoader 去访问应用class中加载的类,这种模式就无法做到。为了解决这个问题就提出了一个上下文加载器,他用以解决顶层classLoader无法访问底层classLoader的类的问题,基本思想就是在顶层classLoader中传入底层classLoader的实例。它可以任务是一个角色,因为他不是一个具体的classLoder,他可以由任何一个classLoader来做这个上下文加载器。

      下面这个方法来自于javax.xml.parsers.FactoryFinder类中的getProviderClass 方法,我们可以看出其中加载时将 cl 传入后加载className 而此时的 cl 就作为一个上下文加载器。

     

     

    五、双亲模式的破坏

      在jdk中默认的classLoader的加载模式是双亲委派模式,但是并不是必须要这么去做。比如tomcat中的WebappClassLoader就是先加载自己的class,找不到再去委派给上级的 classLoader。 而OSGi(热部署)的classLoader的结构是网状的,它内部有自己的一套算法,根据自己的需要去自由的加载class。

    六、自定义 ClassLoader

      下面代码是自定义 OrderClassLoader 中的部分代码:其中就是首先加载自己的类,如果无法加载则在委托上级加载器去加载。

    /**
     * 自定义 classLoder
     * @author Herrt灬凌夜
     *
     */
    public class OrderClassLoader extends ClassLoader {
    
        @Override
        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
            Class re = findClass(name);
            if (re == null) {
                System.out.println("无法载入类:" + name + "需要委托父加载器");
                return super.loadClass(name, resolve);
            }
            return re;
        }
    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            //首先去找这个classloder中是否有加载这个类
            Class clazz = this.findLoadedClass(name);
            //如果没有,则去加载
            if (null == clazz) {
                try {  
                    File file = new File(name);
                    FileInputStream fis = new FileInputStream(file);  
                    ByteArrayOutputStream bos = new ByteArrayOutputStream(1000);  
                    byte[] b = new byte[1000];  
                    int n;  
                    while ((n = fis.read(b)) != -1) {  
                        bos.write(b, 0, n);  
                    }  
                    fis.close();  
                    bos.close();  
                    byte[] buffer = bos.toByteArray(); 
                    //加载类
                    clazz = defineClass(name, buffer, 0, buffer.length);
                } catch (FileNotFoundException e) {  
                    e.printStackTrace();  
                } catch (IOException e) {  
                    e.printStackTrace();  
                }  
            }
            return clazz;
        }
    }

    -------------------- END ---------------------

    
    

    最后附上作者的微信公众号地址和博客地址 

    
    

    公众号:wuyouxin_gzh

    
    

    
    

     

    
    

    Herrt灬凌夜:https://www.cnblogs.com/wuyx/

     

  • 相关阅读:
    探究Spark算子-RDD
    Spark架构中YarnCluster模式作业流程
    Spark运行架构和组件
    Spark部署模式&端口号&提交作业参数说明
    Spark和Hadoop的联系和区别
    Idea中文件大小配置
    设计模式-之Scala单例模式
    HDFS集群格式化踩过的坑
    安装Spark时遇见的坑
    配置群起zookeeper的脚本所踩过的坑
  • 原文地址:https://www.cnblogs.com/wuyx/p/9705132.html
Copyright © 2011-2022 走看看