Java虚拟机——初探字节码class文件内部结构

之前介绍过Java编译器如何将Java源码编译成字节码class文件。

Java虚拟机——从Java源码到字节码到底经历了什么

那么最终的到的字节码文件是怎样的一个文件,内部结构又是如何?此文对字节码class文件的内部结构进行初步探索,介绍其各个重要组成部分,对之后的Java虚拟机学习做好基础。

下面展示了一个class文件的构成,其中u2、u4等表示类型,分别表示占2、4个字节的数据,属于class文件的基本类型。cp_info表示常量池类型,field表示成员变量类型,method表示类或接口的方法类型,attribute表示属性类型。

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 interface_count;
    u2 interfaces[interface_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];
}

 

1. magic

每个字节码文件开头都包含4个字节的magic number:0xCAFEBABE 用来快速校验是否是Java Class文件。Cafe babe和Java一样,命名的由来都和咖啡有关。

2. minor_version 和 major_version

这4个字节先后包含的是次版本号和主版本号。JVM在校验完魔法数后紧接着就会检查版本号是否在JVM支持的有效范围,如果过高版本的字节码文件在低版本的JVM上是无法执行的。

3. constant_pool_count 和 constant_pool[]

常量池,里面存储了符号引用和字表量,包含了类和接口名、字段名和描述符、方法名和描述符、字符串、final变量值等等,这些信息以列表的形式存储在常量池列表中。constant_pool_count即常量池计数项,用于记录常量池中的常量数量,计数的总和等于1+常量池列表的项数,这里的1代表常量池表项,索引为0。

值得一提的是,常量池是字节码文件内部结构中与外部关联最多的数据项,也是占空间最大的项。但是class文件不保存各个方法字段的内存布局的信息,内存的分配是在运行时完成的。当类加载器将类加载到内存时,常量池中指向类和非final静态字段的符号引用会转换成直接引用指向指定的内存地址。当初始化生成一个对象时,常量池中指向非静态字段的符号引用会转换成直接引用指向指定的内存地址。当执行某个方法时,方法对应的当前栈帧中的动态链接会将常量池中指向方法的符号引用转换为调用方法的直接引用。

4. access_flags

访问标志项用来描述这个文件类型的一些访问标志信息。区分这个文件定义的是类还是接口,类或接口包含了哪些修饰符,是抽象的还是公共的或是final的。

Java类的所有access flags标志
类型作用
ACC_PUBLIC0x0001声明为public,可以从它的包外访问
ACC_FINAL0x0010声明为final,不允许有子类
ACC_SUPER0x0020用invokespecial指令处理超类的调用
ACC_INTERFACE0x0200表明是一个接口
ACC_ABSTRACT0x0400声明为abstract,不能被实例化

5. this_class 和 super_class

this_class的值指向了常量池中类型为CONSTANT_Class_info的一个类的常量,进一步可以查到this_class的全限定名,super_class同理。因为Java只支持单继承,因此父类只有一个,super_class的值为0时说明这个class文件的类直接继承了java.lang.Object,其他情况不允许为0。

6. interface_count 和 interfaces[]

interface_count用来记录interfaces的容量,u2类型的interfaces代表这个类实现的接口以及接口的父接口的常量池索引集合,指向常量池接口常量。如果这个类没有实现任何一个接口interface_count的值为0。

7. fields_count 和 info_fields[]

fields_count用来记录类或接口中声明的变量的个数,info_fields是field类型列表,用来描述类或接口中声明的变量,以上的变量指的都是指成员变量(类变量和实例变量),不包括方法内部的局部变量(方法内部的局部变量在方法在执行的时候存储在当前栈帧中的局部变量表中,也就是我们说的局部变量存储在Java栈区)。

field类型的结构
类型名称说明
u2access_flags声明成员变量时使用的访问标志
u2name_index成员变量的名称索引
u2descriptor_index变成员量的描述符索引
u2attributes_count属性列表中属性的个数
attribute_infoattributes成员变量的属性

成员变量访问标志包含:

ACC_PUBLIC, ACC_PRIVATE, ACC_PROTECTED, ACC_STATIC,

ACC_FINAL, ACC_VOLATILE(并发可见性)

name_index和descriptor_index指向常量池中成员变量的名称和描述符。成员变量的名称并不是全限定名,而是没有类型的简单名称。成员变量的描述符描述其类型,描述符的格式:基本数据类型以及void类型都用一个特定的大写字符表示,而对象类型用字符L加对象的全限定名来表示,而数组类型,N维数组就在前面个N个[,例如:String[][]的描述符就是“[[Ljava/lang/String”。

attributes_count记录成员变量的属性个数,如果没有则为0。在这里会出现的属性有ConstantValue属性,后面会介绍这个属性。

需要说明info_fields中不包含从父类或父接口中继承来的字段信息。

8. methods_count 和 info_methods[]

methods_count用来记录这个类中的所有方法个数,info_methods是method类型数组,用来描述类或接口中声明的方法。method类型的结构类似field。

method类型的结构
类型名称说明
u2access_flags声明方法时使用的访问标志
u2name_index方法的名称索引
u2descriptor_index方法的描述符索引
u2attributes_count属性列表中属性的个数
attribute_infoattributes方法的属性

类或接口中方法的访问标志包含:

ACC_PUBLIC, ACC_PRIVATE, ACC_PROTECTED,

ACC_ABSTRACT,

ACC_STATIC, ACC_FINAL, ACC_SYNCHRONIZED, ACC_NATIVE, ACC_STRICT

name_index和descriptor_index指向常量池中方法名称和描述符。方法的名称并不是全限定名,而是简单名称。方法的描述符描述了方法的参数类型列表和返回值类型,参数类型列表的排序必须严格按照顺序。方法描述符的格式是“(参数1类型,参数2类型...)返回类型”例如,方法java.lang.String toString()的描述符为“()Ljava/lang/String”(L是表示引用类型),方法 void init()方法的描述符为“()V”。这里要说明的是,在Java语法中是不允许存在除返回类型不同其他完全相同的两个方法,这会导致编译不通过,但是Class文件允许这样的情况存在。

讲到这里,方法的访问标志、方法名称以及方法的描述符(参数列表和返回类型)都各归其位,那么最主要的方法体中的代码呢?好了,方法属性隆重登场,方法体中的方法代码经过编译器编译成一条条指令存放在方法的属性类型中有个“Code”属性中。其实在栈帧中,局部变量表和操作数栈所需的容量大小在编译期就可以完全被确定下来,并保存在方法的Code属性中。不要忘记,方法还能抛出异常,而对于可能抛出的异常信息就存储在属性中的Exception属性中。

同feild一个methods列表中不包含父类或父接口中继承来的方法。

9. attributes_count 和 info_attributes[]

上文在field和method中已经出现并提到过attribute属性,这里的info_attributes是在最外层的class文件中,例如,一些内部类和匿名类的信息就存储在对应的属性项中。下面会列举一些Java预定义的属性。先来看attribute的内部结构。

attribute类型结构
类型名称说明
u2attribute_name_index常量池中属性名称的索引
u4attribute_lenght属性数据的长度(以字节计算)
u1info包含的属性数据,长度为attribute_lenght

Java虚拟机规范规定,只要遵循一定规则,任何人都能向class文件中加入属性,这里不做深入探讨。下面列举一些Java预定义的属性。

属性名称说明长度
CodeJava代码编译成的字节码指令可变
ConstantValue只有同时被final和static修饰的成员变量才有ConstantValue属性,且限于基本类型和String固定
Deprecated过时的类、方法或成员变量固定
Exceptions方法可能抛出的异常可变
InnerClasses内部类列表可变
SourceFile源文件名称固定
Synthetic标识类,方法或成员变量是否是编译器自动生成的固定
RuntimeVisibleAnnotations运行时可见注解可变
RuntimeInvisibleAnnotations运行时不可见注解可变
BootstrapMethods用于保存invokedynamic指令引用的引导方法限定符可变
LineNumberTable源码行数与字节码指令对应关系可变
LocalVariableTable方法的局部变量描述可变
StackMapTable检查和处理目标方法的局部变量和操作数栈所需的类型是否匹配可变

 

以上是列举的部分属性,和class文件中的其他项不同的是,属性列表没有严格的顺序要求,属性项的长度也可以是可变的,因此需要数据结构中的attribute_lenght以字节为单位记录属性数据的总长度。下面列举下几个属性的内部结构。

Code属性结构
类型名称说明
u2attribute_name_index常量池中属性名称的索引
u4attribute_length属性数据的长度(以字节计算)
u2max_stack方法的操作数栈的最大长度(以字节计算)
u2max_locals局部变量所需存储的空间长度(以Slot计算)
u4code_length方法的字节码流长度(以字节计算)
u1code方法的字节码指令的一系列字节流
u2exception_table_lengthexception_table的中异常的个数
exception_infoexception_table异常表
u2attributes_countCode的属性个数
attribute_infoattributesCode属性(LineNumberTable, LoaclVaribaleTable和StackMapTable)
ConstantValue属性结构
类型名称说明
u2attribute_name_index常量池中属性名称的索引
u4attribute_length属性数据的长度(固定为2个字节)
u2constantvalue_index常量池中常量值的索引
Exception属性
类型名称说明
u2attribute_name_index常量池中属性名称的索引
u4attribute_length属性数据的长度(以字节计算)
u2number_of_exceptionsthrows关键字后面列举的异常数量
u2exception_index_table常量池中throws关键字后面列举的异常的索引

总结

到这里字节码class文件大致的内部结构就介绍完了。

Java不像C或者C++那样编译完成后就保存了类、方法和变量的内存布局信息,字节码文件只有符号引用,而没有直接指向内存空间的引用,是属于静态文件,因此才会有之后的Java虚拟机加载字节码文件进行动态连接,通过解析翻译将符号引用转换成直接引用,这也是Java的魅力所在。

对于Java虚拟机的初学者来说,刚接触这些内容可能感觉会比较枯燥,没有太多感知,容易忘记。但是随着之后的学习,当了解了类加载的过程,方法的执行和栈帧的结构,堆内存以及对象的初始化等一系列原理后,再回过头看这些内容,我相信肯定会有和第一次学习时有不一样的感受。了解字节码文件内部结构是接下去学习JVM的基石,当掌握JVM这套编译执行流程后,字节码文件的结构也自然会熟记在心。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java虚拟机可以读取字节码文件并将其转换成可执行的代码。字节码文件Java源代码编译后生成的二进制文件,它包含了一系列指令,这些指令被Java虚拟机解释和执行。通过这种方式,Java程序可以在不同的硬件平台和操作系统上运行,实现了"Write Once, Run Anywhere"的目标。 Java虚拟机读取字节码文件的过程可以简单概括为以下几个步骤: 1. 加载:Java虚拟机通过类加载器加载字节码文件,将其转换为运行时的类对象。类加载器负责查找并加载类文件,并将其转换为内存的类对象。 2. 验证:在加载字节码文件后,Java虚拟机会对字节码文件进行验证,确保其符合Java语言规范和虚拟机规范。验证过程包括对字节码文件结构、语义和安性进行检查。 3. 准备:在验证通过后,Java虚拟机会为类变量(静态变量)分配内存,并设置默认初始值。此时,还没有执行任何Java代码。 4. 解析:在准备阶段之后,Java虚拟机会对字节码文件的符号引用进行解析,将其转换为直接引用。这个过程将类或接口的符号引用解析为实际的内存地址。 5. 初始化:在准备阶段之后,Java虚拟机会执行类的初始化操作,包括执行静态初始化块和静态变量的赋值操作。在这个阶段,Java程序的主方法会被调用,程序开始执行。 通过以上步骤,Java虚拟机可以读取字节码文件并执行其的指令,实现Java程序的运行。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Java 进阶:实例详解 Java 虚拟机字节码指令](https://blog.csdn.net/m0_54853420/article/details/126104672)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值