在阅读JAVA字节码以前,需要回忆一下JVM的结构:
Java字节码的信息主要在Java栈中间体现,下图来自网络,描述了java栈的基本结构:
值得注意的是方法区,在Java虚拟机中,方法区(Method Area)是可供各条线程共享的运行时内存区域。方法 区与传统语言中的编译代码储存区(Storage Area Of Compiled Code)或者操作系统进程的正文段(TextSegment)的作用非常类似,它存储了每一个类的结构信息,例如运行时常量池(RuntimeConstantPool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法。
每一个方法从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
对于执行引擎来说,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。
关于栈帧内部4个区域的含义,直接引用了JAVA虚拟机规范当中的内容。
局部变量表:
每个栈帧内部都包含一组称为局部变量表(LocalVariables)的变量列表。栈帧中局部变量表的长度由编译期决定,并且存储于类和接口的二进制表示之中,既通过方法的Code属性保存及提供给栈帧使用。
一个局部变量可以保存一个类型为 boolean
、byte
、char
、short
、float
、reference
和 returnAddress
的数据,两个局部变量可以保存一个类型为 long
和double
的数据。
局部变量使用索引来进行定位访问,第一个局部变量的索引值为零,局部变量的索引值是从零 至小于局部变量表最大容量的所有整数。
long
和 double
类型的数据占用两个连续的局部变量,这两种类型的数据值采用两个局部变 量之中较小的索引值来定位。例如我们讲一个 double
类型的值存储在索引值为 n
的局部变量中, 实际上的意思是索引值为 n
和 n+1
的两个局部变量都用来存储这个值。索引值为n+1
的局部变量是无法直接读取的,但是可能会被写入,不过如果进行了这种操作,就将会导致局部变量n的内容失效掉。
上文中提及的局部变量 n 的 n 值并不要求一定是偶数,Java 虚拟机也不要求 double 和 long 类型数据采用 64 位对其的方式存放在连续的局部变量中。虚拟机实现者可以自由地选择适当的方 式,通过两个局部变量来存储一个 double 或 long 类型的值。
Java 虚拟机使用局部变量表来完成方法调用时的参数传递,当一个方法被调用的时候,它的 参数将会传递至从 0 开始的连续的局部变量表位置上。特别地,当一个实例方法被调用的时候,第0个局部变量一定是用来存储被调用的实例方法所在的对象的引用(即 Java 语言中的“this” 关键字)。后续的其他参数将会传递至从 1开始的连续的局部变量表位置上。
说白了,局部变量表就是存储方法参数和局部变量的地方。
操作数栈
每一个栈帧内部都包含一个称为操作数栈(Operand Stack)的后进先出 (Last-In-First-Out,LIFO)栈。栈帧中操作数栈的长度由编译期决定,并且存储于类和接 口的二进制表示之中,既通过方法的 Code 属性保存及提供给栈帧使用。
在上下文明确,不会产生误解的前提下,我们经常把“当前栈帧的操作数栈”直接简称为“操 作数栈”。
操作数栈所属的栈帧在刚刚被创建的时候,操作数栈是空的。
Java 虚拟机提供一些字节码指 令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于 从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用的时候,操作数栈也用来准备 调用方法的参数以及接收方法返回结果 。
举个例子,iadd 字节码指令的作用是将两个 int 类型的数值相加,它要求在执行的之前操作 数栈的栈顶已经存在两个由前面其他指令放入的 int 型数值。在 iadd 指令执行时,2 个 int 值 从操作栈中出栈,相加求和,然后将求和结果重新入栈。在操作数栈中,一项运算常由多个子运算 (Subcomputations)嵌套进行,一个子运算过程的结果可以被其他外围运算所使用。
每一个操作数栈的成员(Entry)可以保存一个 Java 虚拟机中定义的任意数据类型的值,包 括 long 和 double 类型。
在操作数栈中的数据必须被正确地操作,这里正确操作是指对操作数栈的操作必须与操作数栈 栈顶的数据类型相匹配,例如不可以入栈两个 int 类型的数据,然后当作 long 类型去操作他们, 或者入栈两个 float 类型的数据,然后使用 iadd 指令去对它们进行求和。有一小部分 Java 虚 拟机指令(例如 dup 和 swap 指令)可以不关注操作数的具体数据类型,把所有在运行时数据区 中的数据当作裸类型(Raw Type)数据来操作,这些指令不可以用来修改数据,也不可以拆散那 些原本不可拆分的数据,这些操作的正确性将会通过 Class 文件的校验过程来强制保障。
在任意时刻,操作数栈都会有一个确定的栈深度,一个 long 或者 double 类型的数据会占用 两个单位的栈深度,其他数据类型则会占用一个单位深度。
动态链接
每一个栈帧内部都包含一个指向运行时常量池的引用来支持当前方法 的代码实现动态链接(Dynamic Linking)。在 Class 文件里面,描述一个方法调用了其他方法, 或者访问其成员变量是通过符号引用(Symbolic Reference)来表示的,动态链接的作用就是 将这些符号引用所表示的方法转换为实际方法的直接引用。类加载的过程中将要解析掉尚未被解析 的符号引用,并且将变量访问转化为访问这些变量的存储结构所在的运行时内存位置的正确偏移 量。
由于动态链接的存在,通过晚期绑定(Late Binding)使用的其他类的方法和变量在发生 变化时,将不会对调用它们的方法构成影响。
分析Class文件的字节码
假设有这样一个JAVA文件:
public class HelloWorld
{
String str = "";
public String getStr()
{
return str;
}
public void setStr(String str)
{
this.str = str;
}
}复制代码
编译.Java文件:javac -g HelloWorld.java
输出字节码:javap -verbose HelloWorld
显示如下的内容:
public class test01.HelloWorld
SourceFile: "HelloWorld.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#21 // java/lang/Object."<init>":()V
#2 = String #22 //
#3 = Fieldref #4.#23 // test01/HelloWorld.str:Ljava/lang/String;
#4 = Class #24 // test01/HelloWorld
#5 = Class #25 // java/lang/Object
#6 = Utf8 str
#7 = Utf8 Ljava/lang/String;
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Ltest01/HelloWorld;
#15 = Utf8 getStr
#16 = Utf8 ()Ljava/lang/String;
#17 = Utf8 setStr
#18 = Utf8 (Ljava/lang/String;)V
#19 = Utf8 SourceFile
#20 = Utf8 HelloWorld.java
#21 = NameAndType #8:#9 // "<init>":()V
#22 = Utf8
#23 = NameAndType #6:#7 // str:Ljava/lang/String;
#24 = Utf8 test01/HelloWorld
#25 = Utf8 java/lang/Object
{
java.lang.String str;
flags:
public test01.HelloWorld();
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String
7: putfield #3 // Field str:Ljava/lang/String;
10: return
LineNumberTable:
line 5: 0
line 7: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Ltest01/HelloWorld;
public java.lang.String getStr();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #3 // Field str:Ljava/lang/String;
4: areturn
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ltest01/HelloWorld;
public void setStr(java.lang.String);
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #3 // Field str:Ljava/lang/String;
5: return
LineNumberTable:
line 16: 0
line 17: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Ltest01/HelloWorld;
0 6 1 str Ljava/lang/String;
}复制代码
说明:
javac -g TestClass.java
- -g:生成所有的调试信息,包括局部变量名和行号信息。
javap -c TestClass > TCC.txt,对于javap常用的参数: - -c:输出字节码Code
- -l(小写L):输出Code、LineNumberTable与LocalVariableTable
- -s:输出方法签名(方法的接收参数列表和返回值)
- -verbose:包含-c、-l以及输出class文件的编译版本,常量池,Stack, Locals, Args_size
- 对于javap而言,常用的就是-c或-verbose
使用16进制的Hex Fiend 查看字节文件得到如下信息:
CA FE BA BE 00 00 00 33 00 1A 0A 00 05 00 15 08 00 16 09 00 04 00 17 07 00 18 07 00 19 01 00 03 73 74 72 01 00 12 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 12 4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65 01 00 04 74 68 69 73 01 00 13 4C 74 65 73 74 30 31 2F 48 65 6C 6C 6F 57 6F 72 6C 64 3B 01 00 06 67 65 74 53 74 72 01 00 14 28 29 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 01 00 06 73 65 74 53 74 72 01 00 15 28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56 01 00 0A 53 6F 75 72 63 65 46 69 6C 65 01 00 0F 48 65 6C 6C 6F 57 6F 72 6C 64 2E 6A 61 76 61 0C 00 08 00 09 01 00 00 0C 00 06 00 07 01 00 11 74 65 73 74 30 31 2F 48 65 6C 6C 6F 57 6F 72 6C 64 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 00 21 00 04 00 05 00 00 00 01 00 00 00 06 00 07 00 00 00 03 00 01 00 08 00 09 00 01 00 0A 00 00 00 39 00 02 00 01 00 00 00 0B 2A B7 00 01 2A 12 02 B5 00 03 B1 00 00 00 02 00 0B 00 00 00 0A 00 02 00 00 00 05 00 04 00 07 00 0C 00 00 00 0C 00 01 00 00 00 0B 00 0D 00 0E 00 00 00 01 00 0F 00 10 00 01 00 0A 00 00 00 2F 00 01 00 01 00 00 00 05 2A B4 00 03 B0 00 00 00 02 00 0B 00 00 00 06 00 01 00 00 00 0B 00 0C 00 00 00 0C 00 01 00 00 00 05 00 0D 00 0E 00 00 00 01 00 11 00 12 00 01 00 0A 00 00 00 3E 00 02 00 02 00 00 00 06 2A 2B B5 00 03 B1 00 00 00 02 00 0B 00 00 00 0A 00 02 00 00 00 10 00 05 00 11 00 0C 00 00 00 16 00 02 00 00 00 06 00 0D 00 0E 00 00 00 00 00 06 00 06 00 07 00 01 00 01 00 13 00 00 00 02 00 14复制代码
先来看看Class的文件结构:
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];
}复制代码
其中u1、u2、u4分别代表1、2、4个字节无符号数。
magic:
魔数,魔数的唯一作用是确定这个文件是否为一个能被虚拟机所接受的Class文件。魔数值固定为0xCAFEBABE,不会改变。
## minor_version、major_version:
分别为Class文件的副版本和主版本。它们共同构成了Class文件的格式版本号。不同版本的虚拟机实现支持的Class文件版本号也相应不同,高版本号的虚拟机可以支持低版本的Class文件,反之则不成立。
constant_pool_count:
常量池计数器,constant_pool_count的值等于constant_pool表中的成员数加1。
## constant_pool[]:
常量池,constant_pool是一种表结构,它包含Class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其它常量。常量池不同于其他,索引从1开始到constant_pool_count -1。
## access_flags:
访问标志,access_flags是一种掩码标志,用于表示某个类或者接口的访问权限及基础属性。access_flags的取值范围和相应含义见下表:
### this_class:
类索引,this_class的值必须是对constant_pool表中项目的一个有效索引值。constant_pool表在这个索引处的项必须为CONSTANT_Class_info类型常量,表示这个Class文件所定义的类或接口。
## super_class:
父类索引,对于类来说,super_class的值必须为0或者是对constant_pool表中项目的一个有效索引值。如果它的值不为0,那constant_pool表在这个索引处的项必须为CONSTANT_Class_info类型常量,表示这个Class文件所定义的类的直接父类。当然,如果某个类super_class的值是0,那么它必定是java.lang.Object类,因为只有它是没有父类的。
interfaces_count:
接口计数器,interfaces_count的值表示当前类或接口的直接父接口数量。
interfaces[]:
接口表,interfaces[]数组中的每个成员的值必须是一个对constant_pool表中项目的一个有效索引值,它的长度为interfaces_count。每个成员interfaces[i] 必须为CONSTANT_Class_info类型常量。
## fields_count:
字段计数器,fields_count的值表示当前Class文件fields[]数组的成员个数。
## fields[]:
字段表,fields[]数组中的每个成员都必须是一个fields_info结构的数据项,用于表示当前类或接口中某个字段的完整描述。
## methods_count:
方法计数器,methods_count的值表示当前Class文件methods[]数组的成员个数。
## methods[]:
方法表,methods[]数组中的每个成员都必须是一个method_info结构的数据项,用于表示当前类或接口中某个方法的完整描述。
## attributes_count:
属性计数器,attributes_count的值表示当前Class文件attributes表的成员个数。
## attributes[]:
属性表,attributes表的每个项的值必须是attribute_info结构。
重新看一下Class File的表结构:
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];
}复制代码
可以看到前4个字节为魔数,也就是0xCAFEBABE,这里都是十六进制。接下来两个字节是副版本号,是00。
再接下来是副版本号:
0x0033 转换成十进制是51。
接下来是常量池个数。
0x001A 转换成10进制是26。但按理说应该是从索引0到25,但实际上只有1到25。
接下来的数据就是常量池的数据。
所有常量池的数据都有如下的通用结构:
cp_info
{
u1 tag;
u1 info[];
}复制代码
以1个字节的tag开头,后面info[]项的内容tag由的类型所决定。tag有效的类型和对应的取值如下表:
下面我们来介绍下不同类型的tag所对应的结构和规则:
CONSTANT_Class_info结构:
CONSTANT_Class_info结构用于表示类或接口,格式如下:
CONSTANT_Class_info
{
u1 tag;
u2 name_index;
}复制代码
CONSTANT_Class_info结构的tag项的值为CONSTANT_Class(7)。name_index项的值,必须是对常量池的一个有效索引。常量池在该索引处的项必须是CONSTANT_Utf8_info结构,代表一个有效的类或接口二进制名称的内部形式。
CONSTANT_Fieldref_info, CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info结构:
字段,方法和接口方法由类似的结构表示:
CONSTANT_Fieldref_info
{
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_Methodref_info
{
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_InterfaceMethodref_info
{
u1 tag;
u2 class_index;
u2 name_and_type_index;
}复制代码
CONSTANT_Fieldref_info
结构的tag项的值为CONSTANT_Fieldref(9)
。
CONSTANT_Methodref_info结构的tag项的值为CONSTANT_Methodref(10)。 CONSTANT_InterfaceMethodref_info结构的tag项的值为CONSTANT_InterfaceMethodref(11)
class_index项的值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Class_info结构,表示一个类或接口,当前字段或方法是这个类或接口的成员。
name_and_type_index项的值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,它表示当前字段或方法的名字和描述符。
CONSTANT_String_info结构:
CONSTANT_String_info用于表示java.lang.String类型的常量对象,格式如下:
CONSTANT_String_info
{
u1 tag;
u2 string_index;
}复制代码
CONSTANT_String_info结构的tag项的值为CONSTANT_String(8)。string_index项的值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示一组Unicode码点序列,这组Unicode码点序列最终会被初始化为一个String对象。
CONSTANT_Integer_info和CONSTANT_Float_info结构:
CONSTANT_Intrger_info和CONSTANT_Float_info结构表示4字节(int和float)的数值常量:
CONSTANT_Integer_info
{
u1 tag;
u4 bytes;
}
CONSTANT_Float_info
{
u1 tag;
u4 bytes;
}复制代码
CONSTANT_Integer_info结构的bytes项表示int常量的值,按照Big-Endian的顺序存储。 CONSTANT_Float_info结构的bytes项按照IEEE 754单精度浮点格式。表示float常量的值,按照Big-Endian的顺序存储。
CONSTANT_Long_info和CONSTANT_Double_info结构:
CONSTANT_Long_info和CONSTANT_Double_info结构表示8字节(long和double)的数值常量:
CONSTANT_Long_info
{
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
CONSTANT_Double_info
{
u1 tag;
u4 high_bytes;
u4 low_bytes;
}复制代码
在Class文件的常量池中,所有的8字节的常量都占两个表成员(项)的空间。如果一个CONSTANT_Long_info或CONSTANT_Double_info结构的项在常量池中的索引为n,则常量池中下一个有效的项的索引为n+2,此时常量池中索引为n+1的项有效但必须被认为不可用。
CONSTANT_Long_info结构的tag项的值是CONSTANT_Long(5)。 CONSTANT_Double_info结构的tag项的值是CONSTANT_Double(6)。
CONSTANT_Long_info结构中的无符号的high_bytes和low_bytes项用于共同表示long型常量,构造形式为((long) high_bytes << 32) + low_bytes,high_bytes和low_bytes都按照Big-Endian顺序存储。 CONSTANT_Double_info结构中的high_bytes和low_bytes共同按照IEEE 754双精度浮点格式表示double常量的值。high_bytes和low_bytes都按照Big-Endian顺序存储。
CONSTANT_NameAndType_info结构:
CONSTANT_NameAndType_info结构用于表示字段或方法,但是和前面介绍的三个表示字段方法的结构不同,CONSTANT_NameAndType_info结构没有标识出它所属的类或接口,格式如下:
CONSTANT_NameAndType_info
{
u1 tag;
u2 name_index;
u2 descriptor_index;
}复制代码
CONSTANT_NameAndType_info结构的tag项的值为CONSTANT_NameAndType(12)。
name_index项的值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,这个结构要么表示特殊的方法名,要么表示一个有效的字段或方法的非限定名。
descriptor_index项的值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info(§4.4.7)结构,这个结构表示一个有效的字段描述符或方法描述符。
CONSTANT_Utf8_info
结构:
CONSTANT_Utf8_info
结构用于表示字符串常量的值:
CONSTANT_Utf8_info
{
u1 tag;
u2 length;
u1 bytes[length];
}复制代码
CONSTANT_Utf8_info
结构的tag项的值为CONSTANT_Utf8(1)。length项的值指明了bytes[]数组的长度,bytes[]是表示字符串值的byte数组。
CONSTANT_MethodHandle_info
结构:
CONSTANT_MethodHandle_info结构用于表示方法句柄,结构如下:
CONSTANT_MethodHandle_info
{
u1 tag;
u1 reference_kind;
u2 reference_index;
}复制代码
CONSTANT_MethodHandle_info
结构的tag项的值为CONSTANT_MethodHandle(15)
。reference_kind
项的值必须在1至9之间(包括1和9),它决定了方法句柄的类型。
reference_index
项的值必须是对常量池的有效索引,索引项和reference_kind
的对应关系如下:
CONSTANT_MethodType_info
结构:
CONSTANT_MethodType_info
结构用于表示方法类型:
CONSTANT_MethodType_info
{
u1 tag;
u2 descriptor_index;
}复制代码
CONSTANT_MethodType_info
结构的tag项的值为CONSTANT_MethodType(16)。descriptor_index项的值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符。
CONSTANT_InvokeDynamic_info
结构:
CONSTANT_InvokeDynamic_info
用于表示invokedynamic
指令所使用到的引导方法、引导方法使用到动态调用名称、参数和请求返回类型以及可以选择性的附加被称为静态参数的常量序列。
CONSTANT_InvokeDynamic_info
{
u1 tag;
u2 bootstrap_method_attr_index;
u2 name_and_type_index;
}复制代码
CONSTANT_InvokeDynamic_info
结构的tag项的值为CONSTANT_InvokeDynamic(18)
。bootstrap_method_attr_index项的值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引。ame_and_type_index项的值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info
结构,表示方法名和方法描述符
#1 = Methodref #5.#21 // java/lang/Object."<init>":()V复制代码
Methodref 在常量类型表中间是10。观察字节码:
0A 刚好是10。那么接下来就是methodref的结构:
CONSTANT_Methodref_info
{
u1 tag;
u2 class_index;
u2 name_and_type_index;
}复制代码
class_index
为0x0005,name_and_type_index
为0015。正好对应“#5.#21”
接下来就是按照这种模式分析就可以了。
注意#25 = Utf8 java/lang/Object
这就说明索引25存储的是java/lang/Object
这个UTF-8字符串。UTF-8的字符串如下:6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74
常量池完毕以后,就是JAVA类本身的信息了(javap 出现的信息也可以看出来)
类本身的信息
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];
}复制代码
常量池后面两个字节是访问标志access_flags
:00 21
其中ACC_PUBLIC
的值为0x0001,ACC_SUPER
的值为0x0020,这个字段是复合的。
字段
每个字段(Field)都由field_info
结构所定义,在同一个Class
文件中,不会有两个字段同时具有相同的字段名和描述符。
field_info
结构格式如下:
field_info
{
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}复制代码
access_flags
项的值是用于定义字段被访问权限和基础属性的掩码标志。取值范围如下表:
name_index
项的值必须是对常量池的一个有效索引。常量池在该索引处的项必须是CONSTANT_Utf8_info
结构,表示一个有效的字段的非全限定名。
descriptor_index
项的值必须是对常量池的一个有效索引。常量池在该索引处的项必须是CONSTANT_Utf8_info
结构,表示一个有效的字段的描述符。
attributes_count
的项的值表示当前字段的附加属性的数量。
attributes
表的每一个成员的值必须是attribute
结构,一个字段可以有任意个关联属性。
方法
所有方法(Method),包括实例初始化方法和类初始化方法在内,都由method_info结构所定义。在一个Class文件中,不会有两个方法同时具有相同的方法名和描述符。method_info结构格式如下:
method_info
{
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
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_SYNCHRONIZED 0x0020 synchronized,方法由管程同步
ACC_BRIDGE 0x0040 bridge,方法由编译器产生
ACC_VARARGS 0x0080 表示方法带有变长参数
ACC_NATIVE 0x0100 native,方法引用非java语言的本地方法
ACC_ABSTRACT 0x0400 abstract,方法没有具体实现
ACC_STRICT 0x0800 strictfp,方法使用FP-strict浮点格式
ACC_SYNTHETIC 0x1000 方法在源文件中不出现,由编译器产生
name_index
项的值必须是对常量池的一个有效索引。常量池在该索引处的项必须是CONSTANT_Utf8_info
结构。
descriptor_index
项的值必须是对常量池的一个有效索引。常量池在该索引处的项必须是CONSTANT_Utf8_info
结构,表示一个有效的方法的描述符。
attributes_count
的项的值表示这个方法的附加属性的数量。attributes
表的每一个成员的值必须是attribute
结构,一个方法可以有任意个与之相关的属性。
属性:
属性(Attributes)在Class
文件格式中的ClassFile
结构、field_info 结构,method_info
结构和Code_attribute
结构都有使用,所有属性的通用格式如下:
attribute_info
{
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}复制代码
对于任意属性,attribute_name_index必须是对当前Class文件的常量池的有效16位无符号索引。常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示当前属性的名字。attribute_length项的值给出了跟随其后的字节的长度,这个长度不包括attribute_name_index和attribute_name_index项的6个字节。
对于字段、方法、和属性的结构,我们很容易的可以通过javap工具查看到。
Class的文件结构是十分复杂的,要在一篇博客里面讲清楚是不太可能的,只能算是入门。有什么问题的话可以在评论里提问。
小广告
新书《Java并发编程系统与模型》目前已经写完一部分。但是实际上说实话,并不知道读者们对java并发系统有哪些比较关心的,而且闭门造车实在是太累,写出来怕没人看。所以我放在这里征求大家的意见。大家可以直接在评论里提问或者希望作者重点描写哪一部分内容,我好有针对性的写。