JVM-类加载的过程

93次阅读

共计 1355 个字符,预计需要花费 4 分钟才能阅读完成。

类从被加载到 JVM 中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。

类加载的过程:加载 -> 链接 -> 初始化

加载 : 简单的说,类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到 JVM 内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的对象实例
加载需要借助类加载器,在 Java 虚拟机中,类加载器使用了双亲委派模型,即接收到加载请求时,会先将请求转发给父类加载器。

双亲委派模型:每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载

链接 : 链接阶段要做的是将加载到 JVM 中的二进制字节流的类数据信息合并到 JVM 的运行时状态中, 经由验证、准备和解析三个阶段
即将类合并至 Java 虚拟机中,使之能够执行的过程。

(1)验证
验证类数据信息是否符合 JVM 规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等。格式验证:验证是否符合 class 文件规范
语义验证:检查一个被标记为 final 的类型是否包含子类;检查一个类中的 final 方法视频被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否通过富豪引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)(2)准备
为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)被 final 修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在 ConstantValue 属性,则在准备阶段,其值就是 ConstantValue 的值(3)解析
将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。可以认为是一些静态绑定的会被解析,动态绑定则只会在运行是进行解析;静态绑定包括一些 final 方法(不可以重写),static 方法(只会属于当前类),构造器(不会被重写)

初始化 : 是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。
只有当初始化完成之后,类才正式成为可执行的状态。以盖房子为例,只有当房子装修过后,Tony 才能真正地住进去。
类的初始化仅会被执行一次,这个特性被用来实现单例的延迟初始化。

那么,类的初始化何时会被触发呢?JVM 规范枚举了下述多种触发情况:

1. 当虚拟机启动时,初始化用户指定的主类;2. 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;3. 当遇到调用静态方法的指令时,初始化该静态方法所在的类;4. 当遇到访问静态字段的指令时,初始化该静态字段所在的类;5. 子类的初始化会触发父类的初始化;6. 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;7. 使用反射 API 对某个类进行反射调用时,初始化这个类;8. 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。

正文完
 0

jvm类加载的过程

94次阅读

共计 1915 个字符,预计需要花费 5 分钟才能阅读完成。

一个类从加载到虚拟机到使用结束从虚拟机卸载包括了加载、验证、准备、解析、初始化、使用、卸载,即为一个类的生命周期
下面来看一下类加载的过程,即加载、验证、准备、解析、初始化 5 个阶段都做了什么事:
阶段 1:加载
加载阶段虚拟机主要 3 件事:

通过类的全名获取其二进制字节流;
将字节流代表的静态结构转化为方法区识别的运行时数据结构;
在内存中实例化这个类的 java.lang.Class 对象(不一定在堆内存中的,HotSpot 就将 Class 对象放在了方法区里),程序访问这个类在方法区中的类型数据时会通过这个类去访问;以上三点虚拟机并不要求如何实现,只是一个规范,比如第一步,通过类全名获取其二进制流,动态代理技术是在运行时获取、JSP 应用是根据 jsp 文件获取并生成对应的 Class 以及从 ZIP 包中获取(JAR、EAR、WA 同理)等

阶段 2:验证
验证阶段大体上会完成 4 个阶段的验证(文件格式验证、元数据验证、字节码验证、符号引用验证),以保证虚拟机中类的规范和安全。

文件格式验证,校验字节流是否复合 Class 文件的格式:

验证文件是否以魔数 0xCAFEBABE(十六进制 class 文件中的前 4 个字节) 开头;
主、次版本号 (十六进制 class 文件中的第 5、第 6 个字节)能否被当前版本的虚拟机处理;
常量池中是否有不被支持的类型;
指向常量的索引中是否指向了不存在的常量;
Class 文件中各个部分以及文件本身是否有被删除或附加的其他信息;

……

元数据类型,校验语义是否符合 Java 语言规范的要求:

验证类是否有父类(除了 java.lang.Object);
验证父类是否继承了不可被继承的类;
如果不是抽象类,那么要判断是否实现了父类或接口的所要求实现的所有方法;

……

字节码验证,校验类的方法体,确定语义是否符合逻辑:

保证操作数栈中的数据类型与指令序列一致;
保证跳转指令不会跳到方法体外的字节码指令上;
保证方法体中的类型转换有效;

阶段 3:准备
准备阶段是为类变量分配内存并设置类变量初始值的阶段
这里所说的初始值并不是指代码赋的值,而是数据类型的默认值,如 public static int value = 123; 在准备阶段过后,value 会被置为 0, 而不是 123。同时要注意,public static final int value = 123; 这种使用 final 修饰的变量,在准备阶段就会被赋值为 123, 而不是初始值。

阶段 4:解析
解析阶段会将常量池内的符号引用转换为直接引用,关于符号引用和直接引用的解释如下:
符号引用:以一组符号来描述所引用的目,比如定义了在类 IntF 中定义了 intValue = 123,接着让 Test.foo 中的 a 变量指向 Intf.intValue:
public class Test{
public void foo(){
int a = Intf.intValue;
}
}
class Intf{
public static int intValue = 123;
}
编译代码之后我们用 javap -verbose Test 来查看 class 文件中的内容:
Constant pool:
#1 = Methodref #4.#12 // java/lang/Object.”<init>”:()V
#2 = Fieldref #13.#14 // Intf.intValue:I
#3 = Class #15 // Test
#4 = Class #16 // java/lang/Object
// 省略部分代码 …
public void foo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=1
0: getstatic #2 // Field Intf.intValue:I
3: istore_1
4: return
LineNumberTable:
line 3: 0
line 4:
可以看到常量池第 2 项是一个符号引用,指向了 Intf.intValue
直接引用:就是我们常说的指针或者句柄,直接引用的目标一定会在虚拟机内存中存在。

阶段 5:初始化
初始化阶段是类加载的最后一个阶段,主要执行类的 <clinit> 方法(不同与 <init> 方法,<init> 方法是在显式调用 constructor 时执行,而 <clinit> 方法在初始化阶段就会执行),<clinit>() 方法会执行赋值操作和执行静态语句快中的内容,换句话说,如果代码中没有静态语句块和赋值操作,那么就可以没有 <clinit>() 方法。这个阶段虚拟机会保证父类的 <clinit>() 方法会在子类的 <clinit>() 方法前执行,而且在多线程环境中,虚拟机会保证 <clinit>() 方法的同步。
参考文献:《深入理解 Java 虚拟机》– 周志明

正文完
 0