Java的跨平台特性建立在Java虚拟机之上。
- Java虚拟机在不同平台上有不同的版本,但是他们都能执行同一class文件。
- 任何编程语言的源代码,只要能编译成class文件,都能运行在Java虚拟机上面。例如Groovy语言。
1. Class文件结构
Class文件在Java虚拟机规范中的定义如下:
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];
}
可以看出,其中依次包含魔数、小版本号、大版本号、常量池、类访问标志、当前类引用、超类引用、实现的接口引用、字段、方法以及类的属性。更详细、直观的结构如下图所示:
下面来逐一说明Class文件中的每一种结构。
1.1 常量池
常量池中有很多种常量,有一种可以通用来描述它们的结构:
cp_info {
u1 tag;
u1 info[];
}
每一种常量都以一个描述当前常量类型的u1(8字节无符号)整数tag开始,后面接上根据不同常量类型变化的2个或多个字节组成的,用来描述该常量携带的具体信息的东东。比如CONSTANT_Class_info常量
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
它的info[]就是u2(2字节无符号整数)表示的name_index。而CONSTANT_Integer_info的info[]则是u4(4字节无符号整数)表示的整数具体值:
CONSTANT_Integer_info {
u1 tag;
u4 bytes;
}
截止到Java7,常量池包含了14种常量,他们各自的tag值如下表:
<h6 align = "center">表1 常量类型及其tag值</h6>
Constant Type | Value |
---|---|
CONSTANT_Class | 7 |
CONSTANT_Fieldref | 9 |
CONSTANT_Methodref | 10 |
CONSTANT_InterfaceMethodref | 11 |
CONSTANT_String | 8 |
CONSTANT_Integer | 3 |
CONSTANT_Float | 4 |
CONSTANT_Long | 5 |
CONSTANT_Double | 6 |
CONSTANT_NameAndType | 12 |
CONSTANT_Utf8 | 1 |
CONSTANT_MethodHandle | 15 |
CONSTANT_MethodType | 16 |
CONSTANT_InvokeDynamic | 18 |
总的来说,常量池中的常量包括字面量和符号引用两类
- 字面量。底层的数据类型,包括数字常量CONSTANT_Integer、CONSTANT_Float、CONSTANT_Long以及CONSTANT_Double和字符串常量CONSTANT_Utf8。它们的值就存储在各自的info[]之中。
- 符号引用。包括类和接口名、字段和方法信息等。他们都通过索引直接或间接地指向CONSTANT_Utf8常量。
1.1.1 CONSTANT_Class
The CONSTANT_Class_info structure is used to represent a class or an interface:
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
CONSTANT_Class类型的常量,通过name_index指向常量池中的一个CONSTANT_Utf8常量,用来表示自己所代表的类或接口名。
由于数组同样是对象,CONSTANT_Class表示数组的方式与方法与域的描述符中数组表示方式一样,如int[][]表示成[[I,Thread[]表示成[Ljava/lang/Thread。
1.1.2 CONSTANT_NameAndType
The CONSTANT_NameAndType_info structure is used to represent a field or method, without indicating which class or interface type it belongs to:
CONSTANT_NameAndType_info {
u1 tag;
u2 name_index;
u2 descriptor_index;
}
- name_index,指向常量池中的一个CONSTANT_Utf8_info,表示一个field or method的名称。
- descriptor_index,同样指向CONSTANT_Utf8_info,field的类型或method的签名(返回类型+参数列表),统统叫它descriptor描述符。
1.1.3 CONSTANT_Fieldref, CONSTANT_Methodref, and CONSTANT_InterfaceMethodref
分别表示Fields, methods, and interface methods引用,结构相似:
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_Class和CONSTANT_NameAndType可以完全确定一个字段或者方法,所以Fields, methods, and interface methods引用都含有:
- class_index。指向CONSTANT_Class_info常量,代表该field或method所在的类,或者interface method所在的接口。
- name_and_type_index。指向CONSTANT_NameAndType常量。
所以,CONSTANT_Fieldref, CONSTANT_Methodref, and CONSTANT_InterfaceMethodref与CONSTANT_Class和CONSTANT_NameAndType以及CONSTANT_Utf8的引用关系大致如下:
常量池疑问
- 不太明白的是,为什么有了CONSTANT_Utf8还要CONSTANT_String,其实CONSTANT_String也是指向CONSTANT_Utf8的啊?
- CONSTANT_MethodHandle、CONSTANT_MethodType、CONSTANT_InvokeDynamic是Java7新加入的,它们用来干啥?我还没学到这儿来……
1.2 字段与方法
如图1所示,类的字段与方法具有相似的结构。以字段为例,字段包括字段数目以及字段具体信息数组:
u2 fields_count;
field_info fields[fields_count];
如图1中看到的那样,字段具体信息的具体结构:
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。
- descriptor_index,字段类型,同样指向常量池中的CONSTANT_Utf8。
- attributes_count,属性数组的长度。一个字段可能拥有一些属性,用于存储额外信息,如初始化值、注释信息等。
- attributes[attributes_count],属性数组。
字段疑问
- fields与常量池中的CONSTANT_Fieldref的关系?
- 看到field的属性中也许带有初始化值,突然想到《Java编程思想》中的一道题,定义2个String类型的字段,问在定义的时候初始化与在构造函数中初始化有什么不同?
我当时编写了这样的程序:
public class Task2 {
public String s1="has initialized";
public String s2;
public Task2(){
s2="initialized in constructor";
}
}
编译之后,通过javap -c Task2 查看:
Compiled from "Task2.java"
public class peris.sky.learn.Task2 {
public java.lang.String s1;
public java.lang.String s2;
public peris.sky.learn.Task2();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String has initialized
7: putfield #3 // Field s1:Ljava/lang/String;
10: aload_0
11: ldc #4 // String initialized in constructor
13: putfield #5 // Field s2:Ljava/lang/String;
16: return
}
发现,s1与s2的初始化其实都是在构造函数中完成的,只是先后顺序不一样。
学了class文件的字段之后,利用classpy查看class文件,发现s1与s2的属性表中,并没有初始化值,于是仔细阅读java虚拟机规范,发现这个初始化值只有常量(static and final)类型的field的属性表中才有。
1.3 属性
属性出现在class文件中的类属性表、字段属性表、方法属性表以及方法中的Code属性的属性表四种地方。属性与常量池中的常量类似,有多种类型,但都有相似的结构:
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
其中:
- attribute_name_index,指向常量池中的CONSTANT_Utf8_info常量,表示该属性的类型(如表2所示)。
- attribute_length,指出下一项info[]的长度。
- info[attribute_length], 属性的具体信息,这个是跟随属性类型的不同而变化的。
<h6 align = "center">表2 属性类型、其可能出现的位置以及含义</h6>
AttributeType | classfile | field_info | method_info | Code | 含义 |
---|---|---|---|---|---|
ConstantValue | y | 常量字段的值 | |||
Code | y | 方法执行的字节码 | |||
StackMapTable | y | 类型检查的时候用 | |||
Exceptions | y | 方法可能抛出的一样信息 | |||
InnerClasses | y | 内部类 | |||
EnclosingMethod | y | ||||
Synthetic | y | y | y | 源文件中不存在的类成员 | |
Signature | y | y | y | ||
SourceFile | y | 源文件名 | |||
SourceDebugExtension | y | ||||
LineNumberTable | y | 方法的行号 | |||
LocalVariableTable | y | 方法的局部变量信息 | |||
LocalVariableTypeTable | y | ||||
Deprecated | y | y | y | 指出不建议使用的东东 | |
RuntimeVisibleAnnotations | y | y | y | ||
RuntimeInvisibleAnnotations | y | y | y | ||
RuntimeVisibleParameterAnnotations | y | ||||
RuntimeInvisibleParameterAnnotations | y | ||||
AnnotationDefault | y | ||||
BootstrapMethods | y |
1.3.1 Code属性
Code只存在于method_info之中,Code属性中存放字节码等方法相关信息,正如前面所说的那样,Code属性中还可以有其它属性,所以比较麻烦。它的结构如下:
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
可以看出,Code的info[]内容非常丰富:
- max_stack,方法执行过程中,操作数栈不停地变化,在整个执行过程中存在着一个最大深度,这是在编译过程中决定的。
- max_locals,局部变量表的最大size。相比于操作数栈,局部变量表像一个数组。
- code[code_length],这个就是方法最重要的字节码咯,jvm把它翻译成一个又一个的jvm指令,然后就可以执行方法了。
- exception_table,异常表,表中每一项是由start_pc、end_pc、handler_pc以及catch_type组成的结构体那样的东东。从方法字节码的start_pc偏移量开始到end_pc偏移量为止的这段代码中,如果遇到catch_type所指向的异常,那么代码就跳转到handler_pc的位置执行,说白了,就是写代码的try-catch结构。
- attributes[attributes_count],Code属性的属性。从表2中可以看出Code属性可以拥有包括StackMapTable、LineNumberTable、LocalVariableTable以及LocalVariableTypeTable在内的4中属性。其中LineNumberTable、LocalVariableTable以及LocalVariableTypeTable三个与类的SourceFile属性一起是调试时使用的。比如,LineNumberTable用来指定字节码与源文件中的行号的对应关系。 StackMapTable是用来class文件的类型检验。这4个属性都不是运行时必需的。