一、类加载的时机
(1)类加载的步骤
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期都将经历加载,验证,准备,解析,初始化,使用和卸载七个阶段,其中验证,准备,解析三个部分统称为连接。
(2)类加载的顺序
加载,验证,准备,初始化和卸载这五个阶段的顺序是确定的,而解析阶段在某种情况下可以在初始化阶段之后再开始,这是为了支持 Java 语言的运行时绑定特性。重要的一点是:这些阶段都是互相交叉地混合进行,会在一个阶段执行的过程中调用,激活另一阶段。
初始化阶段:有六种情况必须立即对类进行初始化(加载,验证,准备,自然需要在这之前完成)
- 遇到 new ,getstatic ,putstatic ,invokestatic 这四条字节码指令时,如果类型没有进行初始化,则需要触发初始化阶段;
- 使用java.lang.reflect 包的方法对类型进行反射调用的时候,如果类型没有进行初始化,则现需要触发其初始化;
- 当初始化类时,发现父类还没有进行初始化,则需要先触发父类的初始化;
- 当虚拟机启动时,用户需要指定一个执行的主类,虚拟机会先初始化这个类;
- 当使用 JDK 7 新加入动态语言支持时,如果一个java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic ,REF_putStatic,REF_invokeStatic,REF_newInvokeSpecial 四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化;
- 当一个接口中定义了 JDK 8 新加入的默认方法时(被 default 关键字修饰的接口方法),如果这个类的实现类发生了初始化,那么接口要在其之前被初始化
二、类加载的过程
(1)加载
在加载阶段,Java 虚拟机需要完成三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流;
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的入口;
(2)验证
这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行之后不会危害虚拟机自身的安全。验证阶段大致分为下面四个阶段的验证动作:
1. 文件格式验证:
第一阶段要验证字节流是否符合Class 文件格式规范,并且能够被当前虚拟机处理,验证点包括以下几个:
- 是否以魔数 0xCAFEBABE 开头;
- 主次版本号是否在Java 虚拟机接受范围内;
- 常量池中的常量是否有不被支持的常量类型;
- 指向常量的各种索引值是否有指向不存在的常量或者不符合类型的常量;
Class 文件中各个部分及文件本身是否又被伤处或者附加信息;
2. 元数据验证:
第二阶段是对字节码描述信息进行语义分析,以保证描述信息符合要求,验证点包括以下几个:
- 这个类是否有父类;
- 这个类的父类是否继承了不允许被继承的类;(被 final 修饰的类)
- 如果这个类不是抽象类,是否实现了父类或者接口之中要求是想的所有方法;
- 类中的字段方法是否与父类产生矛盾;
3. 字节码验证:
第三阶段验证目的是通过数据流分析和控制流分析,确定程序语义是合法的,验证点包括以下几点:
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作;
- 保证任何跳转指令都不会跳转到方法体以外的字节码指令上;
- 保证方法体中的类型转换是有效的;
4. 符号引用验证:
第四阶段验证发生在虚拟机将符号引用转化为直接引用时,可以看作是对类自身以外的信息进行匹配性校验,验证点包括以下几点:
- 符号引用中通过字符串描述的全限定名是否能找到对应的类;
- 在指定的类中是否存在复合方法的字段描述及简单名称所描述的方法和字段;
- 符号引用中的类,字段,方法的可访问性是否可被当前类访问;
(3)准备
这一阶段是正式为类中定义的变量分配内存并设置类变量初始值的阶段,这时候进行的内存分配仅包括类变量,而不包括实例变量,实例变量将会在着对象实例化时随着对象一起分配在 Java 堆中。
数据类型 | 零值 |
---|---|
int | 0 |
long | 0L |
short | (short) 0 |
char | ‘\u0000’ |
byte | (byte) 0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
(4)解析
解析过程是 Java 虚拟机将常量池中的符号因引用替换成直接引用的过程,主要针对类或接口,类方法,接口方法,方法类型,方法句柄和调用点限定符这七类符号引用进行。
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无奇异地定位到目标即可;
- 直接引用:直接引用是可以直接指向目标的指针,相对偏移量或者是一个间接定位到目标的句柄;
(5)初始化
初始化阶段是类加载过程的最后一个阶段,直到初始化阶段,Java 虚拟机才真正开始执行类中编写的 Java 程序代码。在初始化阶段,会根据程序员通过程序编码制定的主观计划去初始化类变量个其他资源。换句话来说就是执行类构造器 < clinit >方法的过程。这个方法并不是程序员编写出来的代码,它是 Javac 编译器的自动生成物。
三、类加载器
(1)类与类加载器
类加载器的作用:比较两个类是否 “ 相等 ” ,只有在这两个类由同一个类加载器加载的前提下才有效,否则,即使这两个类来源于同一个Class 文件,被同一个Java 虚拟机加载,只要加载他们的类加载齐不同,那就一定不同,这里的相等,包括代表类的Class 对象的 equals() 方法,isAssignableFrom() 方法,isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。
(2)双亲委派模型
三层类加载器:
- 启动类加载器:这个类加载器负责加载存放在 < JAVA_HOME >\lib 目录,或者是被 - Xbootclasspath 参数指定的路劲中存放的,而且是Java 虚拟机能够识别的类库加载到虚拟机的内存中;
- 扩展类加载器;这个类加载器是在类sun.misc.Launcher$ExtClassLoader 中以 Java 代码的形式实现的,它负责加载< JAVA_HOME >\bin\ext 目录中,或者被 java.ext.dirs 系统变量所指定的路径中所有的类库;
- 应用程序类加载器:它负责加载用户路径(ClassPath)上的所有类库,开发者同样直接在代码中使用这个类加载器;
双亲委派模型:是指各种类加载器之间的层次关系。它要求出顶层的启动类加载器之外,其他所有的类加载器都应该有自己的父类加载器,并且类加载器之间的继承关系是通过组合来实现的,这样就使得 Java 中的类随着类加载器一起具备了带有优先级的层次关系;
工作过程:如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把请求委派给父类加载器去完成,每一层的类加载器都是如此,因此所有的请求都会传送到最顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子类加载器猜会尝试去加载请求;