什么是.class
Java源文件被编译后被Java虚拟机所执行的代码使用了一种平台中立(不依赖于特定硬件及操作系统)的二进制格式来表示,并且经常(但并非绝对)以文件的形式存储,因此这种格式成为class文件格式。class文件格式中精确地定义了类与接口的表示形式。
每个class文件都由字节流组成,每个字节含有8个二进制位,所有16位,32位和64位长度的数据将通过构造2个、4个和8个连续的8位字节来表示。在Java SDK中,可以使用java.io.DataInput、java.io.DataOutput等接口和java.io.DataInputStream和java.io.DataOutputStream等类来访问这种格式的数据。
ClassFile结构
每个class文件对应一个如下所示的ClassFile结构:
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];
}
JVM规范专门定义了一组专用的数据类型来表示class文件的内容,它们包括u1、u2和u4,分别代表1、2和4个字节的无符号数。对于其他类型如cp_info、field_info等都是复合结构,介绍到再详细讲。
准备
在本次分析字节码的过程中以下代码将一直陪伴我们,我们将分析它的class文件:
package bytecode;
public class Test1 {
private int a = 1;
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
}
class文件是16进制的数据,需要使用WinHex打开,先下载安装吧!打开class文件如下所示(看不懂没关系,有规则的):
为了判断我们的分析结果我们可以使用javap -verbose <filename>
命令让Java告诉我们class文件的详细信息。
魔数
所有的.class字节码文件的前4个字节都是魔数,魔数值为固定值:0xCAFEBABE。它的作用是确定这个文件是否为一个能被虚拟机所接受的class文件。
版本号
继魔数之后的4个字节是版本信息,其中前2个字节为minor_version(副版本号),后2个字节为(主版本号)。如这次使用的Test1.class文件中的版本信息为00 00 00 34
,转换为十进制则副版本号为0,主版本号为52。
常量池计数器
紧接着版本号的2个字节为constant_pool_count(常量池计数器)
,此数值表示常量池中成员的数目。如这次使用的Test1.class文件中的版本信息为00 18
,转换为十进制为24,说明常量池数目为24个。但是,我们查看javap
给我们的信息可以发现实际上常量池数目为23。
注意:常量池数组中元素的个数 = 常量池数 - 1(其中0暂时不使用),目的是满足某些常量池索引值的数据在特定情况下需要表达【不引用任何一个常量池】的含义;根本原因在于,索引为0也是一个常量(保留常量),只不过它不位于常量表中,这个常量就对应null值,所以常量池的索引从1而非0开始。
常量池
一个Java类中定义的很多信息都是由常量池来维护和描述的,可以将常量池看作是Class文件的资源仓库,比如说Java类中定义的方法与变量信息,都是存储在常量池中。常量池中主要存储两类常量:字面量与符号引用。字面量如文本字符串,Java中声明为final的常量值等,而符号引用如类和接口的全限定名,字段的名称和描述符,方法的名称和描述符等。
常量池中的每一项都具备相同的特征——第1个字节作为类型标记,用于确定该项的格式,这个字节成为tag byte(标记字节、标签字节)
。
常量池表中的所有项都具有如下通用格式。
cp_info {
u1 tag;
u1 info[];
}
在常量池表中,每个cp_info项都必须表示cp_info类型的单字节"tag"项开头。后面info[]数组的内容由tag的值所决定。有效的tag和对应的值如下表所示。每个tag字节之后必须有两个或更多的字节,这些字节用于指定这个常量的信息,附加信息的格式由tag的值来决定。
名称 | 项目 | 类型 | 描述 |
CONSTANT_Utf8_info | |||
tag | u1 | 值为1 | |
length | u2 | UTF-8编码的字符串长度 | |
bytes | u1 | 长度为length的UTF-8编码的字符串 | |
CONSTANT_Integer_info | |||
tag | u1 | 值为3 | |
bytes | u4 | 按照高位在前存储的int值 | |
CONSTANT_Float_info | |||
tag | u1 | 值为4 | |
bytes | u4 | 按照高位在前存储的float值 | |
CONSTANT_Float_info | |||
tag | u1 | 值为5 | |
bytes | u8 | 按照高位在前存储的long值 | |
CONSTANT_Double_info | |||
tag | u1 | 值为6 | |
bytes | u8 | 按照高位在前存储的double值 | |
CONSTANT_Class_info | |||
tag | 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_info的索引项 | |
CONSTANT_Methodref_info | |||
tag | u1 | 值为10 | |
index | u2 | 指向声明方法的类描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向名称及类型描述符CONSTANT_NameAndType_info的索引项 | |
CONSTANT_InterfaceMethodref_info | |||
tag | u1 | 值为11 | |
index | u2 | 指向声明方法的接口描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向名称及类型描述符CONSTANT_NameAndType_info的索引项 | |
CONSTANT_NameAndType_info | |||
tag | u1 | 值为12 | |
index | u2 | 指向改字段或方法名称常量项的索引 | |
index | u2 | 指向该字段或方法描述符常量项的索引 | |
CONSTANT_MethodHandle_info | |||
tag | u1 | 值为15 | |
kind | u1 | 值必须在1~9之间,表示方法句柄的类型 | |
index | u2 | 指向该方法 常量项的索引 | |
CONSTANT_MethodType_info | |||
tag | u1 | 值为16 | |
index | u2 | 指向该方法类型描述符常量项的索引 | |
CONSTANT_InvokeDynamic_info | |||
tag | u1 | 值为18 | |
index | u2 | 对当前class文件中引导方法表的bootstrap_methods数组的有效索引 | |
index | u2 | 对常量项的索引,必须为CONSTANT_NameAndType_info结构 |
上表记不住?没关系,不必要去记,会查就行。看不懂,没关系,马上教!
继Constant_pool_count后就是常量池了,我们知道常量池中的每一项都具备相同的特征——第1个字节作为类型标记。查看Test1.class字节码,tag为0x0A
,十进制为10
,查表得类型为CONSTANT_Methodref_info
,该类型除tag外还有两个u2类型的index,所以接着数4个字节,前2个字节(0x0004表示4
),后2个字节(0x0014表示20
),OK!我们得到了第一个常量:#1=Methodref #4.#20
。由于我们还没分析完所以暂时不知道引用的具体常量项含义。
接下来的类型是Constant_Utf8
,除tag外length和bytes分别代表不同含义,length
表示给Utf8字符串的占多少个字节,比如占5个字节,那么我们往后数5个字节的数据就是该字符串表示的具体内容了(就是bytes)。
按照上述方法分析出索引5~19之间的Utf8类型的数据如下:
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10= Utf8 LineNumberTable
#11= Utf8 LocalVariableTable
#12= Utf8 this
#13= Utf8 Lbytecode/Test1;
#14= Utf8 getA
#15= Utf8 ()I
#16= Utf8 setA
#17= Utf8 (I)V
#18= Utf8 SourceFile
#19= Utf8 Test1.java
最后的两个常量项都是Utf8类型,值为:
#22= Utf8 bytecode/Test1
#23= Utf8 java/lang/Object
至此常量池中的所有项都分析完了,我们可以根据引用的索引项写上注释,结果如下所示:
Constant_pool:
#1 = Methodref #4.#20 //java/lang/Object.<init>:()V
#2 = Fieldref #3.#21 //bytecode/Test1.a:I
#3 = Class #22 //bytecode/Test1
#4 = Class #23 //java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10= Utf8 LineNumberTable
#11= Utf8 LocalVariableTable
#12= Utf8 this
#13= Utf8 Lbytecode/Test1;
#14= Utf8 getA
#15= Utf8 ()I
#16= Utf8 setA
#17= Utf8 (I)V
#18= Utf8 SourceFile
#19= Utf8 Test1.java
#20= NameAndType #7:#8 //<init>:()V
#21= NameAndType #5:#6 //a:I
#22= Utf8 bytecode/Test1
#23= Utf8 java/lang/Object
也许您会疑惑诸如()V,Lbytecode/Test1;
等书写方式,读完下面这三段就知道了:
- 在JVM规范中,每个变量/字段都有描述信息,描述信息主要的作用是描述字段的数据类型、方法的参数列表(包括数量、类型与顺序)与返回值。根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符来表示,对象类型则使用字符L加对象的全限定名来表示。为了压缩字节码文件的体积,对于基本数据类型,JVM都只使用一个大写字母来表示,B - byte,C - char,D - double,F - float,I - int,J - long,S - short,Z - boolean,V - void,L - 对象类型,如Ljava/lang/String;
- 对于数组类型来说,每一个维度使用一个前置的[来表示,如int[]被记录为[I,String[][]被记录为[[Ljava/lang/String;
- 用描述符描述方法时,按照先参数列表,后返回值的顺序来描述。参数列表按照参数的严格顺序放在一组()之内,如方法String
setNameAndAge(String name,int
age)的描述符为:(Ljava/lang/String;,I)Ljava/lang/String;
对比javap
命令分析的结果,完美!OK,分析常量池就结束了,接下来的文章将分析余下的结构。
版权属于:带翅膀的猫
本文链接:https://www.chengpengper.cn/archives/56/
转载时须注明出处及本声明