Java各个版本能够保持非常良好的向后兼容性,Class文件结构的稳定性功不可没,不同版本的Java虚拟机规范也只是在原有基础上新增内容、扩充功能。
这里只列举class文件各项标识得作用,详细信息可以参考官网第 4 章。类 File Format (oracle.com)
文件内容如下
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
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];
}
每个Class文件的头四个字节被称为魔数,它的作用是确定这个文件是否为一个能被虚拟机接受的Class文件,Java默认的魔数为0xCAFEBABE(咖啡宝贝)
次版本号在jdk1.2-12中都固定为0,到了12以后,重新启用,用于标识“技术预览版”功能特性的支持,如果Class文件中使用了该版本JDK尚未列入正式特性清单中的预览功能,则次版本号标识为65535,以便Java虚拟机在加载类文件时能够区分
jdk1.2对应的版本为44,用主版本号-44就能得到当前版本,如版本号为52,对应jdk版本为52-44=8,jdk版本就是8
constant_pool_count
是一个无符号短整型,表示常量池中的条目数量,从1开始计数,值为10,那么常量池实际上包含9个条目,索引范围从1到9constant_pool
是一个结构化的数组,它存储了类中所有常量信息的集合在Java的 .class
文件中,access_flags
是一个很重要的字段,它位于类文件的Class File Structure中,用于描述类或者接口的访问权限和特征。access_flags
是一个16位的无符号整数(u2
),其中每一位或几位组合代表了一个特定的访问标志。以下是一些常见的访问标志及其含义:
在 .class
文件中,access_flags
的值是通过将这些标志位进行按位或(|
)运算得到的。例如,一个公共的、最终的类,其 access_flags
值将是 0x0011
(0x0001 | 0x0010
)。这些标志在JVM加载类时会被解析,并根据这些标志确定类的访问规则和行为特征。
在Java字节码文件(.class文件)中,this_class
是Class File Structure的一部分,它是一个 u2
类型的字段,用于存储当前类的常量池索引。这个索引指向的是常量池中的一个 CONSTANT_Class_info
结构,该结构包含了当前类的全限定名(fully-qualified name)。
this_class
字段非常重要,因为它是JVM在加载类时识别类的关键信息之一。当JVM需要创建一个新的类实例或调用类的方法时,它会使用 this_class
字段的信息来定位正确的类。
this_class
的值并不是直接存储类名字符串,而是存储了一个指向常量池中相应条目的索引。常量池中可能有多个 CONSTANT_Class_info
条目,每个条目代表一个类或接口的名称。this_class
字段指向的就是代表当前类的那个条目。
例如,如果你有一个名为 com.example.MyClass
的类,那么 this_class
字段将指向常量池中存储 "com/example/MyClass"
的一个条目(注意在字节码中,.
被替换成了 /
)
在Java字节码文件(.class文件)中,super_class
字段是一个 u2
类型的字段,用于指定当前类的直接超类的索引。这个索引指向常量池中的一个 CONSTANT_Class_info
条目,该条目包含了超类的全限定名。
super_class
字段是Class File Structure的一部分,它紧随 this_class
字段之后。this_class
字段指出了当前类的标识,而 super_class
字段则指出了该类继承的直接父类的标识。
值得注意的是,super_class
字段并不包含 java.lang.Object
类的任何信息,因为 java.lang.Object
是所有Java类的根父类,没有直接的超类。因此,如果一个类没有显式声明继承任何其他类(即它直接继承自 java.lang.Object
),那么 super_class
字段将包含一个值为1的索引,这是因为在常量池中 java/lang/Object
的 CONSTANT_Class_info
条目的索引通常为1。
如果当前类是 java.lang.Object
,那么 super_class
字段的值将为0,表示没有超类。
interfaces_count
是类文件格式中的一部分,它表示类实现的接口数量。这个值位于类文件的 ClassFile
结构中的 interfaces
字段之前,指示随后的 interfaces
数组中有多少个元素。
interfaces
是一个数组,包含该类实现的所有接口的索引。这些索引指向常量池中的 CONSTANT_Class_info
结构,其中包含了接口的全限定名。每个元素都是一个短整型值,代表了接口在常量池中的位置。
fields_count
是一个 u2
类型的字段,表示字段表(fields
)中的条目数量。它指出了类中有多少个字段定义,包括所有实例变量和静态变量。这个计数不包括从超类或接口继承的字段,只计算在当前类中直接声明的字段。
fields
是一个字段表的数组,它紧跟在 fields_count
后面。每个条目都描述了一个字段,包含以下信息:
u2
):字段的访问标志,例如 public
、private
、protected
、static
、final
等。u2
):字段名称的索引,指向常量池中的一个 CONSTANT_Utf8_info
条目,该条目包含字段的名称。u2
):字段描述符的索引,指向常量池中的一个 CONSTANT_Utf8_info
条目,该条目描述了字段的数据类型。u2
):字段属性的数量,表示有多少附加信息与该字段相关联。ConstantValue
属性,它可以存储字段的初始值。methods_count
是一个 u2
类型的字段,表示方法表(methods
)中方法条目的数量。它指出了类中定义了多少个方法,包括实例方法和静态方法,但不包括从超类或接口继承的方法。
methods
是一个方法表的数组,它紧跟在 methods_count
字段之后。每个方法条目(method_info
)都包含以下信息:
u2
):方法的访问标志,例如 public
、private
、protected
、static
、final
、synchronized
、native
、abstract
等。u2
):方法名称的索引,指向常量池中的一个 CONSTANT_Utf8_info
条目,该条目包含方法的名称。u2
):方法描述符的索引,指向常量池中的一个 CONSTANT_Utf8_info
条目,该条目描述了方法的参数类型和返回类型。u2
):方法属性的数量,表示有多少附加信息与该方法相关联。Code
属性(包含方法体的字节码)、ExceptionTable
属性(描述异常处理)、LineNumberTable
属性(源代码行号映射)、LocalVariableTable
属性(局部变量表)等。attributes_count
是一个无符号的短整型(u2
类型),它指明了紧随其后的 attributes
表中有多少个属性。这个计数器对 ClassFile
结构、FieldInfo
结构、MethodInfo
结构以及 CodeAttribute
结构都是适用的,也就是说,在类、字段、方法以及代码属性中都可以找到 attributes
表及其计数器。
attributes
是一个可变长度的数组,每个元素都是一个 AttributeInfo
结构。每个 AttributeInfo
结构都包含:
attribute_name_index
:这是一个索引,指向常量池中的 CONSTANT_Utf8_info
结构,用来获取属性的名称。attribute_length
:表示属性信息体的字节数。attribute_length
指定,包含了特定于属性的信息。例如,常见的 attributes
包括:
Code
:包含方法的字节码指令。LineNumberTable
:包含源代码行号到字节码指令偏移量的映射。LocalVariableTable
:提供局部变量在方法中的作用域信息。ConstantValue
:为字段提供常量值。Synthetic
:表明字段或方法是编译器自动生成的。Deprecated
:表明类、字段或方法已被弃用。Exceptions
:列出方法可能抛出的异常。InnerClasses
:描述内部类的信息。Signature
:提供泛型类型的签名。RuntimeVisibleAnnotations
和 RuntimeInvisibleAnnotations
:提供运行时可见或不可见的注解信息。这里做一个简介,并列举几个常见的指令,更多命令参考Chapter 6. The Java Virtual Machine Instruction Set (oracle.com)
Java虚拟机得指令由1个字节长度的、代表某种给特定操作含义的数字以及跟随其后的0至多个代表此操作所需要的参数构成。
opcode、byte、short、int、long、float、double、char、reference
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,这里的命令中,i代表队int类型的操作,l代表long,f代表float,d代表double,a代表reference
先编写一个i++得方法
public class HelloWorld {
public void add(){
int i=0;
i=i++;
}
}
字节码文件得方法如下
0 iconst_0
1 istore_1
2 iload_1
3 iinc 1 by 1
6 istore_1
7 return
经过上述操作后,i得值仍然是0
编写代码
public class HelloWorld {
public void add(){
int i=0;
i=++i;
}
}
字节码方法如下
0 iconst_0
1 istore_1
2 iinc 1 by 1
5 iload_1
6 istore_1
7 return
经过上述操作后,i得值变为1
二者的区别其实就是iload和iinc两个指令执行顺序的区别,i++是先执行iload将局部变量表数据加载到操作数栈,然后使用iinc增加局部变量表存储的值,++i是先增加局部变量表的值,再将增加后的值复制到操作数栈
然后两者都从操作数栈出栈并且覆盖局部变量表的值