一. Class文件
Java有一个非常著名的宣传口号:“一次编写,到处运行”(Write once, run anywhere、WORA,有时也作“write once, run everywhere”、WORE)。理想中Java可以在任何设备上开发,编译成一段标准的字节码并且可以在任何安装有Java虚拟机(JVM)的设备上运行。当不同平台的Jvm都要能正常的运行Java程序,就需要一个标准来让Jvm能统一的理解我们所编写的Java代码,这个标准就是class文件,class文件主要就是解决平台无关性的中间文件。
在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且所有平台都统一支持程序存储格式:字节码(Byte Code),因此Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。
Class文件是一种8位字节的二进制流文件,各个数据项按顺序紧密的从前向后排列,相邻的项之间没有间隙, 这样可以使得class文件非常紧凑,体积轻巧,可以被JVM快速的加载至内存,并且占据较少的内存空间(方便于网络的传输)。根据 Java 虚拟机规范,Class文件通过ClassFile定义,结构如下:
ClassFile {
u4 magic; //Class 文件的标志
u2 minor_version; //Class 的小版本号
u2 major_version; //Class 的大版本号
u2 constant_pool_count; //常量池的数量
cp_info constant_pool[constant_pool_count-1]; //常量池
u2 access_flags; //Class 的访问标记
u2 this_class; //当前类
u2 super_class; //父类
u2 interfaces_count; //接口数量
u2 interfaces[interfaces_count]; //一个类可以实现多个接口
u2 fields_count; //字段数量
field_info fields[fields_count]; //一个类可以有多个字段
u2 methods_count; //方法数量
method_info methods[methods_count]; //一个类可以有个多个方法
u2 attributes_count; //此类的属性表中的属性数
attribute_info attributes[attributes_count]; //属性表集合
}
我们如果想看一个类的字节码信息,可以用过IDEA的jclasslib插件进行查看,效果图如下:
接下来过一遍ClassFile 的结构:
magic 魔数
每个 Class 文件的头 4 个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的 Class 文件。Java 规范规定魔数为固定值:0xCAFEBABE
。如果读取的文件不是以这个魔数开头,Java 虚拟机就会拒绝加载它。
minor_version 和 major_version 版本号
紧接着魔数的四个字节是Class文件的此版本号和主版本号。紧接着魔数的四个字节存储的是 Class 文件的版本号:第5和第6个字节是次版本号,第7和第8个字节是主版本号。随着Java的发展,Class文件的格式也会做相应的变动。版本号标志着Class文件在什么时候,加入或改变了哪些特性。举例来说,不同版本的javac编译器编译的Class文件,版本号可能不同,而不同版本的JVM能识别的class文件的版本号也可能不同,一般情况下高版本的JVM能识别低版本的javac编译器编译的class文件,而低版本的JVM不能识别高版本的javac编译器编译的class文件。 如果使用低版本的JVM执行高版本的class文件,JVM会抛出java.lang.UnsupportedClassVersionError
。
constant_pool 常量池
Class 文件的 constant_pool(常量池)是一个变长表,用于存储编译器生成的各种字面量和符号引用。常量池紧跟在版本后之后,常量池的数量是 constant_pool_count-1
,常量池计数器是从 1 开始计数的,将第0项常量空出来是有特殊考虑的,索引值为0代表“不引用任何一个常量池项”。
常量池中存放了文字字符串、常量值、当前类的类名、字段名、方法名、各个字段和方法的描述符、对当前类的字段和方法的引用信息、当前类中对其他类的引用信息等等。
常量池是一个类的结构索引,其它地方对“对象”的引用可以通过索引位置来代替,我们知道在程序中一个变量可以不断地被调用,要快速获取这个变量常用的方法就是通过索引变量。这种索引我们可以直观理解为“内存地址的虚拟”。我们把它叫静态池的意思就是说这里维护着经过编译“梳理”之后的相对固定的数据索引,它是站在整个JVM(进程)层面的共享池。
常量池中每一项常量都是一个表,这 14 种表有一个共同的特点:开始的第一位是一个u1类型的标志位 -tag
来标识常量的类型,代表当前这个常量属于哪种常量类型。
类型 | 标志(tag) | 描述 |
---|---|---|
CONSTANT_utf8_info | 1 | UTF-8编码的Unicode字符串 |
CONSTANT_Integer_info | 3 | 整形字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_FieldRef_info | 9 | 字段的符号引用 |
CONSTANT_MethodRef_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodRef_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 标志方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
上面提过的IDEA插件 jclasslib 也可以用来查看Class文件的常量池信息:
此处思考一个问题:Class文件的常量池 和 Jvm内存划分中的字符串常量池和运行时常量池 有什么关联?
看过我之前文章:Jvm相关知识 这一篇的同学或者对Jvm有了解的同学,到这里应该就能联想了:
Class 文件中的常量池是在编译期间生成的,用于表示类的各种常量和信息;而字符串常量池和运行时常量池是 JVM 在运行时使用的,用于存储字符串字面量和支持运行时操作的常量信息。字符串常量池和运行时常量池中的内容是与 Class 文件中的常量池相关联的,它们是 JVM 运行时常量池的一种映射。
Access Flags 访问标志
在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口,是否为 public 或者 abstract 类型,如果是类的话是否声明为 final 等等。访问标志的取值是一个16位的无符号整数,其中每一位都代表了一种特定的访问权限或特性,访问标志的组合可以表示不同的访问权限和特性。例如,一个类既可以是 ACC_PUBLIC 和 ACC_FINAL 的,表示它是一个公共且不可继承的类。通过读取访问标志字段,可以确定一个类或接口的访问权限和特性,从而在加载和执行时进行相应的限制和处理。
常见的访问标志包括:
FlagName | Value | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | 表示类或接口是公共的,可以被其他类访问 |
ACC_FINAL | 0x0010 | 表示类不能被继承,或者方法不能被重写 |
ACC_SUPER | 0x0020 | 已过时,现在的 JVM 实现都会设置这个标志 |
ACC_INTERFACE | 0x0200 | 表示这个类是一个接口 |
ACC_ABSTRACT | 0x0400 | 表示类是抽象的,不能被实例化,或者方法是抽象的,需要子类提供具体实现 |
ACC_SYNTHETIC | 0x1000 | 表示这个类或方法是由编译器生成的,而不是由开发人员编写的 |
ACC_ANNOTATION | 0x2000 | 表示这个类是一个注解类型 |
ACC_ENUM | 0x4000 | 表示这个类是一个枚举类型 |
上面提过的IDEA插件 jclasslib 也可以用来查看Class文件的访问标志信息:
this_class(当前类)、super_class(父类) 和 interfaces(接口索引集合)
- this_class 保存了当前类的全局限定名在常量池里的索引
- super class 保存了当前类的父类的全局限定名在常量池里的索引
- interfaces 保存了当前类实现的接口列表,包含两部分内容:interfaces_count 和interfaces[interfaces_count],interfaces_count 指的是当前类实现的接口数目,interfaces[] 是包含interfaces_count个接口的全局限定名的索引的数组,一个类可以实现多个接口
Fields 字段表集合
字段表(field info)用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。包含两部分内容:interfaces_count
和interfaces[interfaces_count]
,interfaces_count
指的是当前类实现的接口数目,interfaces[]
是包含interfaces_count个接口的全局限定名的索引的数组。
一个字段(field info)的结构可以简单描述为:
{
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
- access_flags: 字段的作用域(public ,private,protected修饰符),是实例变量还是类变量(static修饰符),可否被序列化(transient 修饰符),可变性(final),可见性(volatile 修饰符,是否强制从主内存读写);
- name_index: 对常量池的引用,表示的字段的名称;
- descriptor_index: 对常量池的引用,表示字段和方法的描述符;
- attributes_count: 一个字段还会拥有一些额外的属性,attributes_count 存放属性的个数;
- attributes[attributes_count]: 存放具体属性具体内容,这些属性可以提供关于字段的详细信息,例如注解、泛型签名、常量值等。
其中Access Flags字段的常见取值如下:
Name | value | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | 可以被其他类访问 |
ACC_PRIVATE | 0x0002 | 只能在声明字段的类内部访问 |
ACC_PROTECTED | 0x0004 | 可以在同一包内及其子类中访问 |
ACC_STATIC | 0x0008 | 表示静态字段 |
ACC_FINAL | 0x0010 | 表示字段值在初始化后不能被修改 |
ACC_VOLATILE | 0x0040 | 表示字段可能会被多个线程同时访问,因此不会被缓存 |
ACC_TRANSIENT | 0x0080 | 表示字段不会被序列化 |
ACC_SYNTHETIC | 0x1000 | 表示字段是编译器生成的 |
ACC_ENUM | 0x4000 | 表示字段是枚举类型 |
其中Attributes的常见属性如下:
- ConstantValue(常量值属性):用于表示字段的常量值,在类加载时被设置并且不能改变。例如 final static int COUNT = 10; 中的 10。
- Synthetic(合成属性):表示字段是由编译器自动生成的,而不是源代码中明确声明的,通常是为了支持 Java 语言的特性,如内部类访问外部类的私有字段。
- Deprecated(过时属性):表示字段已被废弃,不建议继续使用,但仍然保留在类中以保持向后兼容性。
- Signature(签名属性):用于表示字段的泛型签名信息,包括类型参数和泛型类型。
- RuntimeVisibleAnnotations(可见注解属性):用于表示字段的可见注解信息,可以在运行时通过反射等机制获取这些注解。
- RuntimeInvisibleAnnotations(不可见注解属性):与 RuntimeVisibleAnnotations 类似,但注解在运行时不可见,仅供编译器等工具使用。
- RuntimeVisibleTypeAnnotations(可见类型注解属性):用于表示字段的可见类型注解信息,包括对字段类型的注解。
- RuntimeInvisibleTypeAnnotations(不可见类型注解属性):与 RuntimeVisibleTypeAnnotations 类似,但注解在运行时不可见。
- AnnotationDefault(注解默认值属性):用于表示注解类型的字段的默认值。
- BootstrapMethods(引导方法属性):用于支持动态链接,例如在 Java 8 中用于支持 lambda 表达式和动态方法调用。其中包含了一组引导方法,用于在运行时动态创建和调用方法。
Methods 方法表集合
methods 保存了当前类的方法列表,包含两部分的内容:methods_count和methods[methods_count],methods_count是该类或者接口显示定义的方法的数量,method[]是包含方法信息的一个详细列表。
一个方法(method_info)的结构可以简单描述为:
{
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
可以看到方法和字段的描述基本一致
- 方法访问标志(Access Flags):用于描述方法的访问权限和特性,比如 ACC_PUBLIC、ACC_PRIVATE、ACC_STATIC 等。
- 方法名索引和描述符索引:方法名索引指向常量池中方法名的常量项,描述符索引指向常量池中方法描述符的常量项。通过这两个索引可以确定方法的名称和参数类型。
- 属性表(Attributes):描述方法的额外信息,比如注解、方法的代码等。
其中Access Flags字段的常见取值如下:
Name | value | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | 可以被其他类访问 |
ACC_PRIVATE | 0x0002 | 只能在声明方法的类内部访问 |
ACC_PROTECTED | 0x0004 | 可以在同一包内及其子类中访问 |
ACC_STATIC | 0x0008 | 表示静态方法 |
ACC_FINAL | 0x0010 | 表示方法在子类中不能被重写 |
ACC_SYNCHRONIZED | 0x0020 | 表示方法在多线程环境下同步执行 |
ACC_BRIDGE | 0x0040 | 表示方法是由编译器生成的桥接方法 |
ACC_VARARGS | 0x0080 | 表示方法的参数是一个可变长度参数 |
ACC_NATIVE | 0x0100 | 表示方法用本地方法实现,即由本地代码(Native Code)实现 |
ACC_ABSTRACT | 0x0400 | 表示方法没有具体实现,需要子类实现 |
ACC_STRICT | 0x0800 | 表示方法使用了严格的浮点计算规则 |
ACC_SYNTHETIC | 0x1000 | 表示方法是编译器生成的 |
ACC_MANDATED | 0x8000 | 表示方法是由 Java 核心库或者其他标准引入的,不能在源代码中直接使用 |
其中Attributes的常见属性如下:
- Code:用于存储方法的字节码指令,包括方法的操作数栈、局部变量表和异常处理器等信息。
- Exceptions:用于指定方法可能抛出的异常类型。
- RuntimeVisibleParameterAnnotations:用于存储方法参数的可见注解。
- RuntimeInvisibleParameterAnnotations:用于存储方法参数的不可见注解。
- AnnotationDefault:用于存储注解类型的方法的默认值。
- MethodParameters:用于存储方法参数的名称和修饰符。
- Synthetic:表示方法是由编译器生成的。
- Signature:表示方法的泛型签名。
- Deprecated:表示方法已被废弃。
- RuntimeVisibleAnnotations:用于存储方法的可见注解。
- RuntimeInvisibleAnnotations:用于存储方法的不可见注解。
- RuntimeVisibleTypeAnnotations:用于存储方法的可见类型注解。
- RuntimeInvisibleTypeAnnotations:用于存储方法的不可见类型注解。
Attributes 属性表集合
Attributes 包含了当前类的Attributes列表,包含两部分内容:attributes_count 和 attributes[attributes_count],是Class文件的最后一部分是属性,它描述了该类或者接口所定义的一些属性信息,包括类的版本号、常量池、字段、方法、接口等。attributes_count指的是attributes列表中包含的attribute_info的数量。
在Class文件中,属性表集合包括Java虚拟机预先规范定义的属性以及用户自定义的属性,对于用户自定义的属性,虚拟机加载的时候会自动忽略掉。Class文件、字段表、方法表都可以携带自己的属性表集合,便于描述某些场景专有的信息。在 Class 文件中每个结构(如类、字段、方法等)都有自己的属性表集合。例如,一个类的属性表集合包含了类的修饰符、常量池、字段、方法、类的注解等信息;一个字段的属性表集合包含了字段的修饰符、字段的初始值、字段的注解等信息;一个方法的属性表集合包含了方法的修饰符、方法的字节码、方法的异常信息、方法的注解等信息。
属性表集合中的属性以二进制格式存储,每个属性由一个属性名索引和一个属性长度组成,后面跟着属性的具体内容。属性名索引指向常量池中的一个常量项,表示属性的名称,属性长度表示属性的长度,单位为字节。属性的具体内容根据属性类型不同而不同,例如 Code 属性存储方法的字节码指令,Exceptions 属性存储方法可能抛出的异常类型等。
对于每一个属性,它的名称都要从constant_pool(常量池)中引入一个CONSTANT_UTF8_info
类型的常量来表示,而属性表的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。
简单来说:Attributes属性表集合用于存储额外的元数据信息,描述类或者类中成员的特性、行为等,每个类、字段、方法等结构都可以包含零个或多个属性,以提供更丰富的描述信息,属性以二进制格式存储,包含属性名索引、属性长度和属性内容等信息。
Attributes中预定义的属性有:
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量池 |
Deprecated | 类,方法,字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClass | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部便狼描述 |
StackMapTable | Code属性 | JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 |
Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | 用于存储额外的调试信息 |
Synthetic | 类,方法表,字段表 | 标志方法或字段为编译器自动生成的 |
LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 |
RuntimeInvisibleAnnotations | 表,方法表,字段表 | 用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParameterAnnotation | 方法表 | 作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法 |
RuntimeInvisibleParameterAnnotation | 方法表 | 作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数 |
AnnotationDefault | 方法表 | 用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | 用于保存invokeddynamic指令引用的引导方式限定符 |
二. 类加载过程
Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢?系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析。
1. 加载(Loading)
加载(Loading)是Java虚拟机(JVM)类加载过程的第一步,主要负责将类的字节码文件从磁盘加载到内存中,并创建表示该类的Class对象。加载类的过程大致如下:
-
定位类文件: 类加载器根据类的全限定名(Fully Qualified Name)来定位对应的类文件。全限定名就是包名加上类名,如"com.example.MyClass"。类加载器会根据类路径(Classpath)来搜索类文件,类路径可以包括文件系统路径或者JAR包路径。
-
读取类文件: 类加载器根据定位到的类文件路径,使用文件输入流(FileInputStream)或其他读取类文件的方式,将类的字节码文件(以.class为扩展名)读取到内存中,并以字节流的形式表示。
-
创建类的数据结构: 在将类的字节码文件加载到内存后,JVM会根据字节流内容创建表示类的数据结构。这个数据结构通常包括类的字段信息、方法信息、父类引用、接口列表等。
-
生成Class对象: JVM会为每个被加载的类生成一个对应的Class对象。Class对象是在JVM中表示类的对象,它包含了对应类的结构信息,并提供了访问类的字段和方法的接口。
-
存储在方法区: 类的数据结构和对应的Class对象会被存储在方法区(Method Area)中。在JDK 8中,Class 对象的实例数据是在堆中分配的,而类的元数据(如类名、访问修饰符、字段信息、方法信息等)存储在元空间(Metaspace)中。元空间是是堆外的一部分,与堆和方法区是独立的。在JDK 8之前的版本中,类的元数据存储在永久代(PermGen)中,但由于永久代的内存管理和限制问题,JDK 8引入了元空间来替代永久代,从而更好地管理类的元数据。
类的加载主要依靠类加载器(双亲委派模型)完成,文章后续会专门分析类加载器,目前只需要知道这个是就行;需要注意的是,在加载阶段,JVM只是将类的字节码文件加载到内存中,并创建对应的数据结构和Class对象,并不会执行类的静态代码块或其他初始化操作。这些操作会在接下来的链接(Linking)和初始化(Initialization)阶段中完成。
2. 验证(Verification)
验证是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
验证阶段通常包括以下几个方面的内容:
- 文件格式验证(File Format Verification): 首先,验证器会检查类文件的格式是否符合Java虚拟机规范。这包括检查魔数、版本号等是否正确,以及各个部分的长度、起始位置等是否合法。
- 元数据验证(Metadata Verification): 验证器会对类的元数据信息进行验证,确保类的结构、字段和方法的描述符等信息是正确的,符合JVM规范。
- 字节码验证(Bytecode Verification): 验证器会对类的字节码进行验证,确保字节码是合法、安全的。这包括检查跳转指令是否指向正确的指令地址,访问数组是否越界,方法调用是否合法等。
- 符号引用验证(Symbolic Reference Verification): 验证器会检查类中的符号引用是否能够正确解析。符号引用是指在编译期间生成的引用,而验证器会尝试将这些引用解析为直接引用,以确保引用的目标是有效的。
通过这些验证步骤,JVM可以确保加载的类文件是合法的、安全的,不会破坏JVM的稳定性和安全性。如果验证失败,则会抛出VerifyError
,表示类文件不符合规范,无法被加载和执行。
3. 准备(Preparation)
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。大致过程如下:
-
分配内存空间: 对于每个类的静态变量,在方法区(JDK 8 后的元空间)中分配内存空间。这样,每个静态变量都有自己的内存地址,但并未初始化为真正的值。
-
设置默认初始值: 对于基本数据类型的静态变量,Java 虚拟机会根据类型设置默认初始值。例如,int 类型默认为 0,boolean 类型默认为 false,引用类型默认为 null。这些默认值是在准备阶段被设定的。
-
初始化常量: 对于被声明为常量(用 final 修饰)的静态变量,如果其值在编译时已经确定(即为编译时常量),则在准备阶段会将其值设定为编译时的常量值。这样,这些常量在运行时无法改变其值。
需要注意的是,在准备阶段中,实例变量不会被分配内存空间或者设置默认初始值,因为实例变量是在对象实例化时才会进行这些操作。另外,准备阶段并不会执行静态变量的赋值操作,真正的静态变量赋值操作是在初始化阶段进行的。
4. 解析(Resolution)
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。
解析阶段主要包括以下步骤:
- 符号引用转换为直接引用: 解析阶段会将类、字段、方法等符号引用解析为直接引用,以便执行引擎能够准确地定位到对应的数据或代码。
- 解析类、字段和方法: 解析阶段会根据类加载器加载的类信息,将符号引用解析为直接引用,并将解析后的直接引用存储在方法区(或元空间)中,供后续使用。
- 解析的时机: 解析阶段通常在初始化阶段之前进行,以确保类、字段、方法的直接引用已经准备就绪,可以在初始化阶段使用。
综上所述,解析阶段是将符号引用转换为直接引用的重要过程,确保了类的引用能够准确地指向类、字段、方法的实际位置,为程序的正确执行提供了基础支持。
举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。
5. 初始化(Initialization)
初始化阶段是类加载的最后一步,它负责执行类的初始化操作,包括对静态变量的赋值和静态代码块的执行。初始化阶段是类加载过程中的一个重要阶段,确保类在使用之前被正确地初始化。
初始化阶段实际上就是执行类的<clinit>()
方法的过程。这个方法的执行确保了类的静态变量被正确赋值和静态代码块被正确执行,保证了类的正确初始化。在 Java 中,<clinit>()
方法是由编译器自动收集类中所有静态变量的赋值动作和静态代码块中的语句合并产生的,编译器会保证在类的初始化阶段,这个方法被正确调用。
当类被加载到内存中时,如果需要进行初始化,Java 虚拟机会自动调用类的 <clinit>()
方法。这个方法会按照在源代码中出现的顺序执行类中的静态变量赋值和静态代码块的内容。如果一个类中没有静态变量或静态代码块,编译器不会为该类生成 <clinit>()
方法。
在初始化阶段,Java 虚拟机大致会执行如下操作:
- 执行
<clinit>()
方法:在初始化阶段,虚拟机会执行类的 () 方法,这个方法包含了类中所有静态变量赋值和静态代码块的内容。编译器会自动合并类中所有静态变量赋值和静态代码块中的语句生成 () 方法。这个方法确保了类的静态变量被正确初始化,静态代码块被正确执行。 - 初始化父类: 如果当前类有父类,并且父类还没有被初始化过,则会先初始化父类。这个过程是递归进行的,即父类的父类也会依次被初始化,直到最顶层的父类。
- 静态变量赋值和静态代码块的执行顺序: 静态变量赋值和静态代码块的执行顺序按照在类中的声明顺序依次执行。这确保了静态变量和静态代码块的正确初始化顺序。
- 多线程环境下的安全性: 在多线程环境下,虚拟机会保证
<clinit>()
方法的线程安全性,即在多线程环境下只会执行一次,避免了多个线程同时初始化一个类的问题。 - 异常处理: 如果在
<clinit>()
方法的执行过程中发生异常,初始化过程会中断,并且该类将被标记为初始化失败,后续对该类的使用会抛出异常。 - 延迟初始化: 如果一个类没有被主动使用,那么其静态变量和静态代码块可能不会被执行,直到类被主动使用为止。比如静态变量的懒汉单例模式。
在 Java 虚拟机加载 class 文件时,并不一定会立即进行类的初始化。类的初始化是在类被主动使用时才会进行的,而不是在类加载阶段就一定会进行。具体来说,只有当以下情况发生时,类的初始化才会被触发:
- 创建类的实例(使用 new 关键字): 当通过 new 关键字创建类的实例时,会触发类的初始化。
- 访问类的静态变量(使用类名访问): 当通过类名访问类的静态变量时,会触发类的初始化。
- 调用类的静态方法(使用类名调用): 当通过类名调用类的静态方法时,会触发类的初始化。
- 反射调用类的方法: 当通过反射机制调用类的方法时,会触发类的初始化,比如
Class.forName(String)
,newInstance()
。 - 初始化子类: 如果一个类的子类被初始化,那么它的父类也会被初始化。
- JVM启动时指定的初始化类: 如果在启动JVM时指定了需要初始化的类(通过
-Djava.system.class.loader
参数指定),则这些类会在JVM启动时被初始化。 - 含有
main
方法的程序入口类,在加载时一定会被初始化。
有一个特殊点:Class.forName()
这个方法会触发类的加载和初始化,但需要注意的是,Class.forName()
方法有多个重载形式,其中有一个重载方法可以指定是否要进行类的初始化,默认情况下会进行初始化。这个重载方法是:Class.forName(className, false, classLoader)
方法,其中第二个参数传入 false 表示不进行初始化,