Class文件结构

Java之所以能实现“Write Once, Run Anywhere”,是因为不同平台的虚拟机都统一使用一种程序存储格式——字节码。Java虚拟机不和包括Java在内的任何语言绑定,它只于“Class”文件这种特定的二进制文件格式所关联。

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在Class文件中,中间无任何分隔符。

明确两个概念:无符号数和表

无符号数属于基本的数据类型,以u1、u2、u4来分别代表1个字节、2个字节和4个字节的无符号数。

表是由多个无符号数或者其他表作为数据项构成的复合数据结构,整个Class文件本质上就是一张表。

Class文件格式
类型 名称 数量 描述
u4 magic 1 魔数
u2 minor_version 1 次版本号
u2 major_version  1 主版本号
u2 constant_pool_count 1 常量池容量
cp_info constant_pool costant_pool_count-1 常量池
u2 access_flags 1 访问标志
u2 this_class 1 当前类常量索引
u2 super_class 1 超类常量索引
u2 interfaces_count 1 接口数量
u2 interfaces interfaces_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 属性信息

解释Class文件格式之前,先编写一个简单的java类

1 package com.yyl.Test;
2 public class Test{
3     private int i = 2;
4     
5     public int getResult(){
6         return i + 2;
7     }
8 }

编译成class文件后,用winhex软件打开class字节码

使用javap命令帮助分析

1、魔数(magic)

每个Class文件头4个字节称为魔数,它的唯一作用是确定这个文件能否为一个能为虚拟机接收的Class文件,基于安全考虑,使用魔数而不是扩展名来进行身份识别。从16进制字节码中看出前4个字节为CAFEBABE(咖啡宝贝?)。

2、次/主版本号(minor_version/major_version)

紧接着魔数的4个字节分别是次版本号和主版本号,java的版本号是从45开始,高版本的JDK能向下兼容以前版本的Class文件,虚拟机拒绝执行超过其版本号的Class文件。从16进制字节码中看出次版本号0x0000,主版本号0x0032。

3、常量池容量、常量池(constant_pool_count、constant_pool)

常量池主要存放两大类常量:

a.字面值:接近java语言层面的常量概念,如文本字符串、final常量值等。

b.符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。

由于常量池中常量的数量是不固定的,所以在常量池入口需要设置一项u2类型数据,代表常量池容量计数值。其计数不是从0开始,而是从1开始。

如图,常量池容量值为0x0013,即十进制19,因此容量为19-1=18个。查看javap命令输出的常量表中也可以看出Constant pool总共有18个常量。

设计者把第0项空出来目的在于在特定情况下需要表达“不引用任何一个常量池项目”的含义。

常量池项目结构第一项均为u1类型的tag,该标志代表常量池项目的类型,而其他结构各异。

下面只列出部分结构:

常量池中常量项结构
常量 项目 类型 描述
CONSTANT_Utf8_info tag u1 值为1
length u2 占用字节数
bytes u1 长度为length的UTF-8编码的字符串
CONSTANT_Integer_info tag u1 值为3
bytes u4 按照高位在前存储的int值
CONSTANT_Class_info teg u1 值为7
index u2 指向全限定名常量项的索引
CONSTANT_String_info  tag u1 值为8
index u2 指向字符串字面值的索引
CONSTANT_Fieldref_info tag u1 值为9
index u2 指向声明字段的类或接口描述符CONSTANT_Class_info的索引项
index u2 指向字段描述符CONSTANT_NameAndType的索引项
CONSTANT_Methodref_info tag u1 值为10
index u2 指向声明方法的类描述符CONSTANT_Class_info的索引项
index u2 指向方法描述符CONSTANT_NameAndType_info的索引项
CONSTANT_NameAndType_info tag u1 值为12
index u2 指向该字段或方法名称常量项的索引
index u2 指向该字段或方法描述符常量项的索引

接着分析字节码:

如图可以看出第一个常量项tag为0x0A,即十进制10,类型为CONSTANT_Methodref_info,接着的u2类型0x0004指向类的索引,即指向java/lang/Object类,后面的0x000F指向方法描述符,即指向方法名为<init>返回值为()V的描述符。所以整个常量项就表示如图中的“结果”。

其他常量项类似上面方法,就不一一阐述。

另外,由于Class文件中方法、字段等都需要引用CONSTAN_Utf8_info型常量来描述名称,所以该类型最大长度也就是java中方法、字段名的最大长度(u2类型表达的最大值为65535),所以如果定义了超过64KB英文字符的变量或方法名,将无法编译。

4、访问标志(access_flags)

常量池结束后,紧接着的两个字节表示访问标志,用于识别类或接口层次的访问信息,包括这个Class是类还是接口,是否定义为public类型、abstract类型等等。

访问标志
标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为public类型
ACC_FINAL 0x0010 是否被声明为final,只有类可设置
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语义,JDK1.0.2之后编译出来的类这个标志必须为真
ACC_INTERFACE 0x0200 标识这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口或抽象类来说此值为真,其他类值为假
ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码产生的
ACC_ANNOTATION 0x2000 标识这个一个注解
ACC_ENUM 0x4000 标识这是一个枚举

Test类被public关键字修饰,因此它的ACC_PUBLIC、ACC_SUPER标志应当为真,因此其access_flags值应为0x0001|0x0020=0x0021。

5、类索引、父类索引与接口索引集合(this_class、super_class、interfaces)

类索引和父类索引都是一个u2类型的数据,接口索引集合是一组u2类型的数据的集合。它们各自指向一个类型为CONSTANT_Class_info的类描述符常量。

 

6、字段表集合(field_info)

字段表用于描述接口或类中声明的变量,字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。

字段表结构
类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

字段修饰符放在access_flags项目中,如下表

字段访问标志
标志名称 标志值 含义
ACC_PUBLIC 0x0001 字段是否为public类型
ACC_PRIVATE 0x0002 字段是否为private
ACC_PROTECTED 0x0004 字段是否为protected
ACC_STATIC 0x0008 字段是否为static
ACC_FINAL 0x0010 字段是否为final
ACC_VOLATILE 0x0040 字段是否为volatile
ACC_TRANSIENT 0x0080 字段是否transient
ACC_SYNTHETIC 0x1000 字段是否由编译器自动产生
ACC_ENUM 0x4000 字段是否为enum

跟随access_flags的标志是两项索引值:name_index和descriptor_index,他们都是对常量池的引用,分别代表字段的简单名称以及字段和方法的描述符。

区分三个概念:全限定名、简单名称、描述符

a.全限定名,如java/lang/Object,仅仅将类全名中的“.”替换成“/”。

b.简单名称是指没有类型和参数修饰的方法或者字段名称,如类中getResult()方法和i字段的简单名称分别为“getResult”和“i”。

c.描述符是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。

描述符标识字符含义
标识字符 含义 标识字符 含义
B 基本类型byte J 基本类型long
C 基本类型char S 基本类型short
D 基本类型double Z 基本类型boolean
F 基本类型float V 特殊类型void
I 基本类型int             L 对象类型,如Ljava/lang/Object

对于数组类型,每一唯独使用一个前置的“[”字符描述,如一个定义为“java.lang.String[][]”类型的二维数组,将被记录为“[[Ljava/lang/String”。

当描述符描述方法时,按照先参数列表后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如int getResult()方法的描述符为“()I”。

字段表最后的属性表结构可用于存储一些额外的信息。

7、方法表集合(method_info)

方法表的结构跟字段表结构一样,依次包括access_flags、name_index、descriptor_index、attributes,而访问标志则有所区别。

方法访问标志
标志名称 标志值 含义
ACC_PUBLIC 0x0001 方法是否为public类型
ACC_PRIVATE 0x0002 方法是否为private
ACC_PROTECTED 0x0004 方法是否为protected
ACC_STATIC 0x0008 方法是否为static
ACC_FINAL 0x0010 方法是否为final
ACC_SYNCHRONIZED 0x0020 方法是否为synchronized
ACC_BRIDGE 0x0040 方法是否由编译器产生的桥接方法
ACC_VARARGS 0x0080 方法是否接收不定参数
ACC_NATIVE 0x0100 方法是否为native
ACC_ABSTRACT 0x0400 方法是否为abstract
ACC_STRICTFP 0x0800 方法是否为strictfp
ACC_SYNTHETIC 0x1000 字段是否由编译器自动产生

方法定义可以由访问标志、名称索引、描述符表达清楚,而方法里面的代码经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里面。

8、属性表集合(attribute_info)

在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。

下文只介绍了其中一些属性:

Code属性表结构
类型 名称 数量 描述
u2 attribute_name_index 1 常量值固定为Code,代表该属性名称
u4 attribute_length 1 属性值长度
u2 max_stack 1 操作数栈深度的最大值
u2 max_locals 1 局部变量表所需的存储空间
u4 code_length 1 字节码长度
u1 code code_length 存储字节码指令的一系列字节流
u2 exception_table_length 1 异常处理表长度
exception_info exception_table exception_table_length 异常属性表
u2 attributes_count 1 属性集合中属性个数
attribute_info attributes attributes_count 属性信息

其中max_locals代表局部变量的存储空间,单位为Slot(虚拟机为局部变量分配内存所使用的最小单位)。对于byte、char、float、int、short等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long这两种64位的数据类型则需要两个Slot来存放。方法参数,包括实例方法中的隐藏参数“this”、显式异常处理器的参数、方法体中定义的局部变量都需要使用局部变量表来存放。max_locals并不是简单将所有局部变量所占Slot之和作为其值,java编译器会根据变量的作用域来分配Slot给各个变量使用,然后计算max_locals的大小。

字节码中每个u1类型的单字节代表一个指令。意义请自行查找虚拟机字节码指令表。

异常表结构
类型 名称 数量 类型 名称 数量
u2 start_pc 1 u2 handler_pc 1
u2 end_pc 1 u2 catch_type 1

这些字段的含义为:如果当字节码在第start_pc行(相对于方法体开始的偏移量)到第end_pc行(不包括)之间出现类型为catch_type或其子类的异常,则转到handler_pc行继续处理。当catch_type为0时,代表任意异常情况都需要转向handler_pc行处处理。

LineNumberTable属性结构
类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 line_number_table_length 1
line_number_info line_number_table line_number_table_length

该属性用于描述java源码行号与字节码行号(偏移量)之间的对应关系,line_number_table是一个数量为line_number_table_length、类型为line_number_info的集合,line_number_info表包括start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是java源码行号。

后面是另一个方法的字节码,就不再赘述。

原文地址:https://www.cnblogs.com/honghuzidelaoren/p/3622021.html