一、概述
1.1 概念
类的加载机制
:虚拟机将描述类的数据从Class文件
加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。Class文件
: 指一串二进制的字节流(无论以何种形式存在),而非特指某个存在于具体磁盘中的文件。
1.2 参考
- 《深入理解Java虚拟机》
- JVM字节码之整型入栈指令(iconst、bipush、sipush、ldc)
二、类的加载流程
类加载的7个阶段:加载阶段、验证阶段、准备阶段、解析阶段、初始化阶段、使用阶段、卸载阶段
;
加载、验证、准备、初始化、卸载
5个阶段的顺序是确定的;但是解析
阶段可能是在初始化
阶段之前或之后,其目的是为了支持 Java 的动态绑定;加载
阶段要根据虚拟机具体实现来确定,不同虚拟机可能存在差异;初始化
阶段有且只有
以下5中情况;遇到
new、getstatic、putstatic、invokestatic
这4条字节码指令时,如果类没有进行初始化,则需要先触发其初始化;new
:实例化对象;getstatic
:读取一个类的静态字段的值(被final修饰、已在编译期把结果放入常量池的静态字段除外);putstatic
:给一个类的静态字段赋值(被final修饰、已在编译期把结果放入常量池的静态字段除外);invokestatic
:调用一个类的静态方法);
使用
java.lang.reflect
包的方法对类进行反射调用,如果类没有初始化,则需要先触发其初始化;- 当初始化一个类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化(这个只针对于类,接口在初始化时,不要求其父接口全部完成初始化,只有在真正使用到父接口的时候,才会初始化);
- 虚拟机启动时,会调用包含了
main()
方法的那个类; - 使用JDK1.7的动态语言时,如果一个
java.lang.invoke.MethodHandle
实例最后解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic
的方句柄,且这个方法所对应的类没有进行初始化,则需要先触发其初始化;
三、类加载的过程
下面详细讲解一下类加载的前5个阶段,即 加载阶段、验证阶段、准备阶段、解析阶段、初始化阶段
;
3.1 加载阶段
在加载阶段,虚拟机完成了3件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流;
- 将这个字节流缩代表的静态存储结构转化为方法区的运行时数据结构;
- 在内存中生成一个代表这个类的
java.lang.Class
对象(该对象存储于方法区中,而不是对内存中),作为方法区这个类的各种数据的访问入口;
3.2 验证阶段
连接阶段: 验证、准备、解析
;
目的: 为了确保 Class 文件的字节流中包含的信息,符合当前虚拟机的要求,且不会危害虚拟机自身的安全;
验证阶段: 文件格式验证、元数据验证、字节码验证、符号引用验证
;
文件格式验证
验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理;验证之后,字节流才会进入到内存的方法区中进行存储;
元数据验证
对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求;
字节码验证
通过数据流和控制流的分析,确定程序语义是合法的、符合逻辑的;
符号引用验证
符号引用验证:在虚拟机将符号引用转化为直接引用时进行,这个转化的动作在连接的第三阶段(即解析阶段)发生;
目的: 确保解析动作能正常执行;
3.3 准备阶段
准备阶段是正式为 类变量(被static修饰的变量)
分配内存并设置变量 初始值
的阶段,这些变量所使用的内存都将在 方法区
中进行分配;
类变量: 被 static
修饰的变量;
初始值: 通常情况下,变量的数值为零值;特殊情况下(如常量),在准备阶段变量就为指定的值;
3.4 解析阶段
解析阶段:虚拟机将常量池内的符号引用替换为直接引用;
符号引用: 以一组符号来描述所引用的目标,符号可以是任何形式的字面量,能定位到目标即可;
直接引用: 可以直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄;
3.5 初始化阶段
初始化阶段:真正开始执行类中定义的Java程序代码;
在 加载阶段、验证阶段、准备阶段、解析阶段、初始化阶段
这几个阶段中,除了加载阶段用户可以自定义类的加载器(可操作性强)之外,其余阶段都是由虚拟机主导和控制;
四、类加载器
每一个类加载器,都拥有一个独立的类名称空间;
问: 如何确定一个类的唯一性 (如何比较两个类是否相等)?
答: 由加载这个类的类加载器和这个类本身来共同确立;(换句话说,即时这两个类来源于同一个Class文件,且被同一个虚拟机加载,但只要加载他们的类加载器不同,就认定这个两个类不相等;)