如果你还不相识Java类的加载过程,来看看这一篇吧
文章首发于【Java天堂】,跟随我探索Java进阶之路!虚拟机类加载机制
在Java代码被编译成Class文件之后,终极需要加载到Java虚拟机中才能被运行和利用,Java虚拟机加载Class文件到内存,并对数据进行校验、转换、解析和初始化之后,才变成了我们真正可以利用的Java类型,这个过程就叫做Java虚拟机的类加载机制。
C++等语言在步伐编译时有一个连接的过程,在连接时相称于就是把需要依赖的资源进行整合到一起,变成一个可实验步伐。但Java的编译差别,在Java语言中,类型的加载、连接和初始化这些动作都是在步伐运行期间动态完成的,这样会导致Java语言在提前编译方面变得困难,由于要到实际运行的时候才能知道实际的实现类。
例如,你写了一个接口,可以等到步伐实际运行的时候再动态的加载详细的实现,而且加载的方式也不一定非得从Class文件加载,你甚至可以从网络上或者其他地方加载一个二进制流来作为步伐的一部分,这种Java运行期类加载的方式,大大提升的Java语言的灵活性。
类加载的机遇
一个类型从加载开始到终极的消亡,整个生命同期会履历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、利用(Using)、卸载(Unloading)共七个阶段,整个过程如下:
https://img2024.cnblogs.com/other/2337066/202405/2337066-20240511234941957-1744244890.png
验证、准备、解析三个过程,可以统称为连接(Linking)过程。
上图中的各个过程,并不是严格按照指定的顺序按部就班的实验,其中加载、验证、准备、初始化和卸载,这几个的顺序是确定的,其他阶段可能会穿插在这个过程当中交叉混合的实验,会在一个阶段实验的过程当中调用另外一个阶段。
关于什么时候触发第一个过程"加载",《Java虚拟机规范》中没有强制的规定,可以交给虚拟机自由发挥,可能差别的虚拟机触发的机遇会存在差异,这部分内容不做重点先容。但对于初始化的触发条件,《Java虚拟机规范》有严格的规则,有且只有六种情况必须对类进行初始化动作
[*]碰到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要触发其初始化。这四个字节码指令,分别表示对创建新对象、获取静态字段、设置静态字段、调用静态方法
[*]利用java.lang.reflect包的方法对类型进行反射调用时,如果类型没有初始化,需要触发其初始化
[*]对于父类,如果子类在初始化的时候发现父类没有初始化,会触发父类的初始化
[*]当虚拟机启动时,需要先指定一个启动类(包罗main方法),虚拟机会先初始化这个启动类
[*]当利用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化
[*]当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化
1 - 4对于熟悉Java底子的同砚,应该比力好理解,基本上都是常规操作。5-6相对来说利用的不太多,相识一下即可
类的加载过程
上面我们先容了类生命周期的7个过程,加载过程主要包括加载、验证、准备、解析和初始化,重点先容这几个部分的过程
1、加载
加载阶段,Java虚拟机主要完成三件事情:
[*]通过类的全限定名称,获取类的二进制字节流
[*]将二进制流中的静态存储结构转化为方法区的运行时数据结构
[*]在内存中生成一个此类的java.lang.Class对象
在根据全限定名称获取二进制字节流时,并没有详细的规则,从哪里获取?如何获取?这就给了开发者很大的发挥空间。
我们前面说过,Java虚拟机加载的类型不仅限于从Class文件加载, 它可以从网络或者其他任何地方加载,只要遵循Java虚拟机的规则就行。
在Java的发展历程中,布满创造力的开发者们玩出了各种花样,Java许多重要的技术都基于这种特性发展起来的
[*]从压缩包中读取,比如War包、Jar包,有没有很熟悉?
[*]从网络中获取,比如Web Applet等
[*]运行时计算生成,这个动态署理技术利用的最多,比如Spring框架大量利用到动态署理
......
还有许多其他的加载途径,这里就不一一罗列了。
加载阶段结束后,表示Java类型的二进制字节流就按照虚拟机的格式存储在方法区之中了,之后会在Java堆内存中实例化一个java.lang.Class类的对象,这个对象作为步伐访问方法区中的类型数据的外部接口。
2、验证
验证阶段主要是为了保证Class字节流中包罗的信息是符合《Java虚拟机规范》所要求的,不能出现一些危害Java虚拟机自身安全的内容。
可能有的人会有疑问,为什么不在编译的时候就完验证?直接在编译的时候发现有恶意的代码,编译器就直接拒绝编译,这就可以避免后面加载的时候再去验证正当性。但是前面我们说过,Class文件二进制字节流不一定非得从Java文件编译而来,它可以从许多其他途径加载而来,Java虚拟机如果不检查输入的字节流,对其完全信托的话,很可能会由于载入了有错误或有恶意计划的字节码流而导致整个系统受攻击甚至瓦解,所以验证字节码是Java虚拟机保护自身的一项必要措施
从团体上看,验证阶段会完成四个方面的验证
[*]文件格式验证
[*]元数据验证
[*]字节码验证
[*]符号引用验证
1、文件格式验证
这一阶段主要验证字节流是否符合Class文件格式的规范,并且能被当前版本的Java虚拟机处理。主要的验证点包括
[*]魔数验证,是否以0xCAFEBABE开头
[*]主、次版本号是否在当前Java虚拟机接受范围之内
[*]常量池的常量中是否有不被支持的常量类型(检查常量tag标志)
[*]指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
.......
还有许多其他的验证,总之这一阶段侧重于对格式的验证,保证字节流能被正确的解析并存储于方法区内,格式上符合一个Java类型的基本要求。
2、元数据验证
这一阶段主要是对于字节码的描述文件进行分析,以保证其符合《Java语言规范》的要求,主要的验证点包括
[*]是否有除了Object类以外的父类
[*]是否继承了不被答应继承的类
[*]类中的方法、字段是否与父类有辩说
......
还有许多其他类型的验证,总之这一阶段侧重于对类的元数据信息进行分析,保证不存在与《Java语言规范》相违背的内容出现
3、字节码验证
这一阶段主要是对字节码进行分析,通过分析数据流和控制流,确定步伐的语义是正当的,符合逻辑的。这一阶段是对类的方法体进行分析,保证类的方法在运行时不会做出危害Java虚拟机的行为。主要验证点包括:
[*]保证任何跳转指令都不会跳转到方法体以外的字节码指令上
[*]保证方法体中的类型转换总是有效的
[*]保证任意时刻操作数栈的数据类型与指令代码序列都能共同工作
......
还有许多其他类型的验证,总之这一阶段侧重于对代码逻辑进行分析和验证,确保不会有非法行为。由于数据流分析和控制流分析的高度复杂性,所以这一阶段是整个验证过程最复杂的部分
4、符号引用验证
这一阶段可以看作是对类自身以外的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。主要验证点包括:
[*]符号引用中通过字符串描述的全限定名是否能找到对应的类
[*]在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段
[*]符号引用中的类、字段、方法的可访问性(private、protected、public、)是否可被当前类访问
......
还有许多其他类型的验证,总之这一阶段的主要目标是确保解析行为能正常实验
验证阶段对于虚拟机的类加载机制来说,是一个非常重要的、但却不是必须要实验的阶段,由于验证阶段只有通过或者不通过的差别,只要通过了验证,其后就对步伐运行期没有任何影响了。如果步伐运行的全部代码(包括自己编写的、第三方包中的、从外部加载的、动态生成的等所有代码)都已经被反复利用和验证过,在生产环境的实施阶段就可以考虑利用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间
3、准备
准备阶段主要的任务就是为类中的静态(static修饰)变量分配内存空间并设置初始值。这里需要注意,准备阶段仅仅是为静态变量分配内存,由于静态变量是属于类的变量,普通变量分配内存需要等到创建对象时才会进行。而且在这一阶段设置初始值,是设置各个类型的零值,比如:
public static int hello = 123456;变量hello在准备阶段会被赋初始值0,而不是123456。
当然,上面是一般情况下,也会有特殊情况,如果类的字段存在常量值,那么在准备阶段就会被赋值常量值,比如:
public static final int hello = 123456;javac在编译时,就会将123456赋给hello,那么在准备阶段虚拟机就会根据123456的值来设置给hello
4、解析
解析阶段是Java虚拟机将常量池内的符号引用更换为直接引用的过程,先来解释一下两个概念:符号引用、直接引用
[*]符号引用:是以一组符号来表示所引用的目标,引用的目标不一定是已经加载到虚拟机当中的的内容
[*]直接引用:表示可以直接指向目标的指针、相对偏移量或者是能间接定位到目标的句柄。如果有了直接引用,引用的目标必然已经存在于虚拟机的内存中
《Java虚拟机规范》之中并未规定解析阶段发生的详细时间,只要求了在实验ane-warray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invoke-special、invokestatic、invokevirtual、ldc、ldc_w、ldc2_w、multianewarray、new、putfield和putstatic这17个用于操作符号引用的字节码指令之前,先对它们所利用的符号引用进行解析
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行,分别对应于常量池的CONSTANT_Class_info、CON-STANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_Dyna-mic_info和CONSTANT_InvokeDynamic_info 8种常量类型
详细解析动作较复杂,对于每一种类型都有差别的解析方法,如果对于解析过程想深入研究,可以参考《Java虚拟机规范》,这里再不进行过多的阐述
5、初始化过程
前面的过程,都是由Java虚拟机来主导的。从初始化开始,Java虚拟机才真正开始实验类中编写的Java步伐代码,这是类加载的最后一个阶段。
在准备阶段,静态变量已经被赋值过一次零值。在初始化阶段才会真正赋予步伐员编码时给定的值。
其实简单来讲,初始化阶段就是实验类构造器()方法的过程,()并不是步伐员自己写的,是由javac编译器主动调用的
()方法是由编译器主动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
Java虚拟机必须保证一个类的()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去实验这个类的()方法,其他线程都需要阻塞期待,直到运动线程实验完毕()方法
本文由博客一文多发平台 OpenWrite 发布!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]