当前位置: 首页 > 后端技术 > Java

Java虚拟机类加载机制全面解析

时间:2023-04-01 14:23:22 Java

什么是类加载机制?JVM从Class文件中加载描述类的数据到内存中,并对数据进行校验、转换、解析、初始化,最终形成JVM可以直接使用的文件。Java类型,这是JVM的类加载机制。如果对Class文件的结构不熟悉,可以参考之前的文章Class文件结构综合解析(上)和Class文件结构综合解析(下)。一个类的生命周期从加载到内存到从内存中卸载,类分为以下几个步骤:加载(Loading)验证(Verification)准备(Preparation)解析(Resolution)初始化(Initialization)使用(Using)卸载(Unloading)类加载的全过程,包括加载、验证、准备、解析、初始化等阶段。加载加载是类加载的第一阶段。在这一步中,JVM规范要求完成以下三件事:通过类的完全限定名获取定义该类的二进制字节流。将这个字节流表示的静态存储结构转换成方法区的运行时数据结构。在内存中创建一个表示此类的java.lang.Class对象。以上需求其实并不具体,JVM的具体实现和应用都比较灵活。比如:获取这个类的二进制字节流,并没有说从哪里获取,如何获取,所以有从压缩包中读取(jar,war,ear),从网络中获取(Applet),运行时计算生成(DynamicProxy)。对于不是数组的类的加载,我们可以定义自己的类加载器来控制如何获取字节流。但是对于数组类就不一样了,因为数组类本身并不是通过类加载器创建的,而是由JVM直接创建的。这个阶段的验证是为了确保Class文件的字节流中包含的信息符合当前JVM的要求,不危及JVM本身的安全。大致分为以下四个阶段:文件格式验证验证字节流是否符合Class文件格式的规范,是否可以被当前的JVM处理。验证点很多,比如:是否以幻数0xCAFEBABE开头,主次版本号是否在当前JVM的处理范围内,常量池中的常量是否有不支持的常量类型,是否有不符合UTF8编码数据的CONSTANT_Utf8_info类型常量等。该阶段基于二进制字节流进行验证。只有这个阶段验证通过了,字节流才能存放到内存的方法区。元数据验证这个阶段主要是对类的元数据信息进行语义分析和验证,确保不存在不符合Java语言规范的元数据信息。例如:java.lang.Object以外的类是否有父类,是否继承了不允许继承的类,非抽象类是否实现了其父类需要实现的所有方法或者接口,以及是否重写了父类的final字段等。字节码验证阶段对数据流和控制流进行分析,确保程序语义合法、合乎逻辑。例如:放置和使用操作栈时,保证数据类型一致,保证跳转指令不会跳转到方法体以外的字节码指令,保证方法体中的类型转换有效,等等。Symbolreferenceverification这个阶段是验证类本身以外的信息(常量池中的各种符号引用)的匹配。发生在解析步骤,保证解析能够正常进行,例如:字符串在符号引用中传递是否描述的全限定名是否能找到对应的类,类字段方法中的可访问性符号引用可以访问当前类,等等。准备工作该阶段为静态变量分配内存,设置静态变量初值。这里所说的初始值通常不是代码中写的初始值,而是数据类型的零值。代码中写入的初始值是在初始化阶段赋值的。如果是静态常量(被final修饰),这个阶段会直接赋值代码中写的初始值。解析在这个阶段,JVM将常量池中的符号引用替换为直接引用。符号引用使用一组符号描述引用的目标。该符号可以是任何形式的字面量,只要它可以用来明确定位目标即可。它与JVM实现的内存布局无关。直接引用可以是直接指向目标的指针,也可以是相对偏移量,也可以是可以间接定位目标的句柄,这与JVM实现的内存布局有关。如果存在直接引用,那么被引用的目标一定存在于内存中。解析主要针对类或接口的符号引用、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符,对应类的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_InvokeDynamic_info常量池分别。初始化初始化阶段实际上开始执行类中定义的字节码,也就是执行类构造函数()方法的过程。()方法是编译器自动收集类中所有的静态变量赋值和静态语句块中的语句生成的。编译器收集的顺序由语句在源文件中出现的顺序决定,静态语句块只能访问静态语句块之前定义的变量,其后定义的变量,静态语句块可以已分配,但无法访问。JVM会保证父类的()方法在子类的()方法执行之前已经执行完毕,也就是说父类中定义的静态语句块优先于子类的变量赋值操作。如果类没有静态语句块,也没有给静态变量赋值,编译器就不会为这个类生成()方法。接口的()方法不需要先执行父接口的()方法,只有使用到父接口中定义的变量时才会初始化父接口。JVM会确保类的()方法在多线程环境中被正确锁定和同步。如果一个线程正在执行该类的()方法,其他线程需要阻塞等待。()方法执行后,其他线程不会再进入()方法。在同一个类加载器下,一个类只会被初始化一次。结束语这次我们了解了类加载过程的几个阶段,即加载、验证、准备、解析和初始化。加载是将二进制字节码加载到内存中,验证是检查字节流中包含的信息是否符合要求,准备是为静态变量分配内存并设置静态变量的初始值,解析是替换中的符号引用常量池为直接引用,初始化是执行静态语句块中的所有静态变量赋值动作和语句。