类加载
加载 -> 验证 -> 准备 ->解析 -> 初始化 -> 使用 -> 卸载
初始化
对于加载阶段 虚拟机并没有强制约束 但是对于初始化有且只有 6 种情况:
1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。
2)使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需 要先触发其初始化。
3)当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先 初始化这个主类。
5)当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解 析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
6)当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
其他均为被动引用
类的加载
1)通过类的全限定名获取定义此类的二进制字节流
2)将这个字节流代表的静态存储结构转化为方法区的运行时数据结构
3)在内存中生成一个代表这个类的Java.class对象,作为方法区这个类的各种数据访问入口
此加载方法决定java加载类的灵活度是很大的,只需要获取字节流 不需要指定从哪里获得 例如zip,网络web applet,运行时计算生成如动态代理等。
相对于类加载过程的其他阶段,非数组类型的加载阶段(准确地说,是加载阶段中获取类的二进 制字节流的动作)是开发人员可控性最强的阶段。加载阶段既可以使用Java虚拟机里内置的引导类加 载器来完成,也可以由用户自定义的类加载器去完成,开发人员通过定义自己的类加载器去控制字节 流的获取方式(重写一个类加载器的findClass()或loadClass()方法),实现根据自己的想法来赋予应用程序获取运行代码的动态性。
对于数组类
但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型(ElementType,指的是数组去掉所有维度的类型)最终还是要靠类加载器来完成加载
·如果数组的组件类型(Component Type,指的是数组去掉一个维度的类型,注意和前面的元素类 型区分开来)是引用类型,那就递归采用本节中定义的加载过程去加载这个组件类型,数组C将被标 识在加载该组件类型的类加载器的类名称空间上
·如果数组的组件类型不是引用类型(例如int[]数组的组件类型为int),Java虚拟机将会把数组C 标记为与引导类加载器关联。
·数组类的可访问性与它的组件类型的可访问性一致,如果组件类型不是引用类型,它的数组类的 可访问性将默认为public,可被所有的类和接口访问到。
加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段 尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的一部 分,这两个阶段的开始时间仍然保持着固定的先后顺序。
验证
目的保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
1.文件格式验证
对二进制文件验证
2.元数据验证
对字节码描述的信息进行语义分析,比如是否有父类,是否允许继承 抽象之类
3.字节码验证
数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。
4.符号引用验证
将符号引用转换为直接引用 这个发生在第三阶段解析
符号引用验证的主要目的是确保解析行为能正常执行,如果无法通过符号引用验证,Java虚拟机 将会抛出一个java.lang.IncompatibleClassChangeError的子类异常,典型的如: java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。
准备
为类变量分配内存并定义初始值,注意 此处初始值并不是代码中写的 而是默认零值 如
public static int value =123
准备阶段结束后 value为 0,而不是 123 。因为此处没有执行任何java方法,赋值123为putstatic指令时程序被编译后,存放在类构造器()中,所以赋值要等到初始化阶段才可以。
解析
Java虚拟机将常量池内的符号引用替换为直接引用的过程
初始化
直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程 序。
初始化阶段就是执行类构造器 < clinit > ()方法的过程。 < clinit >()并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物
1)< clinit >()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块( static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访 问。
2) < clinit >()方法与类的构造函数(即在虚拟机视角中的实例构造器< init >()方法)不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的< clinit >()方法执行前,父类的< clinit >()方法已经执行完毕。因此在Java虚拟机中第一个被执行的< clinit >()方法的类型肯定是java.lang.Object。
3) 由于父类的< clinit >()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作
4)< clint >不是必需的,没有定义静态语句块和赋值操作 则不生成该方法
5)接口中不能存在静态代码块,但是可以存在变量初始化赋值操作,执行接口 clinit方法不需要先执行父类的方法,只有当父类中的定义的变量被使用时才会执行。
6)java虚拟机必须保证一个类的clinit方法在多线程环境中正确的加锁同步,如果多个线程同时去初始化一个类,那么只有一个线程回执行clinit其他线程需要阻塞等待,直到活动线程结束。如果有一个耗时很长,那就有可能造成多个线程阻塞。