JAVA类加载机制

作者: Wen_Q_M | 来源:发表于2017-07-09 08:48 被阅读21次

    概述

    虚拟机把描述类的数据从Class文件加载到内存中,并对数据进行验证,准备,解析,初始化的一个过程,最终是可以被虚拟机直接使用的java类型,这就是类加载的一个简单的过程。
    Java中的类加载是在运行时加载,这样会比较的消耗性能,但是正是在运行时加载使得java拥有很好的灵活性和可扩展性。

    类加载的时机

    类从被加载到内存中开始,到卸载出内存为止。它的生命周期总共七个阶段:加载---->验证---->准备---->解析---->初始化---->使用---->卸载。其中解析这个过程是不确定的,它可能会在初始化后之后,这是为了使java支持运行时的绑定。

    • new ,getstatic,putstatic,invokestatic这四条指令时会触发初始化的操作。
    • new是new一个新的对象时会触发初始化。
    • getstatic是获取静态字段时会触发。
    • putstatic是设置静态字段时会触发。
    • invokestatic是调用另一个类的静态方法的时候。
      PS:需要注意的是getstatic和putstatic被final修饰的,在编译期就放入到常量池中是不会触发的。
    • 使用java.lang.reflect的包方法对类进行反射调用时,如果类没有初始化就需要进行初始的操作。
    • 子类进行初始化时需要对父类先进行初始。
    • java启动时需要的启动主类,程序的入口。该类就需要进行初始化。
    • 使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.Methondhandle实例最后解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,如果没有进行初始化时会触发初始化。
      PS:接口的初始化和类初始化不同,接口初始化只和类初始化的子类初始化是需要父类先进行初始化,而且并不是接口父类中的所有都是会初始化。

    加载

    加载是类加载中前面提到的其中的一个过程。类加载的基本过程:

    • 通过全限定类名加载二进制流。
    • 将二进制流代表的静态存储结构转换方法区中运行时的数据结构。
    • 在内存中生成java.lang.Class对象,将这个作为该方法区这个类中各种数据的一个入口。

    加载分为数组类加载过程和非数组类的加载过程。java的数组类的加载过程其实是有虚拟机直接加载的但是数组中的类型需要类加载机制加载:

    • 非数组类加载机制:可控性强既可以有系统类加载器进行加载又可以由用户自定义的类加载器进行加载。(重写一个类加载器的loadClass()方法)。
    • 数组类型的加载机制:数组类型的加载机制如果是引用类型,就使用递归进行加载,并且会在加载的类型上加入一个标志。如果是非引用类型则会把标志与引导类加载器关联。

    ps:数组类的可见性与它组件的可见性是相同的,如果组件类型不是引用类型的可见性一般设置为public。
    类加载完成会有一个连接,可能在没完成加载就开始连接,虽然如此但是该顺序是一定的。

    验证

    验证的主要目的是保证加载进来的Class文件的字节流包含的信息符合虚拟机的当前的要求,不会有危害自身的数据存在。
    Java是相对C++语言是安全的语言,例如它有C++不具有的数组越界的检查。这本身就是对自身安全的一一种保护。验证阶段是Java非常重要的一个阶段,它会直接的保证应用是否会被恶意入侵的一道重要的防线,越是严谨的验证机制越安全。验证的四个阶段文件格式验证-->元数据验证-->字节码验证-->符号引用验证。

    • 文件格式验证:主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。
    • 是否以魔数开头。
    • 主,次版本号是否在当前虚拟机处理的范围之内。
    • 常量池中是否有不被支持的常量类型。
    • 指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。
    • CONSTANT_Utf8_info型的常量中有不符合utf8格式的编码数据。
      还有大其它的验证这里就不一一的列举。
    • 元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。
    • 字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。
    • 符号引用验证:主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。

    虽然验证很重要但是并不是必须的阶段。当然大量重复的验证会相当的花费性能和时间的。
    准备


    准备阶段主要是类变量进行分配内存和数据的初始化阶段,所谓的初始化并不是你编码时所定义的变量值。例如:

    public static int age = 20;
    

    数据的初始化并不会将它初始化为20,而是初始化为0,系统有一套自己的初始化值。如下图:

    数据类型 零值
    int 0
    long 0L
    short 0
    char '\u0000'
    byte 0
    boolean false
    float 0.0f
    double 0.0d
    reference null

    当然会有特殊的情况,如下面的代码:

    public static final int value = 20;
    

    这种情况是类的字段时存在ConstantValue属性所指定的字段。用final修饰后出现该属性,加初始化时会直接的使用ConstantValue的属性值,所以会初始化为20。
    解析


    解析是将常量池中的符号引用转化为直接引用的过程,还记得前面验证阶段时出现的符号引用验证吗?就是对该阶段的验证。

    • 符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。
    • 直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的而布局有关的,并且一定加载进来的。

    虚拟机可能会多次的进行解析。解析主要的对类,接口,字段,类方法,接口方法,方法类型,方法句柄和调用点限定符引用进行。这七种解析有细节上的不同,主要的思想是通过限定性类名找到解析的类型进行解析。主要的是会分为数组类型,非数组类型存在一个直接进行解析的过程。在过程还有从下上的匹配查找(主要出现在有继承,接口的情况下)。

    初始化

    初始化算是类加载过程的最后一个阶段,在这个阶段在是真正的开始有java代码主导。大家应该记得在准备阶段已经进行过一次赋值,但是只是系统的默认赋值(ConstantValue的例外情况)。初始化是执行<clinit>的过程。

    • <clinit>的主要是查找static模块,用户自定义类变量的赋值,该顺序是由文件中的顺序界定的。加载过程存在的是父类的一定会比子类先进行加载到,因为会保证子类的<clinit>加载完成时父类的<clinit>一定会加载完成。所有就像大家所知道的java.lang.object一定会是虚拟机中第一个加载完成的。
    • <clinit>在接口中的加载是不同的它是不存在静态块的,接口中也是会有赋值进行的,但是接口中的是在需要用到才会去进行加载的。
    • 允许在定义之前进行赋值的操作,但是不允许使用,如下:
    public class A{
      static{
              s = 20;
              //system.out.printf(s); 
              上面注释的这句话时会出现错误的;
      }
      static int s = 10;
    }  
    
    • 虚拟机会保证在多线程的环境下进行加锁,保证正确执行。如果有多个进行加载一个会保证只有一个去加载,其他的会进去阻塞等待中。同一个类只会加载一次,就算多个进入阻塞也不会重新唤醒。

    类加载器

    • 类与类加载器:一个类的相同判断条件大家都知道,但是如果不是由同一个类加载器加载出来的,就算是看起来相同的也是出现false的。
    • 三大类加载器:
      • 启动类加载器
      • 扩展类加载器
      • 应用程序类加载器
    • 双亲委托机制:


      双器委托机制

      双亲委托机制是当一个类进入加载时,子加载器不会自己尝试去加载,而是将其发送到它的父加载器中加载,以此类推直到达到最后的加载器,只有当父加载器不能进行加载是会发送到子加载器中,子加载此时才会尝试去加载。

    public Class<?> loadClass(String name)throws ClassNotFoundException {
                return loadClass(name, false);
        }
     
        protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
                // 首先判断该类型是否已经被加载
                Class c = findLoadedClass(name);
                if (c == null) {
                    try {
                        if (parent != null) {
                            c = parent.loadClass(name, false);
                        } else {
                        //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
                            c = findBootstrapClass0(name);
                        }
                    } catch (ClassNotFoundException e) {
                     // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
                        c = findClass(name);
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
    
    • 双亲委托机制的破坏
    • 1.2版本为了向前兼容1.0版本
    • 本身模型的问题,基础类要调用用户类而出现的冲突。通过设置线程上下文类加载器,如果出现上面这种情况,通过上下文类加载器去加载所需的类。
    • 用户对动态性的追求,出现没一个模块都有自己的类加载器,如果需要更换时连同类加载器一同换掉。

    相关文章

      网友评论

        本文标题:JAVA类加载机制

        本文链接:https://www.haomeiwen.com/subject/jjcwcxtx.html