Java虚拟机--字节码文件结构

Updated on with 0 views and 0 comments

一、简介

Java各个版本能够保持非常良好的向后兼容性,Class文件结构的稳定性功不可没,不同版本的Java虚拟机规范也只是在原有基础上新增内容、扩充功能。

二、Class类文件结构

这里只列举class文件各项标识得作用,详细信息可以参考官网第 4 章。类 File Format (oracle.com)

2.1 Class文件内容

文件内容如下

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];
}

2.1.1 魔数

每个Class文件的头四个字节被称为魔数,它的作用是确定这个文件是否为一个能被虚拟机接受的Class文件,Java默认的魔数为0xCAFEBABE(咖啡宝贝)

image.png

2.1.2 次版本号

次版本号在jdk1.2-12中都固定为0,到了12以后,重新启用,用于标识“技术预览版”功能特性的支持,如果Class文件中使用了该版本JDK尚未列入正式特性清单中的预览功能,则次版本号标识为65535,以便Java虚拟机在加载类文件时能够区分

2.1.3 主版本号

jdk1.2对应的版本为44,用主版本号-44就能得到当前版本,如版本号为52,对应jdk版本为52-44=8,jdk版本就是8

image.png

2.1.4 常量池

  • constant_pool_count是一个无符号短整型,表示常量池中的条目数量,从1开始计数,值为10,那么常量池实际上包含9个条目,索引范围从1到9
  • constant_pool是一个结构化的数组,它存储了类中所有常量信息的集合

2.1.5 权限

在Java的 .class文件中,access_flags是一个很重要的字段,它位于类文件的Class File Structure中,用于描述类或者接口的访问权限和特征。access_flags是一个16位的无符号整数(u2),其中每一位或几位组合代表了一个特定的访问标志。以下是一些常见的访问标志及其含义:

  1. ACC_PUBLIC (0x0001) :表明类、接口、字段或方法是公共的,可以从任何地方访问。
  2. ACC_PRIVATE (0x0002) :表明字段或方法是私有的,仅在定义它的类中可见。
  3. ACC_PROTECTED (0x0004) :表明字段或方法受保护,可以在同一个包内或子类中访问。
  4. ACC_STATIC (0x0008) :表明字段或方法是静态的,不依赖于类的实例。
  5. ACC_FINAL (0x0010) :表明类、字段或方法是最终的,不可被继承或覆盖。
  6. ACC_SUPER (0x0020) :这个标志主要用于Java 1.0和1.1中,现在已经被废弃,它用于指示默认的接口方法访问规则。
  7. ACC_SYNCHRONIZED (0x0020) :表明方法是同步的,同一时间只允许一个线程执行。
  8. ACC_VOLATILE (0x0040) :用于字段,表明这个字段的值可能会被不同线程异步改变。
  9. ACC_BRIDGE (0x0040) :用于方法,表明这是一个桥接方法,用于实现泛型转换。
  10. ACC_TRANSIENT (0x0080) :用于字段,表明这个字段的值不应该被序列化。
  11. ACC_VARARGS (0x0080) :用于方法,表明这个方法接受可变参数。
  12. ACC_NATIVE (0x0100) :表明方法是一个本地方法,其实现不在Java代码中。
  13. ACC_INTERFACE (0x0200) :表明这是一个接口,不能被实例化,只包含抽象方法。
  14. ACC_ABSTRACT (0x0400) :表明类或方法是抽象的,不包含完整实现。
  15. ACC_STRICT (0x0800) :用于方法,表明这个方法应该严格遵守IEEE 754规范进行浮点运算。
  16. ACC_SYNTHETIC (0x1000) :表明这个类、字段或方法是由编译器自动生成的,不是源代码中直接声明的。
  17. ACC_ANNOTATION (0x2000) :表明这是一个注解类型。
  18. ACC_ENUM (0x4000) :表明这是一个枚举类型或枚举常量。

.class文件中,access_flags的值是通过将这些标志位进行按位或(|)运算得到的。例如,一个公共的、最终的类,其 access_flags值将是 0x00110x0001 | 0x0010)。这些标志在JVM加载类时会被解析,并根据这些标志确定类的访问规则和行为特征。

2.1.6 类

在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"的一个条目(注意在字节码中,.被替换成了 /

2.1.7 父类

在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/ObjectCONSTANT_Class_info条目的索引通常为1。

如果当前类是 java.lang.Object,那么 super_class字段的值将为0,表示没有超类。

2.1.8 接口

interfaces_count 是类文件格式中的一部分,它表示类实现的接口数量。这个值位于类文件的 ClassFile 结构中的 interfaces 字段之前,指示随后的 interfaces 数组中有多少个元素。

interfaces 是一个数组,包含该类实现的所有接口的索引。这些索引指向常量池中的 CONSTANT_Class_info 结构,其中包含了接口的全限定名。每个元素都是一个短整型值,代表了接口在常量池中的位置。

2.1.9 字段

fields_count是一个 u2类型的字段,表示字段表(fields)中的条目数量。它指出了类中有多少个字段定义,包括所有实例变量和静态变量。这个计数不包括从超类或接口继承的字段,只计算在当前类中直接声明的字段。

fields是一个字段表的数组,它紧跟在 fields_count后面。每个条目都描述了一个字段,包含以下信息:

  1. access_flags (u2):字段的访问标志,例如 publicprivateprotectedstaticfinal等。
  2. name_index (u2):字段名称的索引,指向常量池中的一个 CONSTANT_Utf8_info条目,该条目包含字段的名称。
  3. descriptor_index (u2):字段描述符的索引,指向常量池中的一个 CONSTANT_Utf8_info条目,该条目描述了字段的数据类型。
  4. attributes_count (u2):字段属性的数量,表示有多少附加信息与该字段相关联。
  5. attributes :字段的属性表,包含了附加信息,如 ConstantValue属性,它可以存储字段的初始值。

2.1.10 方法

methods_count是一个 u2类型的字段,表示方法表(methods)中方法条目的数量。它指出了类中定义了多少个方法,包括实例方法和静态方法,但不包括从超类或接口继承的方法。

methods是一个方法表的数组,它紧跟在 methods_count字段之后。每个方法条目(method_info)都包含以下信息:

  1. access_flags (u2):方法的访问标志,例如 publicprivateprotectedstaticfinalsynchronizednativeabstract等。
  2. name_index (u2):方法名称的索引,指向常量池中的一个 CONSTANT_Utf8_info条目,该条目包含方法的名称。
  3. descriptor_index (u2):方法描述符的索引,指向常量池中的一个 CONSTANT_Utf8_info条目,该条目描述了方法的参数类型和返回类型。
  4. attributes_count (u2):方法属性的数量,表示有多少附加信息与该方法相关联。
  5. attributes :方法的属性表,包含了附加信息,如 Code属性(包含方法体的字节码)、ExceptionTable属性(描述异常处理)、LineNumberTable属性(源代码行号映射)、LocalVariableTable属性(局部变量表)等。

2.1.11 属性

attributes_count是一个无符号的短整型(u2类型),它指明了紧随其后的 attributes表中有多少个属性。这个计数器对 ClassFile结构、FieldInfo结构、MethodInfo结构以及 CodeAttribute结构都是适用的,也就是说,在类、字段、方法以及代码属性中都可以找到 attributes表及其计数器。

attributes是一个可变长度的数组,每个元素都是一个 AttributeInfo结构。每个 AttributeInfo结构都包含:

  • attribute_name_index:这是一个索引,指向常量池中的 CONSTANT_Utf8_info结构,用来获取属性的名称。
  • attribute_length:表示属性信息体的字节数。
  • 属性信息体:这是一段字节流,其长度由 attribute_length指定,包含了特定于属性的信息。

例如,常见的 attributes包括:

  1. Code:包含方法的字节码指令。
  2. LineNumberTable:包含源代码行号到字节码指令偏移量的映射。
  3. LocalVariableTable:提供局部变量在方法中的作用域信息。
  4. ConstantValue:为字段提供常量值。
  5. Synthetic:表明字段或方法是编译器自动生成的。
  6. Deprecated:表明类、字段或方法已被弃用。
  7. Exceptions:列出方法可能抛出的异常。
  8. InnerClasses:描述内部类的信息。
  9. Signature:提供泛型类型的签名。
  10. RuntimeVisibleAnnotationsRuntimeInvisibleAnnotations:提供运行时可见或不可见的注解信息。

三、字节码指令

这里做一个简介,并列举几个常见的指令,更多命令参考Chapter 6. The Java Virtual Machine Instruction Set (oracle.com)

3.1 简介

Java虚拟机得指令由1个字节长度的、代表某种给特定操作含义的数字以及跟随其后的0至多个代表此操作所需要的参数构成。

3.2 指令集支持的数据类型

opcode、byte、short、int、long、float、double、char、reference

3.3 加载和存储指令

加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,这里的命令中,i代表队int类型的操作,l代表long,f代表float,d代表double,a代表reference

  • 将一个局部变量加载到操作栈iload、lload、fload、dload、aload
  • 将一个数值从操作数栈存储到局部变量表istore、lstore、fstore、dstore、astore

3.4 运算指令

  • 加法:iadd、ladd、fadd、dadd
  • 减法:isub、lsub、fsub、dsub
  • 乘法:imul、lmul、fmul、dmul
  • 除法:idiv、ldiv、fdiv、ddiv

3.5 i++和++i

3.5.1 i++

先编写一个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
  1. iconst_1 将0加入操作数栈
  2. istore_1 将操作数栈栈顶得数据弹出,存储到局部变量表第一槽位(此时操作数栈没有数据)
  3. iload_1 将局部变量表第1个槽位数字加载到操作数栈顶(此时操作数栈和局部变量表都存储i得初始值0)
  4. iinc 1 by 1 将局部变量表第一位的数+1(此时操作数栈数据为0,局部变量表为1)
  5. istore_1 将操作数栈栈顶得数据弹出,存储到局部变量表第一槽位(此时操作数栈数据为0,局部变量表上数据为1,被覆盖为0)

经过上述操作后,i得值仍然是0

3.5.2 ++i

编写代码

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
  • iconst_1 将0加入操作数栈
  • istore_1 将操作数栈栈顶得数据弹出,存储到局部变量表第一槽位(此时操作数栈没有数据)
  • iinc 1 by 1 将局部变量表第一位位置的数+1(此时操作数栈无数据,局部变量表为1)
  • iload_1 将局部变量表第1个槽位数字加载到操作数栈顶(此时操作数栈和局部变量表都存储i得值1)
  • istore_1 将操作数栈栈顶得数据弹出,存储到局部变量表第一槽位(此时操作数栈数据为1,局部变量表上数据为1,局部变量表数据被覆盖为1)

经过上述操作后,i得值变为1

3.5.3 区别

二者的区别其实就是iload和iinc两个指令执行顺序的区别,i++是先执行iload将局部变量表数据加载到操作数栈,然后使用iinc增加局部变量表存储的值,++i是先增加局部变量表的值,再将增加后的值复制到操作数栈

然后两者都从操作数栈出栈并且覆盖局部变量表的值


标题:Java虚拟机--字节码文件结构
作者:wenyl
地址:http://www.wenyoulong.com/articles/2024/07/17/1721230540218.html