Java之所以能实现“Write Once, Run Anywhere”,是因为不同平台的虚拟机都统一使用一种程序存储格式——字节码。Java虚拟机不和包括Java在内的任何语言绑定,它只于“Class”文件这种特定的二进制文件格式所关联。
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在Class文件中,中间无任何分隔符。
明确两个概念:无符号数和表
无符号数属于基本的数据类型,以u1、u2、u4来分别代表1个字节、2个字节和4个字节的无符号数。
表是由多个无符号数或者其他表作为数据项构成的复合数据结构,整个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文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
下文只介绍了其中一些属性:
类型 | 名称 | 数量 | 描述 |
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行处处理。
类型 | 名称 | 数量 |
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源码行号。
后面是另一个方法的字节码,就不再赘述。