原文引自《深入理解Java虚拟机》
Java虚拟机的 类加载 过程主要分为:加载->验证->准备->解析->初始化
除了 解析 过程以外,其他的过程都是按部就班开始的,但结束时间不一定有序。
类加载的触发条件有以下几点:
- 当遇到 new, getstatic, putstatic, incokestatic 这几个字节码指令后会加载相应的类
- 当使用反射调用类的时候也会加载相应的类
- 当初始化一个类的时候,若其弗雷还没有初始化,则先初始化其父类
- 当虚拟机启动时,制定了一个程序入口(定义了主方法的类)需要被初始化
- 因为Java的动态语言支持,当利用MethodHandle实力执行方法句柄时会触发类初始化
有几种特殊情况不会触发类加载:
- 当通过子类类对象调用父类的静态字段时,子类不会加载
- 当定义一个类对象数组时,如果只是定义了数组容量,那么不会加载这个类
- 当调用类的静态常量时,不会出发类加载动作
注:接口的初始化规则和类不一样,接口只有在真正被使用(引用接口中的常量等)时才被初始化,并且在初始化一个接口时并不会去初始化它的父接口。
加载阶段
加载阶段主要执行三个步骤:- 通过类的全限定名来获取类的二进制字节流
- 将字节流代码所表示的静态数据结构转换为JVM中的动态数据结构
- 生成一个代表这个类的 Class 对象作为访问方法区中这个类的入口
(1)class 文件中获得,还可以从 jar、war 等格式的文件中获得
(2)网络中获得
(3)反射等动态代理机制中动态生成二进制字节流
(4)还可以从 JSP 文件中获得
(5)从数据库中读取
(6)...
验证阶段
验证阶段确保加载入的二进制字节代码符合当前虚拟机的规则,不会危害虚拟机安全。如果二进制字节码不是由编译而来的话,不加检查会产生编译期本应该检查出的错误,所以验证阶段会检查代码是否出现了数组越界、跳转错误等错误来保证代码运行的安全。
验证阶段主要分为四个阶段:
- 文件格式验证:主要保证代码的字节流能够正确无误的保存在虚拟机方法去中,格式是否符合 Java 虚拟机规范
- 验证当前代码版本号是否能被当前虚拟机识别
- 是否以魔数开头
- 常量池中是否有不被支持的类型
- 是否符合 UTF-8 编码格式
- Class 文件中是否有删除或附加信息
- ...
- 元数据验证:验证字节码的语义是否规范,保证其符合Java语言规范
- 加载的类是否有父类
- 是否继承了不该被继承的类(final 类)
- 若不是抽象类,是否实现了抽象方法
- 类中的字段、方法是否与父类产生冲突
- ...
- 字节码验证:验证代码是否符合语义、逻辑,维护虚拟机运行安全。
- 防止从操作数栈中存入一个 long 型数据,取出后赋值给其他类型变量
- 防止跳转指令跳转到方法体以外的字节码指令
- 保证;类型转换有效
- ...
- 符号引用验证:发生在将符号引用转换为直接引用的时候,检查符号引用的匹配正确性,若同一个代码被验证过正确的话,可以在以后运行时通过 -Xverify:none 关闭这个阶段的执行
- 是否可以从这个类的全限定名获取这个类
- 是否存在方法区中符号描述的字段及方法
- 类中的方法、字段是否可以被访问
- ...
准备阶段
这个阶段主要为类中的 静态 字段分配内存并设置初始值(这里的初始值并不是在代码中指定的值,而是每种类型的 0 值),非静态变量在初始化对象的时候再回被赋值。解析阶段
这个阶段主要把常量池中的符号引用转换为直接引用符号引用:一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,是要能定位到目标即可,符号引用与内存布局无关。
直接引用:直接引用可以是指针、偏移量、句柄等,可以直接定位到目标的所在地址。
解析阶段具体发生时间并没有定死,可以在类加载时将常量池中的引用进行转换,也可以在这个引用被使用时再进行转换。除了 invokedynamic 指令,符号引用在解析后,在以后的使用中可以不必再次解析。解析阶段针对类、接口、接口方法、字段、类方法、方法类型、方法句柄、调用点限定符进行,这里介绍前四种:
-
类或接口的解析:分为三步
- 如果加载的类不是数组类型,那么就将全限定名称传递给类加载器去加载这个类,然后经历验证阶段的前三个阶段。
- 如果加载的是一个数组类,那么就先加载数组元素的类型,然后虚拟机会生成一个数组对象
- 如果前两步都没有发生异常,那么久开始执行验证阶段的最后一个阶段:引用验证,检查当前类是否具有这个引用的访问权限。
- 字段解析:在解析字段之前需要先解析字段所属的类或者接口的符号引用,如果解析成功,那么开始字段解析后续步骤
- 如果字段所在类中包含了字段的简单名称或和字段描述符,那么就返回这个字段的直接引用
- 否则,如果字段所在类实现了接口,那么按照继承关系自下向上搜索它的父接口中是否有这个字段
- 否则,如果不是 Object 类,那么久按照类继承关系自底向上寻找这个字段
- 否则,查找失败,抛出 java.lang.NoSuchFieldError 异常
- 类方法解析:在解析前需要先解析出方法所属的类或接口的符号引用,如果解析成功,则执行后续步骤
- 类方法和接口方法符号引用的常量定义是分开的,所以如果在类方法表中发现了接口,就会抛出 java.lang.IncompatibleClassChangeError 异常
- 如果通过第一步,就在方法所属类中寻找方法的简单名称和描述符都匹配的方法
- 否则,在当前类的父类中递归查找是否有简单名称和描述符都匹配的方法
- 否则,如果当前类实现了接口,在当前类实现的接口中递归向上查找
- 否则,抛出 java.lang.NoSuchMethodEror
- 接口方法解析:接口解析也需要先解析方法所属接口的符号引用
- 和类方法不一样,接口方法会检查方法所属是否是一个接口,如果是一个类,抛出 java.lang.IncompatibleClassChangeError 异常
- 否则,在接口中查找是否有简单名称和描述符都匹配的方法
- 否则,在接口的父接口中查找是否有简单名称和描述符都匹配的方法
- 否则,抛出 java.lang.NoSuchMethodError 异常
初始化阶段
在初始化之前,除了加载阶段以外,其他阶段都是由虚拟机控制,在初始化阶段才开始正真的代码执行类中的代码。在准备阶段赋值过一次静态变量,给定的是系统的初始值,在初始化阶段会对类变量和其他资源进行程序员指定的初始化操作。
在这个阶段会执行 函数:
-
方法是由编译器自动收集类中所有类变量的赋值操作和静态代码块中的代码结合而产生的,产生的代码顺序是按照语句在源文件中的出现顺序而定的,静态语句块可以访问出现在语句块前的静态变量,在语句块之后出现的静态变量只能赋值,不能访问。
public class Test{ static{ i = 0; System.out.println(i);//这行代码将报错“非法向前引用” } static int i; } 复制代码
- 方法与类构造器不同,它不需要像显式调用父类构造器一样去显式调用父类的 方法,虚拟机会保证在执行它前他的父类 方法已经执行,所以最先执行的总是 Object 类的 方法
- 如果一个类中没有静态语句块,也没有类变量的赋值操作,那么编译阶段可以选择不为这个类生成 方法
- 对于接口的 方法,因为接口中没有静态代码块,只有类变量赋值操作,所以在执行一个接口的 方法时不用必须去执行父接口的 方法,在执行一个类的 方法时也不用必须执行它实现接口的 方法,只有在调用父接口中的类变量时才去执行。
- 虚拟机会保证一个类的 方法会在多线程下被正确的加锁同步执行。