例子主要是《深入jvm》中的例子,class文件是其中的act.class,java源文件是:
class Act {
public static void doMathForever() {
int i = 0;
for (;;) {
i += 1;
i *= 2;
}
}
}
class文件hex形式:
CA FE BA BE 00 03 00 2D 00 11 07 00 07 07 00 10
0A 00 02 00 04 0C 00 06 00 05 01 00 03 28 29 56
01 00 06 3C 69 6E 69 74 3E 01 00 03 41 63 74 01
00 08 41 63 74 2E 6A 61 76 61 01 00 04 43 6F 64
65 01 00 0D 43 6F 6E 73 74 61 6E 74 56 61 6C 75
65 01 00 0A 45 78 63 65 70 74 69 6F 6E 73 01 00
0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65
01 00 0E 4C 6F 63 61 6C 56 61 72 69 61 62 6C 65
73 01 00 0A 53 6F 75 72 63 65 46 69 6C 65 01 00
0D 64 6F 4D 61 74 68 46 6F 72 65 76 65 72 01 00
10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63
74 00 20 00 01 00 02 00 00 00 00 00 02 00 09 00
0F 00 05 00 01 00 09 00 00 00 30 00 02 00 01 00
00 00 0C 03 3B 84 00 01 1A 05 68 3B A7 FF F9 00
00 00 01 00 0C 00 00 00 12 00 04 00 00 00 05 00
02 00 07 00 05 00 08 00 09 00 06 00 00 00 06 00
05 00 01 00 09 00 00 00 1D 00 01 00 01 00 00 00
05 2A B7 00 03 B1 00 00 00 01 00 0C 00 00 00 06
00 01 00 00 00 02 00 01 00 0E 00 00 00 02 00 08
- java的class文件是8位二进制流,数据项按顺序存放,无间隔,占多字节的数据项以高位在前的顺序分几个字节存放;
- java class基本类型:u1,u2,u4,u8,分别对应:1,2,4,8字节的无符号类型;
- java class file表格展示:(太大了,来个url自己看吧,wikipedia)
- 表项详解:1)magic(魔数):说白了就是cafebabe,本来java就是咖啡嘛,这4个字节用来区分是否是java的class文件,有则是;2)minor_version&major_version:两个字节的minor和两个字节的major,以上为例就是minor:3,major:2D(JDK 1.1);3)再之后就是常量池了,2个字节表示constant_pool_count,本例是17,表示class文件中常量池中的项数(比实际的大1),接着就是常量池,连续的constant_pool_count-1个字节存储常量池各个入口,常量池入口项解释见本文第5条笔记;4)在之后的是access_flags:2个字节表示访问类型,是类还是接口,是public还是private,abstract或者final等等,标志位具体定义见本文第6条笔记,本例是00 20,表示老版本ACC_SUPER;5)之后是this_class,这是2字节的一个对常量池的索引,本例是00 01,即本例的自身类叫做Act;6)之后就是super_class,也是一个2字节常量池索引,指向父类的名字, 本例是00 02,即java.lang.Object;7)interface_count和interfaces:interface_count指出本文件有多少直接实现或者由接口扩展的父接口的数量,本例没有,故为00 00,之后是interfaces的具体内容,实际的项数就是之前的interface_count数,因为interface_count=0,所以本例的interfaces就没有值;8)接下来是fields_count和fields:对本类所声明的字段的描述,首先是个2字节的count,本例是00 00,所以后续也没有fields的项;9)再之后就是methods_count和methods了,count是一个2字节数据,本例的method_count是00 02,这个count只表示在类或接口中显式定义的方法,继承的方法不计数,count后是method_info的表,包含方法的一些信息如方法名、描述符等,本例中00 09表明方法是public(01)&static(08),00 0F是方法名的常量池入口,即常量池的第15项doMathForever,再下来00 05是方法描述符常量池入口,即常量池第5项:()V,然后00 01是属性表的count数,表示1项属性,接下来是00 09表示属性表的常量池入口即常量池第9项Code,接下来的4个字节00 00 00 30表示code属性长度:48字节,接着00 02是操作数最大数,然后00 01是局部变量存储长度,这里方法里只有一个变量i,所以是1,然后00 00 00 0c是code字节码长度12,然后的12个字节就是字节码code了,再后的00 00是异常数,之后异常栈数是0,跳过,就是00 01的属性数,然后00 0C指向常量池的第12项即LineNumberTable,这是一个code属性,之后的00 00 00 12是属性长度,再后的00 04是line_number_info表的项数,接下来的4项(每项4字节)表示line_number_info,00 00 表示代码偏移量,00 05表示代码行号,后面的类似;10)最后是attributes_count和attributes,表明了类的属性,属性比较特殊,jvm定义了两种属性:SourceCode和InnerClass。
- 常量池各个标志解读(来源wikipedia) ,这里详解一下本例中的常量池,常量池是
cp_info
{
tag;
info[];
}
类似这样的结构,先有一个无符号byte作为tag标志,对应表格中的数据,额外的info字节数组存储对应的数据index.
我以表格的形式列出常量池的所有数据,应该算一目了然了吧:
-
常量index |
标志 |
标志内容 |
字节 |
具体数据 |
实际含义 |
1 |
07 |
00 07 |
2 |
class reference |
常量池第7项是该class的内容 |
2 |
07 |
00 10 |
2 |
class reference |
常量池第16项是该class的内容 |
3 |
0A |
00 02 00 04 |
4 |
method ref |
两个index,前两个字节表示池内的class索引位置,后两个字节是名字和类型描述 |
4 |
0C |
00 06 00 05 |
4 |
name & type |
就是第3项方法指向的名字和类型的index |
5 |
01 |
00 03 |
2+x |
x个utf-8字符,此处x=3 |
实际值:()V,表示type(第三项方法的类型,具体含义参见描述符定义) |
6 |
01 |
00 06 |
2+x |
x=6 |
实际值:<init>,表示name(第三项方法的名字) |
7 |
01 |
00 03 |
2+x |
x=3 |
实际值:Act,第一项指向的具体字符内容 |
8 |
01 |
00 08 |
2+x |
x=8 |
实际值:Act.java, |
9 |
01 |
00 04 |
2+x |
x=4 |
实际值:Code |
10 |
01 |
00 0D |
2+x |
x=0D=13 |
实际值:ConstantValue |
11 |
01 |
00 0A |
2+x |
x=0A=10 |
实际值:Exceptions |
12 |
01 |
00 0F |
2+x |
x=0F=15 |
实际值:LineNumberTable |
13 |
01 |
00 0E |
2+x |
x=0E=14 |
实际值:LocalVariable |
14 |
01 |
00 0A |
2+x |
x=0A=10 |
实际值:SourceFile |
15 |
01 |
00 0D |
2+x |
x=0D=13 |
实际值:doMathForever |
16 |
01 |
00 10 |
2+x |
x=10=16 |
实际值:java/lang/Object |
- 访问标志,待完善,这里有详细的spec
- 描述符的完整定义:非终结符是正常体,终结符是粗体,*号表示之前符号出现0或多次
FieldDescriptor |
FieldType |
ComponentType |
FieldType |
FieldType |
BaseType,ObjectType,ArrayType |
BaseType |
B,C,D,F,I,J,S,Z |
ObjectType |
L<classname>; |
ArrayType |
[ComponentType |
MethodDescriptor |
(ParameterDescriptor*)ReturnDescriptor |
ParameterDescriptor |
FieldType |
ReturnDescriptor |
FieldType,V |
- 基本类型终结符:V代表void
终结符 |
类型 |
B |
byte |
C |
char |
D |
double |
F |
float |
I |
int |
J |
long |
S |
short |
Z |
boolean |
- 一些描述符的例子:
描述符 |
字段或方法声明 |
I |
int a; |
[[J |
long[][] b; |
[Ljava/lang/Object; |
java.lang.Object[] c; |
Ljava/util/HashMap; |
java.util.HashMap map; |
[[[Z |
boolean[][][] ok; |
()I |
int m1(); |
()Ljava/lang/String; |
String m2(); |
([Ljava/lang/String;)V |
void main(String[] args); |
()V |
void m3(); |
(JI)V |
void m4(long a,int b); |
(Z[Ljava/lang/String;II)Z |
boolean m5(boolean a,String[] b,int c, int d); |
([BII)I |
int m6(byte[] a,int b,int c); |
- 声明字段时的字段表field_info:
类型 |
名称 |
数量 |
含义 |
u2 |
access_flags |
1 |
访问标志 |
u2 |
name_index |
1 |
字段简单名称的常量池utf8_info入口索引 |
u2 |
descriptor_index |
1 |
字段描述符的常量池utf8_info入口索引 |
u2 |
attributes_count |
1 |
attribute_info表的项数 |
attribute_info |
attributes |
attributes_count |
字段属性:ConstantValue, Deprecated, Synthetic(JVM规范) |
- field_info中的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 |
类 |
- 声明方法时的方法表method_info:
类型 |
名称 |
数量 |
含义 |
u2 |
access_flags |
1 |
访问修饰符 |
u2 |
name_index |
1 |
方法简单名称的常量池入口 |
u2 |
descriptor_index |
1 |
方法描述符的常量池入口 |
u2 |
attributes_count |
1 |
属性表的项数 |
attribute_info |
attributes |
attributes_count |
方法属性:Code,Deprecated, Exceptions和Synthetic(JVM规范) |
- method_info表中的访问标志对应含义:值得说明的是接口的方法一定是public和abstract的,接口初始化方法可以用strictFP
标志名 |
值 |
含义 |
使用范围 |
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_NATIVE |
0x0100 |
native |
类 |
ACC_ABSTRACT |
0x0400 |
abstract |
类和接口 |
ACC_STRICT |
0x0800 |
strictFP |
类和接口的<clinit>方法 |
- 类和接口的初始化方法(<clinit>)只有JVM可以直接调用,永远不会被java字节码直接调用。
- JVM规范定义的所有属性:
属性名 |
使用者 |
描述 |
Code |
method_info |
方法的字节码和其他数据 |
ConstantValue |
field_info |
final变量的值 |
Deprecated |
field_info,method_info |
字段或方法被禁用的指示符 |
Exceptions |
method_info |
方法可能抛出的可被检测的异常 |
InnerClasses |
ClassFile |
内部、外部类的列表 |
LineNumberTable |
Code_attribute |
方法的行号与字节码的映射 |
LocalVariableTable |
Code_attribute |
方法的局部变量的描述 |
SourceFile |
ClassFile |
源文件名 |
Synthetic |
field_info,method_info |
编译器产生的字段或者方法的指示符 |
- code属性的表code_attribute:
类型 |
名称 |
数量 |
含义 |
u2 |
attribute_name_index |
1 |
包含“Code”的常量池入口 |
u4 |
attribute_length |
1 |
去除起始6个字节后的code属性长度 |
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 |
code属性:LineNumberTable和LocalVariableTable(JVM规范) |
- 异常表excption_info:
类型 |
名称 |
数量 |
含义 |
u2 |
start_pc |
1 |
代码数组起始处到异常处理器起始处的代码偏移量 |
u2 |
end_pc |
1 |
代码数组起始处到异常处理器结束后的一个字节的偏移量 |
u2 |
handler_pc |
1 |
代码数组起始处跳转到异常处理器的第一条指令的偏移量 |
u2 |
catch_type |
1 |
异常处理器捕获的异常类型的Class_info常量池入口,如果为0则表示处理finally子句,即处理所有异常 |
- constantValue属性:各种基础类型加字符串类型的常量池入口查找,结构很简单,这里就不列出了。
- LineNumberTable属性建立了方法字节码流便宜量和源代码行号之间的映射关系:
类型 |
名称 |
数量 |
含义 |
u2 |
attribute_name_index |
1 |
包含“LineNumberTable”的常量池入口 |
u4 |
attribute_length |
1 |
去除起始6字节后的属性长度 |
u2 |
line_number_table_length |
1 |
line_number_info表长度 |
line_number_info |
line_number_table |
line_number_ table_length |
属性表 |
- line_number_info表很简单,就两个项:u2的start_pc给出新行开始时代码数组的偏移量,u2的line_number给出了从start_pc开始的行号。
这次就到这里。to be continued...