还不懂类文件结构?看看这篇文章

值得强调的是,Java虚拟机并不是Java语言所独自占有的,虚拟机不关心来源是何种语言。只要输入规范的统一的程序存储格式——字节码文件,就能产生需要的效果

Class类文件结构

首先把干货罗列出来

Class类文件是一种“高效率”的结构

Class文件是一组以8位字节为基础单位的二进制流,数据项目严格按照顺序紧密排列,Class文件中几乎所有内容都是程序运行必要的数据,没有空隙

Class文件按照顺序,大致分为

  • 魔数(4字节)
  • 版本号(5,6字节)
  • 常量池-访问标志(access_flags)
  • 类索引、父索引、接口索引集合
  • 字段表集合
  • 方法表集合
  • 属性表集合

Class文件采用类似C语言结构体的伪结构只存储无符号数据,或无符号数据组成的集合——

  • 无符号数可以描述数字引用数量值按照UTF-8编码构成的字符串值,u1,u2,u3,u8分别代表1,2,4,8个字节的无符号数
  • 多个无符号数或者其他表构成的一种复合数据类型,以“_info”结尾,Class文件为一张表
类型 名称 中文名称 数量
u4 magic 魔数 1
u2 minor_version 次版本号 1
u2 major_version 主版本号 1
u2 constant_pool_count 常量池容量计数值 1
cp_info constant_pool 常量池 constant_pool_count-1
u2 access_flags 访问标志 1
u2 this_class 类索引 1
u2 super_class 父类索引 1
u2 interfaces_count 接口数量 1
u2 interfaces 接口 interface_count
u2 fields_count 字段数量 1
field_info fields 字段 fields_count
u2 methods_count 方法数量 1
method_info methods 方法 methods_count
u2 attributes_count 属性数量 1
attribute_info attributes 属性 attributes_count

到这里,很容易迷茫,这里我作出一张更为详细的表格方便理解

在阅读这个表格前,希望各位回忆一下Java对象的基本组成

public class MyClass extends Father implements Boy,Girl{
    
    
    private String name;
    public int age;
    public void speak(){
    
    }
}

Class文件是高效率存储程序信息的文件,一字不多、一字不少的存储了一个类

从下面的表中,你可以清晰的看到类的各个部分。

请先对该部分有一个印象,再继续阅读。

类文件结构

Class类文件的结构就是这样,下面详细的说说各个部分。

个人感觉后面的内容比较枯燥,使用生动的例子反而不是很清晰,若只是希望有一个了解,阅读至此,已经足以,若要对各个项目详细窥探,ways are yours

1、魔数与Class文件版本

打开你的Class文件,会发现开头为cafe,这就是魔数,0xCAFEBABE,这是Class文件的标识符

紧跟魔数的4个字节,为Class文件的版本号

JDK不能执行高于本版本的字节码文件,即使文件未变化,虚拟机也必须拒绝执行超过其版本号的Class文件。

2、常量池

位置:主次版本号之后。

Class文件中的资源仓库,在Class文件中与其他项目关联最多(其他项目需要常量才能正确表达),占用了Class文件空间最大的数据项目之一

  • 表类型数据项目
  • 入口为一个计数值,从1开始而不是从0开始
  • 当某个项目“不引用任何一个常量池项目”时,指向0

主要存放

  • 字面量:接近常量,文本字符串、声明为final的常量值等等。
  • 符号引用:各种
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符

Java在编译时没有“连接”的步骤,Java通过在虚拟机加载Class文件时进行动态连接。Class文件中不会保存各个方法、字段的内存布局信息,不经过运行期转换的话无法得到真正的内存入口地址,无法被虚拟机使用。笔者认为这是Java的方法脱离了类,就不能使用的来源。

常量池中的每一项都是表类型(复合类型)

其中的项目开头第一位是一个u1类型(4位无符号数)的标志位(tag,取值如下)代表当前的常量类型。(JDK1.7前为11种,1.7后增加了三种)

类型 标志 描述
CONSTANT_Utf8_info 1 UTF-8编码的字符串
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 标识一个动态方法调用点

其中CONSTANT_Utf8_info类型

类型 名称 数量
u1 tag 1
u2 length 1
u1 bytes length

由于Class文件中方法、字段等都引用CONSTANT_Utf8_info型常量来描述,所以CONSTANT_Utf8_info的最大长度就是Java中方法、字段名的最大长度。即length的最大值,u2类型的最大值65535。所以超过64KB英文字符的变量或方法名,将无法编译。

3、访问标志

位置:常量池后紧接的两个字节

用于识别一些类或者接口层次的访问信息。

  • 该Class类还是接口
  • 是否定义为public
  • 是否定义为abstract类型
  • 是类的情况下,是否被定义为final

可以使用16个标志位,目前使用了8个

4、类索引、父类索引与接口索引集合

类索引和父类索引为u2类型,接口为一组u2类型数据的集合

这三项数据确定了该类的继承关系。

Object类的父类索引为0.

接口集合包括一个接口计数器,后接接口值的索引(真实值存储在常量池)

5、字段表集合

用于描述声明的变量。

字段表的结构为访问标志、名称的索引、参数信息构成的集合,不在详细的描述

值得一提的是,字段在类文件中,以简单名称进行储存(与全限定名相对,另外全限定名在存储时,点替换为斜杠)

虚拟机提供了描述标识符类型的标识字符

若字段是一个数组类型,在每一个维度前将放置一个“[”进行标识

如java.lang.Stringp[][]将记录为“[[Ljava/lang/String;”

另外,Java中,字段的名字不能重复,而对于字节码,若名字相同,类型描述符不同,则是合法的

6、方法表集合

方法表与字段表极为类似,

不过需要注意的是:方法表中只记录了方法的定义信息,而方法体存放在属性表中一个名为“Code”的属性中。

在Java中,重载(Overload,就是名字相同的那种)一个方法,除了相同的简单名称,还要求必须拥有一个与原方法不同的特征签名(Java中为方法名称、参数顺序、参数类型)。

因为在字节码文件中,特征签名多了方法的返回值以及受查异常表,所以在字节码中,重载的范围更宽了一些。

7、属性表集合

属性表的限制稍微宽松,不再严格要求各个项目的顺序,并且只要不与已知属性名重复,都可以写入自己定义的属性信息,Java虚拟机运行时会忽略不认识的属性。

下面给出预定义的一些属性表

在这里插入图片描述

在这里插入图片描述

其中,我们大致提出一些主要的属性

Code属性

Java程序的方法体经过Javac编译器处理后,最终转换为字节码存储在Code属性中,Code属性出现在方法表中的属性集合之中,但并非所有的方法表都必须有这个属性,比如接口和抽象类中的方法,就不存在Code属性,如果方法表有Code属性存在,那么他的结构如下
img
其中,

  • 第一个u2 attriibute_name_index,指向了常量池中的一个utf8类型常量的索引,此常量固定值为Code
  • attribute_length表示了属性值的长度。
  • max_stack代表了操作数栈深度的最大值,在方法执行的任意时刻,操作数栈都不会超过这个深度,虚拟机在运行时根据这个值来分配栈帧中的操作树栈深度。
  • max_locals代表了局部变量表所需的存储空间。
    • 在这里,max_locals的单位是槽(slot),变量槽是虚拟机为局部变量分配内存的最小单位,对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽,而double和long这种64位的数据类型则需要两个变量槽
    • 方法参数(包括实例方法中隐含的this)、显示异常处理程序的参数(在catch中定义的异常值)、方法体中定义的局部变量都依赖局部变量表来存放。
    • 但是要注意,并不是方法中有多少个局部变量,就把这些局部变量表所占空间之和作为max_locals的值,操作数栈和局部变量表决定了一个方法的栈帧大小,不必要的数量会导致内存的浪费。
    • Java虚拟机的做法是将局部变量表的局部变量池进行重用,当代码执行超过一个局部变量的作用域时,这个局部变量所占用的局部变量槽可以被其他局部变量使用,javac编译器会根据变量的作用域来分配变量槽来给各个变量使用,然后根据同时生存的最大局部变量数和类型来计算出max_locals的大小。
  • code_length和code用来存储Java源程序编译后产生的字节码指令,code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。虽然code_length是一个u4类型的长度值,也就是理论上可以达到2的32次幂,但是根据java虚拟机规范,一个方法不允许超过65535条字节码指令,即实际值使用到了u2的长度。

在字节码指令之后的是显式异常处理表,异常表对于Code属性来说并不是必须存在的。其结构如图
img
字段的含义为,当字节码从start_pc行到end_pc行之间出现了类型为catch_type或者其子类的异常时,则转到第handler_pc行继续处理。 当catch_type的值为0时,代表任意异常情况都需要转到handler_pc行进行处理。

Exceptions属性

这个Exceptions属性是方法表中与Code属性同级的一项属性。作用是列举一个方法可能抛出的受检查异常(Checked Exceptions),也就是方法描述时,throws关键字后面列举的异常,其结构如下图
img
其中exception_index_table指向了常量池中CONSTANT_Class_info的索引。

LineNumberTable属性

LineNumberTable属性是用于描述Java源代码行号和字节码行号之间关系的。他不是运行时必须的,可以通过javac的参数 -g参数进行取消或生成。 如果取消这个属性,最大的影响就是在运行时抛出的异常不会包含报错行号,在调试程序时也无法根据源码行号来设置断点。

LocalVariableTable和LocalVariableTypeTable属性

LocalVariableTable属性是用来描述栈帧中局部变量表的变量与java源码中定义的变量之间的关系,也不是运行时必须的属性,如果不生成,最大的影响是对IDE工具调试时无法根据参数名从上下文中获取参数值。

在JDK5引入泛型后,LocalVariableTable属性增加了一个姐妹属性,LocalVariableTypeTable,其结构与LocalVariableTable很相似,仅仅是把描述字段的字段描述符替换成了字段的特征签名。由于描述符中泛型参数化类型被擦除,描述符不能准确描述泛型类型了,因此出现了LocalVariableTypeTable属性,使用特征签名来完成泛型的描述

猜你喜欢

转载自blog.csdn.net/weixin_44494373/article/details/107988854