虚拟机将 Class 文件加载到内存,并做一系列操作(校验,解析,初始化)后最终生成 Java 类型,就是虚拟机的类加载机制。
「类加载」会经过「加载」「验证」「准备」「解析」「初始化」「使用」「卸载」7 个过程。其中顺序固定不可更改的是「加载」「验证」「准备」「初始化」「卸载」这 5 个步骤,「验证」「准备」「解析」过程可认为是「连接阶段」。
对于何时触发「加载」过程,没有明确规定,但以下 5 种情况会触发「初始化」过程,而在「初始化」之前的过程必然会要先执行。
- 遇到 new, getstatic, putstatic, invokestatic 指令。具体代码场景有 通过 new 创建对象,调用或赋值类的静态变量,调用类的静态方法。
注意:对于静态变量,只有定义它的类会被初始化,其子类间接引用不会初始化子类。对于 final 修饰的变量,引用类的常量也不会初始化类。 - 通过 java.lang.reflect 包的方法对类进行反射调用。
- 当初始化一个类,但其父类还没初始化时。
注意:接口初始化时,不要求其父类要先初始化。 - 用户指定一个主类(包含 main() 方法)。对这个的理解我认为是应用的启动入口类,例如 Android 应用的 Application,启动页 这种。
- 当使用 JDK 1.7 ,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果是 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄。这条没理解~
加载
主要做的事情就是将类的二进制字节流转为内存中代表该类的 java.lang.Class 对象,即:
- 通过类的全限定名来获取定义该类的二进制字节流。
完成这个过程的代码模块叫「类加载器」,但类加载器也不仅仅干这事。对一个类来说,要想在虚拟机中唯一确定,需要类加载器和这个类本身一同确定。
从 Java 虚拟机角度看,类加载器分为两种:启动类加载器,其他类加载器(扩展类加载器,应用程序类加载器)。应用程序类加载器是应用程序默认的类加载器。
在类加载器的实现上,通常会采用「双亲委派模型」,即类的加载首先会委派给父类进行(自定义类加载器 -> 应用程序类加载器 -> 扩展类加载器 -> 启动类加载器),如果父类反馈无法加载,那么子类会自己加载。
这种模型加载的类先天带着优先级的层次关系,同时也是确保类的唯一性的关键。
不过由于双亲委派模型出来的较晚,同时也有些设计不合理的地方,导致实现上会破坏模型结构,此外,热部署方案也导致了双亲委派模型被破坏。 - 将这个字节流所代表的静态存储结构转为方法区的运行时数据结构。
- 在内存中生产一个代表该类的 java.lang.Class 对象,作为在方法区访问该类各种数据的入口。所以特殊的地方在于 Class 对象是在方法区的。
验证
验证是为了保证 class 文件内容符合规范,不会造成虚拟机危害。验证过程分为四步:
- 文件格式验证。检查文件内容格式是否符合,具体文件内容格式可见 深入理解 Java 虚拟机读书笔记5
- 元数据验证。对文件内容进行语义分析,确保符合规范。
- 字节码验证。在语义分析的基础上,再对数据流和控制流进行分析,确保符合逻辑。
- 符号引用验证。这一步将发生在「解析」过程中的引用转化中。
准备
主要就是为类变量分配内存并进行默认值初始化。其分配的内存在方法区,并且仅初始化值为默认值,而并非代码中指定的赋值。
初始化
初始化过程是执行类构造器 < clinit >() 方法的过程,< clinit >() 是所有类变量赋值和静态语句块的集合,注意:静态语句块中只能访问到定义在静态语句块之前的变量,但赋值可以。< clinit > 不需要显示调用,虚拟机会保证调用;并且在子类执行 < clinit > 时,父类的 < clinit > 会优先执行。对于接口来说,类似于接口加载过程,子接口执行 < clinit > 时并不会触发父接口 < clinit > 的执行。
网友评论