Java虚拟机-类加载

1.类加载概述

        虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

2.类加载时机

  • 加载:Java虚拟机规范并没有严格规定,主流虚拟机是懒加载。
  • 连接:加载开始之后,加载完后,连接结束。
  • 初始化:遇到new、getStatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。使用Java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。当一个初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类。
  • 使用:用户使用时。
  • 卸载:代码中再不使用时。
  • 不被初始化的例子:
        通过子类引用父类的静态字段;
        通过数组定义来引用类;

        调用类的常量;

3.加载
    通过一个类的全限定名来获取定义此类的二进制流。
    将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    在内存中生成一个代表这个类的Class对象(方法区),作为这个类的各种数据访问入口。
    源:
        文件
            Class文件
            Jar文件
        网络
        计算生成一个二进制流
            $Proxy
        由其他文件生成
            Jsp

        数据库

4.验证
      验证是连接的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  • 文件格式验证(文件是否是.class等);
  • 元数据验证(是否有父类,父类是否是final的等);
  • 字节码验证(分析控制流,分析程序的执行流是否符合语意);

符号引用验证(描述全限定名是否能找到该类,在指定类中是否有方法或者字段的描述,确保解析的动作能够正常执行);

5.准备

      准备阶段正式为类变量分配内存并设置变量的初始值。这些变量使用的内存都将在方法区中进行分配。这里的初始值并非我们指定的值,而是其默认值。但是如果被final修饰,那么在这个过程中,常量值会一同被指定。

6.解析
      解析阶段是将常量池中的符号引用替换为直接引用的过程。在进行解析之前需要对符号引用进行解析,不同虚拟机实现可以根据需要判断到底在类被加载的时候对常量池的符号进行解析(也就是初始化之前),还是等到一个符号引用被使用之前进行解析(也就是在初始化之后)。

      如果一个符号引用进行多次解析请求,虚拟机中除了invokedynamic指令外,虚拟机可以对第一次解析的结果进行缓存(在运行时常量池中记录引用,并把常量标识为解析状态),这样就避免了一个符号引用的多次解析。解析动作主要针对的是类或者接口、字段、类方法、方法类型、方法句柄和调用点限定符7类符号引用。

  • 类或者接口解析
       如果该符号引用不是一个数组类型,那么虚拟机会将把该符号代表的全限定名传递给类加载器去加载这个类。这个过程由于涉及验证过程所以可能触发其他相关类的加载。
      如果该符号引用是一个数组类型,并且该数组的元素是对象。我们知道符号引用是存在方法区常量池中的,该符号引用的描述符会类似[java/lang/Integer的形式,将会按照上面的规则进行加载数组元素类型,如果描述符入前面假设的形式,需要加载的元素类型就是java.lang.Integer,接着由虚拟机将会生成一个代表此数组对象的直接引用。
       如果上面的步骤没有出现异常,那么该符号引用已经在虚拟机中产生了一个直接引用,但是在解析完成之前需要对符号引用进行验证,主要是确认当前调用这个符号引用的类是否具有访问权限,如果没有访问权限将抛出java.lang.IllegalAccess异常。

  • 字段解析
       对字段的解析需要首先对其所属的类进行解析,因为字段是属于类的,只有在正确得到其类的正确的直接引用才能继续对字段的解析。
       如果该字段符号引用就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,解析结束。
       否则,如果该符号的类实现了接口,将会按照继承关系从下往上递归搜索各个接口和它分父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,那么就直接返回这个字段的直接引用,解析结束。
       否则,如果该符号所在类不是Object类的话,将会按照继承关系从下往上递归搜索其父类,如果父类中包含了简单名称和字段描述符都相匹配的字段,那么直接返回这个字段的直接引用,解析结束。
       否则,解析失败,抛出java.lang.NoSuchFieldError异常。
    如果最终返回了这个字段的直接引用,就进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常。

  • 类方法解析
       进行类方法解析仍然需要先解析此类方法的类。
       类方法和接口方法的符号引用是分开的,所以如果在类方法表中发现class_index(类中的符号引用)的索引是一个接口,那么会抛出java.lang.IncompatibleClassChangeError的异常。
       如果class_index的索引确实是一个类,那么在该类中查找是否有简单名称和描述符都与目标字段相匹配的方法,如果有的话就返回这个方法的直接引用,查找结束。
       否则,在该类的父类中递归查找是否具有简单名称和描述符都与目标字段相匹配的方法,如果有,则返回这个字段的直接引用,查找结束。
     否则,在这个类的接口以及它的父接口中递归查找,如果找到的话就说明这个方法是一个抽象类,查找结束返回java.lang.AbstractMethodError异常。
       否则,查找失败,抛出java.lang.NoSuchMethodError。
       如果最终返回了直接引用,还需要对该符号引用进行权限验证,如果没有访问权限,就抛出java.lang.IllegalAccessError异常。

  • 接口方法解析
    如果在接口方法表中发现class_index的索引是一个类而不是一个接口,那么也会抛出java.lang.IncompatibleClassChangeError的异常。
      否则,在该接口方法的所属的接口中查找是否具有简单名和描述符都与目标字段相匹配的方法,如果有的话就直接返回这个方法的直接引用。
      否则,再改接口以及其父接口中查找,直到Object类,如果找到则直接返回这个方法的直接引用。
      否则,查找失败。

      接口的所有方法都是public,所以不存在访问权限问题。

7.初始化
      初始化是类加载的最后一步。
      初始化是执行<clinit>()方法的过程。
      类初始化阶段是类加载的最后一步,前面类加载的过程除了在加载阶段用用应用程序可以通过自定义代类加载器参与之外,其余动作完全由虚拟机主导与控制。到了初始化阶段,才是真正执行类中定义的Java程序代码。

      在准备阶段,变量已经被赋过一次系统要求的初始值,而在初始化阶段,则根据开发者通过程序控制制定的主观计划去初始化类变量和其他资源。

      先来看一段代码
public class Demo {

        static {
                i = 0;
                System.out.println(i);
        }

        static int i = 1;
}

      上面这段代码变量的赋值语句可以通过编译,下面的输出贬义不过。<clinit>()方法是由编译器自动收集类中的所有变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的语句块中可以赋值,但是不能访问。

        再看一段代码
public 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>()在执行之前,虚拟机保证父类的先执行完毕,因此在赋值前父类static已经执行,因此结果为2。接口中也有变量要赋值,也会生成<clinit>(),但不需要先执行父类的<clinit>()方法,只有父接口中定义的变量使用时才会初始化。
      如果多个线程同时初始化一个类,只有一个线程会执行这个类的<clinit>()方法,其他线程等待执行完毕,如果方法执行时间过长,那么就会造成多个线程阻塞。

猜你喜欢

转载自blog.csdn.net/qq_22866497/article/details/80995812