下面我们接着为访问标志,类索引,父类索引,接口索引集合,字段集合,方法表集合
访问标志:
常量池结束后紧接着的两个字节代表访问标志,用来标识一些类或接口的访问信息,包括:这个Class是类还是接口;是否定义为public;是否定义为abstract;如果是类的话,是否被声明为final等。具体的标志位以及含义如下表:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否是public |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可以设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真 |
ACC_INTERFACE | 0x0200 | 标识是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否是abstract,对于接口和抽象类来说为真,其他类都为假 |
ACC_SYNITHETIC | 0x1000 | 标识这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举类 |
类索引(2个字节)、父类索引(2个字节)与接口索引(2个字节接口数+接口)集合:
在访问标志access_flags后接下来就是类索引(this_class)和父类索引(super_class),这两个数据都是u2类型的,而接下来的接口索引集合是一个u2类型的集合,class文件由这三个数据项来确定类的继承关系。由于Java中是单继承,所以父类索引只有一个;但Java类可以实现多个接口,所以接口索引是一个集合。
类索引用来确定这个类的全限定名,这个全限定名就是说一个类的类名包含所有的包名,然后使用"/"代替"."。比如Object的全限定名是java.lang.Object。父类索引确定这个类的父类的全限定名,除了Object之外,所有的类都有父类,所以除了Object之外所有类的父类索引都不为0.接口索引集合存储了implements语句后面按照从左到右的顺序的接口。
类索引和父类索引都是一个索引,这个索引指向常量池中的CONSTANT_Class_info类型的常量。然后再CONSTANT_Class_info常量中的索引就可以找到常量池中类型为CONSTANT_Utf8_info的常量,而这个常量保存着类的全限定名。
字段表集合:
字段表用来描述接口或类中声明的变量。字段包括类级变量和实例级变量,但不包括方法内变量。所谓的类级变量就是静态变量,这个变量不属于这个类的任何实例,可以不用定义类实例就可以使用;实例级变量不是静态变量,是和类实例相关联的,需要定义类实例才能使用。
那么,声明一个变量需要哪些信息呢?
有:字段的作用域(public、private和protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final修饰符)、并发可见性(volatile修饰符)、是否可被序列化(transient修饰符)、字段的数据类型(基本类型、对象、数组)以及字段名称。包含的信息有点多,不过不需要的可以不写。这些信息中,各个修饰符可以用布尔值表示。而字段叫什么名字、字段被定义为什么类型数据都是无法固定的,只能用常量池中的常量来表示。
下面是字段表的格式:
类型 | 名称 | 数量 |
---|---|---|
U2 | access_flags | 1 |
U2 | name_index | 1 |
U2 | descriptor_index | 1 |
U2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
其中的字段修饰符access_flags,和类中的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 |
显然,ACC_PUBLIC、ACC_PRIVATE和ACC_PROTECTED只能选择一个,ACC_FINAL和ACC_VOLATILE不能同时选择。接口中的字段必须有ACC_PUBLIC、ACC_STATIC和ACC_FINAL标志,这是Java语言本身的规则决定的。
access_flags给出了字段中所有可以用布尔值表示的修饰符,剩下的信息就是字段的名字、变量类型等信息。access_flags后面的是name_index和descriptor_index,前者是字段名的常量池索引,后者是字段描述符的常量池索引。name_index可以描述字段的名字,descriptor_index可以描述字段的数据类型。不过,对于方法的描述符来说就要复杂一些,因为一个方法除了返回值类型,还有参数类型,而且参数的个数还不确定。根据描述符规则,这些类型都使用一个大写字母来表示,如下表:
标志名称 | 含义 | 标志名称 | 含义 |
---|---|---|---|
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",一个double数组"double[]"将标记为"[D"。
当描述符用来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号"()"内。比如方法void inc()的描述符是:()V。方法java.lang.String toString()的描述符是:()Ljava/lang/String。方法int indexOf(char[] source,int sourceOffset,int sourceCount,char[] target,int targetOffset,int targetCount,int fromIndex)的描述符是:([CII[CIII)I。
descriptor_info后面是属性信息,这会在后面属性表集合中介绍。
方法表集合:
在字段表集合中介绍了字段的描述符和方法的描述符,对于理解方法表有很大帮助。class文件存储格式中对方法的描述和对字段的描述几乎相同,方法表的结构也和字段表相同,这里就不再列出。不过,方法表的访问标志和字段的不同,列出如下:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否是public |
ACC_PRIVATE | 0x0002 | 方法是否是private |
ACC_PUBLICPROTECTED | 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 | 方法是否是由编译器自动产生的 |
下面我们来一个例子
JavaCode
public class Test{
public int a;
private String b;
private int add(int arg1,int arg2){
return arg1+arg2;
}
}
二进制解析
Javap解析
C:\Users\GH\Desktop>javap -verbose Test.class
Classfile /C:/Users/GH/Desktop/Test.class
Last modified 2018-8-8; size 287 bytes
MD5 checksum 430dad91948d95b5532953a32356174c
Compiled from "Test.java"
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#16 // java/lang/Object."<init>":()V
#2 = Class #17 // Test
#3 = Class #18 // java/lang/Object
#4 = Utf8 a
#5 = Utf8 I
#6 = Utf8 b
#7 = Utf8 Ljava/lang/String;
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 add
#13 = Utf8 (II)I
#14 = Utf8 SourceFile
#15 = Utf8 Test.java
#16 = NameAndType #8:#9 // "<init>":()V
#17 = Utf8 Test
#18 = Utf8 java/lang/Object
{
public int a;
descriptor: I
flags: ACC_PUBLIC
public Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
}
SourceFile: "Test.java"
属性表集合:
属性表(attribute_info),在Class文件、字段表、方法表都可以携带字节的属性表集合,用于描述某些场景专有信息。
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量池 |
Deprecated | 类,方法,字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClass | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部便狼描述 |
StackMapTable | Code属性 | JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 |
Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | 用于存储额外的调试信息 |
Synthetic | 类,方法表,字段表 | 标志方法或字段为编译器自动生成的 |
LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 |
RuntimeInvisibleAnnotations | 表,方法表,字段表 | 用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParameterAnnotation | 方法表 | 作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法 |
RuntimeInvisibleParameterAnnotation | 方法表 | 作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数 |
AnnotationDefault | 方法表 | 用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | 用于保存invokeddynamic指令引用的引导方式限定符 |
对于每个属性,它的名称需要从常量池中引用一个CONSTANT_utf8_info类型的常量类表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性区说明属性值所占用的位数即可.
属性表定义的结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u2 | attribute_length | 1 |
u1 | info | attribute_length |
1.CODE属性
Java程序方法体中的代码经过Javac编译处理后,最终变为字节码指令存储在Code属性中.Code属性出现在方法表的属性集合中,但是并非所有的方法表都有这个属性.例如接口或类中的方法就不存在Code属性了.
在字节码指令之后的是方法的是方法的显式异常处理表集合,异常表对于Code属性来说并不是必须的.
结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
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_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
2.EXCEPTIONS属性
- Exception属性的作用是列出方法中能抛出的受查异常Check Exceptions,也就是方法描述时在throws关键字之后列举的异常
- Exception属性中的number_of_exceptions项表示方法可能抛出的number_of_exceptions种受查异常,每一种受查异常使用一个exception_index_table项表示,exception_index_table是一个指向常量池中CONSTANT_Class_info型常量的索引,代表了该受查异常的类型.
结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u2 | attribute_lrngth | 1 |
u2 | attribute_of_exception | 1 |
u2 | exception_index_tsble | number_of_exceptions |
3.LINENUMBERTABLE属性
line_number_table是一个数量为line_number_table_length,类型为line_number_info的集合,line_number_info表包括了start_PC和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源代码行号.
结构:
类型 | 名称 | 数量 |
---|---|---|
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 |
虚拟机预定义的属性有20多个,就不意一一介绍,基本上和上述的几个属性差不多.