• 虚拟机类加载机制------类加载的过程


    1.加载

    虚拟机需要干三件事:

    ①、通过一个类的的全限定名来获取定义此类的二进制字节流(没有规定二进制字节流从那里获取,怎样获取,许多java技术也都建立在这基础上)

    ②将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构(将常量池转变成运行时常量池)

    ③在内存中生成一个代表这个类的java.lang.Class对象,作为方法区着各类的各种数据的访问入口。

    相比较于类加载过程的其他阶段,非数组类获取类的二进制字节流的动作是开发人员可控性最强的,因为加载阶段既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成

    ,开发人员可以自己重写一个类加载器的loadClass()方法

    对于数组类,不通过类加载器创建,由java虚拟机直接创建的。但是数组类的元素类型(去掉所有维度的类型)最终是要通过类加载器去创建。

    数组类的创建过程要遵循以下原则:

    ①、如果数组的组件类型(数组去掉一个维度的类型)是引用类型,就递归加载过程去加载这个组件类型,数组C将在加载该组件类型的类加载器的类名称空间上被标识。

    ②、如果数组的组件类型不是引用类型(例如int[]数组),java虚拟机将会把数组C标记为与引导类加载器关联

    ③、数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public

    加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机自行定义。然后在内存中实例化一个java.lang.Classleide duix (没有明确规定在java堆中吗、,对于Hotspot来说,Class对象比较特殊,存放在方法区里面)。这个对象作为程序访问方法区中这些类型数据的外部接口。

    连接阶段:

    2 .验证

    验证的目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的安全。

    整体上看,验证阶段大致上会完成下面四个阶段的检验工作:

    (1)基于字节流检验

    文件格式验证:基于二进制字节流进行的,只有经过这个阶段的验证,字节流才会进入内存的方法区中存储。目的是保证输入的字节流能正确解析并存储于方法区之内

    (2)基于方法区的存储结构

    元数据验证:对字节码描述的信息进行语义分析,保证不存在不符合JAVA语言规范的元数据信息。

    字节码验证:最复杂的一个阶段,对类的方法体进行校验,保证被叫严磊的方法在运行时不会做出还虚拟机的事。目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

    符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段中发生。目的是确保解析动作能正常执行。

    3.准备

    准备阶段是正式为类变量分配内存并设置初始值的阶段,这些变量所使用的内存都在方法区中进行分配。

    注意两点:一点是类变量,另一点是初始值“通常情况下”是数据类型的零值。例如“public static int value=123”在准备过程value的值为0而不是123,因为这时候尚未开始执行任何java方法,而把value赋值为123的puststatic指令是程序被变异后,存放于类构造器<clint>()方法之中,所以把value赋值为123的动作在初始化时候才会进行

    特殊:如果类字段的字段属性表中存在ConstacntValue属性,那么准备阶段变量value就会被初始化为所指定的值

    public static final int value=123;

    这是value被赋值为123.

    4.解析

    解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

    符号引用和直接引用又有什么关联呢?

    符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时可以无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中,各种虚拟机实现的内存布局可以各不相同,但是它们能接收的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在java虚拟机规范的Class文件格式中。

    直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个嫩详见定位到目标的句柄。直接引用时和虚拟机实现的内存布局相关的,通过一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,被引用的目标必在内存中存在。

    看上图的常量池,有”//”注释的就是符号引用常量.它在常量池中的信息是通过一个引用值来标识的.其他可以直接获取到的值,它其实是直接指向目标的指针,偏移量或者句柄.

    解析这个步骤做的事情通俗一点说,就是把”//”后面的数据拿到.

    解析的具体时间没有规定,只要求了在执行anewarray  checkcast getfield getstatic instanceof invokedynamic invokeinterface invokespecial invokestatic invokevirtual ldc multianewarray new putfield putstatic 这十六个用于操作符号引用的字节码指令之前,先对他们所使用的符号引用进行解析

    对一个符号引用进行多次解析请求,除了invokedynamic指令以外,虚拟机可以缓存第一次解析的结果,之后再请求解析,可以直接调用避免重复进行。

    invokedynamic指令是“动态调用点限定符”动态也就是必须等到程序实际运行到这条指令的时候,解析才进行的。

    解析动作主要针对

    类或接口解析 :

    假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,那虚拟机完成这个解析的过程需要以下3个步骤

    ①:C不是一个数组类型,虚拟机会把代表N的全限定名传递给D的类加载器去加载这个类C。在加载过程中,由于元数据验证、字节码验证的需要,有可能触发其他相关类的加载动作。

    ②:如果C是一个数组类型,并且数组的元素类型为对象,例如N是“[Ljava/lang/Integer”的形式,那将会按照第1点的规则加载数组元素类型,如果N的描述符是前面那样,需要加载的元素类型就是“java.lang,Integer”,接着由虚拟机生一个代表此数组维度和元素的数组对象。

    ③:如果上面的步骤没有出现异常,C已经在虚拟机中成为了一个有效的类和接口了,但是解析完成之前还有进行符号引用验证,确保D是否具备对C的访问权限。

    字段解析:

    要解析字段符号引用,首先要对字段表内字段所属的类或接口的符号引用进行解析,如果解析成功,那这个字段所属的类或接口用C表示,虚拟机规范要求安好如下步骤对C进行后续字段的搜索

    ①:如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束

    ②:否则如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束

    ③:否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果父类包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束

    ④:否则查找失败,抛出异常java.lang.NoSuchMethodError

    ⑤::如果上面的步骤没有出现异常,但是解析完成之前还有进行符号引用验证,确保是否具备对字段的访问权限。

    public class FieldResolution{
    
      interface Interface0{
         intA=1;
      }
      interface Interface1 extends Interface0{
    
      int A=2;
    }
    static class Parent implements Interface1{
        public static int A=3;
    }
    public static void main(Strings[] args){
      System.out.println(Parent.A);
    
    }
    
    }

    如果一个字段出现在C的接口和父类中,或者同时在自己或父类的多个接口中出现,那编译器将可能拒绝编译

    类方法解析:

    要解析类方法符号引用,首先要对类方法表中方法所属的类或接口的符号引用进行解析,如果解析成功,那这个方法所属的类或接口用C表示,虚拟机规范要求安好如下步骤对C进行后续类方法的搜索

    ①:类方法和接口房符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个接口,直接抛出java.lang.IncompatibleClassChangeError异常

    ②:如果C本身就包含了简单名称和描述符都与目标相匹配的方法,则返回这个方法的直接引用,查找结束

    ③:否则,在C的父类中递归查找,如果父类包含了简单名称和描述符都与目标相匹配的方法,则返回这个方法的直接引用,查找结束

    ④:在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法, 如果存在匹配的方法,说明类C是一个抽象类,这时查找结束,抛出java.lang,AbstractMethodError异常

    这个需要这么理解,如果是普通的类去实现某一个接口的方法的话,那么它肯定在第(2)步已经直接返回.如果能执行到第(4)步,则说明C本身的常量池中并没有对应的直接引用.那么只能是说明这个方法是抽象方法.包含抽象方法的类必定是抽象类,所以这里有个结论就是C是抽象类.

    ⑤:否则查找失败,抛出异常java.lang.NoSuchMethodError

    ⑥::如果上面的步骤没有出现异常,但是解析完成之前还有进行符号引用验证,确保是否具备对方法的访问权限。

    接口方法解析:

    要解析接口方法符号引用,首先要对接口方法表中方法所属的类或接口的符号引用进行解析,如果解析成功,那这个方法所属接口用C表示,虚拟机规范要求安好如下步骤对C进行后续类方法的搜索

    ①:类方法和接口房符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个类而不是接口,直接抛出java.lang.IncompatibleClassChangeError异常

    ②:如果C本身就包含了简单名称和描述符都与目标相匹配的方法,则返回这个方法的直接引用,查找结束

    ③:否则,在C的父接口中递归查找,直到找到java.lang.Object类位置。如果包含了简单名称和描述符都与目标相匹配的方法,则返回这个方法的直接引用,查找结束

    ④:否则查找失败,抛出异常java.lang.NoSuchMethodError

     接口中方法默认都是public的,因此不存在访问权限的事

    方法类型解析:

    方法句柄解析:

    调用点限定符解析:

    7类符号引用进行解析

    5.初始化

    类加载过程的最后一步。到了初始化阶段,才真正开始执行类中定义的java程序代码。

    准备阶段,已经赋过一次系统要求的初始值,而在初始化阶段,就要根据程序员的要求来赋值了。从另一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。

    <clinit>()一些可能会影响程序运行行为的特点和细节。

    ①<clinit>()是由编译器按顺序收集类中所有的类变量和静态语句块中的语句合并产生的。这里注意一点,静态语句块只能访问定义在他前面的类变量,对于定义在他后面的,他只能赋值,而不能访问。

    public class Test{
    
      static{
              i=0;    //这句赋值正常编译通过
              System.out.print(i); //这句访问就不行
    
       }
       static int i=0;
    
    }

    ②<clinit>()与类的构造函数不同,他不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()已经执行完毕。因此,在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object

    ③ 由于父类的<clinit>()先执行,si所以父类的静态语句块要优于子类的变量复制操作。

    static class Parent{
      public static int A=1;
     static{
      
        A=2;
    }  
     static class Sub extends Parent{
    
           public static int B=A;
    }
     public static void main(String[] args){
    
       System.out.println(Sub.b);
    }
    
    }

    ④<clinit>()对于类或接口来说不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法

    ⑤接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口和雷一样会生成<clinit>()方法,但是不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时一样不会执行接口的<clinit>()。

    ⑥虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程的<clinit>()方法完毕。

  • 相关阅读:
    正则表达式元字符完整列表及行为说明
    吐槽满肚子的负能量
    又一个月了
    关于SVNcommit时强制写注释方法
    SVN源码服务器搭建
    一个 quick set 驱动费了我一下午
    spring自动注入是单例还是多例?单例如何注入多例?
    web.xml 中的listener、 filter、servlet 加载顺序及其详解
    springmvc+hibernate
    oracle 表 库实例 空间
  • 原文地址:https://www.cnblogs.com/wxw7blog/p/7255340.html
Copyright © 2020-2023  润新知