JVM类加载机制:
类加载机制:JVM把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称为类加载机制。过程主要分为加载,验证,准备,解析,初始化这五个步骤。
《Java虚拟机规范》严格规定有且只有六种情况下必须初始化,而加载自然需要在初始化之前:
- 遇到
new
(用于创建一个对象实例)、getstatic
(用于读取一个类的静态字段的值),putstatic(用于将操作数栈顶的值赋给一个类的静态字段)、invokestatic
(用于调用一个类的静态方法)这四条字节码指令会进行初始化。 - 使用
java.lang.reflect
包的方法对类型进行反射调用。 - 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- JVM启动时,包含
main
方法的类会被加载。 - 当Java虚拟机动态扩展支持其他语言时,如果该语言编写的代码使用了Java类,那么这些Java类也会被初始化。
- 当一个接口定义了JDK8新加入的默认方法,如果有类实现了这个接口则进行初始化。
加载:
加载通过一个类的全限定名获取类的二进制流,将字节流的的静态储存转化为方法区的运行时数据结构。
下面是类文件的获取方式:
- 文件系统:这是最常见的方式,JVM可以直接从文件系统中的.class文件加载类。例如,当运行一个Java应用程序时,JVM会使用应用程序类加载器(Application ClassLoader)从文件系统中加载主类及其依赖的类。
- 网络:JVM可以从网络上的远程服务器加载类。这种方式通常涉及到自定义类加载器,它能够从网络上下载字节码文件并加载类。
- JAR/ZIP文件:JVM可以从JAR(Java ARchive)或ZIP文件中加载类。这些文件通常包含了多个.class文件和其他资源,JVM可以使用内置的类加载器或自定义类加载器来读取这些文件。
- 数据库:在某些情况下,类的字节码可能存储在数据库中。JVM可以使用自定义类加载器从数据库中读取字节码并加载类。
- 其他介质:理论上,只要能够将类的字节码以二进制形式读取,JVM就可以从任何介质加载类,包括内存、自定义文件系统、甚至是动态生成的字节码
验证:
在Java虚拟机(JVM)的类加载过程中,验证阶段(Verification)是非常关键的一个环节。这个阶段的目的是确保加载的类的字节码是合法的、符合JVM规范的,并且不会危害JVM的安全。验证阶段的主要作用包括
- 确保格式正确:验证字节码文件的格式是否符合《Java虚拟机规范》的要求,包括魔数、版本号、常量池、字段、方法、属性等结构的正确性。
- 确保类型安全:检查所有的类型引用是否正确,例如字段访问、方法调用、参数传递等是否使用了正确的类型。
- 检查访问权限:确保当前类对其他类的访问(如字段、方法的访问)遵循Java的访问控制规则,例如,私有方法不能被其他类调用。
检查字节码完整性:确保所有的代码路径都是可达的,没有悬挂的分支或者无法到达的代码。 - 检查堆栈操作:验证字节码在执行过程中对操作数栈和局部变量表的操作是否合法,例如,确保操作数栈的出栈和入栈操作是匹配的。
- 确保符合引用规范:检查所有的符号引用(如类、字段、方法的引用)是否能够解析为实际的存在。
准备:
准备阶段是正式为类中定义的变量(静态变量)分配内存并设置初始值的阶段。
private static int value = 1;
//这个`value`的值在初始化阶段为0而不是123。
final
阶段修饰的静态变量比较特殊:
private final static int value = 1;
//这个`value`的值在初始化阶段为123。
解析:
在Java虚拟机(JVM)中,解析(Resolution)是类加载过程的链接阶段的一个子阶段,它发生在类被加载和验证之后。解析的主要任务是解析类、接口、字段和方法的符号引用,将其替换为直接引用。
符号引用(Symbolic References):在编译时,Java类并不知道其他类或接口、字段和方法的实际内存地址,因此使用符号引用来代替。符号引用包含了足够的信息来描述所引用的目标,比如类名、方法名、字段名等。
直接引用(Direct References):直接引用是指向方法区中方法的指针、字段、类、接口的指针或句柄等,它们是内存中的实际地址。
初始化:
在Java虚拟机(JVM)中,初始化阶段(Initialization)是类加载过程的最后一个阶段。在这个阶段,JVM负责执行类的初始化代码,这通常包括执行静态代码块和对静态变量的赋值。初始化阶段是JVM确保类的静态字段能够正确地初始化的机制。
执行静态代码块和对静态变量的赋值合并成<clinit>
方法,下面我们通过实操验证一下这个说法:
这里我们没有声明静态变量和静态代码块,可以看到没有生成
<clinit>
方法。
这里我们声明了一个静态变量,可以看到生成了
<clinit>
方法。
编译器的收集顺序是由语句在源文件出现的顺序决定的,静态语句块只能访问到定义在静态语句之前的变量,定义在它之后的变量可以访问,可以赋值但是不可以访问。
public class Test01 {
static{
value = 20;
System.out.println((value)); //非法向前引用
}
private static int value = 1;
public static void main(String[] args) {
System.out.println(value);
}
}