写博客真的是一件花费耐心的事情。
最近在学习Android ART虚拟机,Class文件是Java虚拟机的可执行文件是必不可少的前置知识点。
如果你想对Class文件有一定程度的了解,希望它会对你有所帮助。
Class文件格式总览
minor_version & major_version: 两个字节长度,分别代表class文件的小版本信息和大版本信息
constant_pool_count: 表示常量池数组中元素的个数,constant_pool是一个存储cp_info信息的数组,每一个class文件都包含一个常量池。常量池在代码中对应一个数组,其元素类型就是cp_info.
access_flags: 标明该类的访问权限,比如public、private等(容易联想到反射的setAccessible())
this_class & super_class: 存储的是指向常量池数组元素的索引。通过这两个索引和常量池对应元素的内容,可以知道本类和父类的类名
interfaces_count & interfaces: 同样是常量池数组里的索引号,表示该类实现了多少个接口以及接口类的类名
fields_count & fields: 表示成员变量的数量和信息。其中成员变量的信息由field_info结构体表示
methods_count & methods: 表示成员函数的数量和信息,其中成员函数的信息由method_info结构体表示
attributes_count & attributes: 该类包含的属性信息。属性信息由attributes_info结构体表示。
u4:表示长度为4个字节 ,同理u2代表两个字节
常量池及相关内容
常量池的类型和关系
虚拟机中的常量池对应的数据结构类型为cp_info的数组。每一个cp_info对象存储了一个常量项。cp_info的伪代码如下:
cp_info {
u1 tag; //每一个cp_info的第一个字节表明该常量项的类型
u1 info[]; //第二个字节表示常量项的具体内容
}
常量项的类型如下图所示(tag类型):
本来想找个网图偷个懒,但是发现网图都有描述不清的歧义,尤其是CONSTANT_String
注意CONSTANT_String 和CONSTANT_Utf8的区别
常见常量项的内容:
Utf8、Class等对应的数据结构:
CONSTANT_Utf8_info{ CONSTANT_Class_info{ CONSTANT_Fiedldref_info{
u1 tag; u1 tag; u1 tag;
u2 length; u2 name_index; u2 class_index;
u1 bytes[length]; } u2 name_and_type_index;
} }
CONSTANT_String_info{ CONSTANT_MethodType_info{ CONSTANT_Methodref_info{
u1 tag; u1 tag; u1 tag;
u2 string_index; u2 descriptor_index; u2 class_index;
} } u2 name_and_type_index;
}
Long、Integer等对应的数据结构:
CONSTANT_Long_info{ CONSTANT_Integer_info{
u1 tag; u1 tag;
u4 high_bytes; u4 bytes;
u4 low_bytes; }
}
CONSTANT_Double_info{ CONSTANT_Float_info{
u1 tag; u1 tag;
u4 high_bytes; u4 bytes;
u4 low_bytes; }
}
我们以CONSTANT_Utf8_info为例子:
其中length表示bytes数组的长度,而bytes成员则真正存储字符串的内容。Class等对应的数据结构中的index实际上都是指向常量池中CONSTANAT_Utf8_info元素的索引。
和String_info一样,Class_info里面的name_index、MethodType_info里面的descriptor_index、NameAndType_info里面的name_index和descriptor_index都代表一个指向类型为Utf8_info元素的索引。
为什么在Double_info、Integer_info等结构体中直接存储数据,而String_info、Class_info等需要采用这种间接元素索引的方式呢?
原因很简单,为了节省Class文件的空间,我们举个例子。
public class Sample{ CONSTANT_Fieldref_info{
public String name; 这个Sample.java编译成Sample.class后 u1 tag;
public String hobby; 将包含两个CONSTANT_Fieldref_info u2 class_index;
} u2 name_and_type_index
}
(1)当class_index不再是索引的时候,要存储类的信息Sample
(2)name_and_type_index也不再是索引,而是name和hobby的具体内容:"name"和"Ljava/lang/String;"
"hobby"和"Ljava/lang/String;" name和hobby是变量名字,Ljava/lang/String;是变量数据类型的字符串表示
显然上面就出现了冗余信息,Ljava/lang/String; 和 Sample就多存了一份。
信息描述规则
根据Java虚拟机规范,如何用字符串换来描述成员变量、成员函数是有讲究的,这些规则主要集中在数据类型,成员变量和成员函数的描述:
(1)数据类型(比如原始数据类型,引用数据类型)的描述规则
(2)成员变量的描述规则,规范里面称作Field Descriptor
(3)成员函数的描述规则,规范里面称作Method Descriptor
数据类型描述规则
(1)原始数据类型对应的字符串描述为B C D F I J S Z,他们分别对应的Java类型为byte、char、double、float、int、long、short、boolean.
(2) 引用数据类型的格式为LClassName。此处的ClassName为对应类的全路径名,比如刚才提到的Ljava/lang/String。全路径名的"."号由"/"替代,并且最后必须带分号。
(3)数组也是一种引用类型,数组用"[其他类型的描述名",比如int数组为"[I",字符串数组就是""[Ljava/lang/String;"
成员变量的描述规则
成员变量 Field Descriptor的描述类型就是前面说的数据类型,举个例子:
FieldDescriptor:
FieldType #FieldDescriptor描述规则只包含FieldType的一种信息
FieldType: #FieldType描述的信息由这三个组成 BaseType、ObjectType、ArrayType
BaseType: #原始数据类型 B C D F I J S Z
ObjectType: #引用类型 LClassName
ArrayType: #数组类型,由[加Componentpe构成
[Componentpe: #是一个新东西(暂时知道这个东西就好了)
ComponentType: #定义ComponentType,他由上面定义的FieldType构成
FieldType
成员函数的描述规则
和成员变量的描述略有不同,一个成员函数(Method Descriptor)需要包括 返回值 及 参数的数据类型
MethodDescriptor:
(ParameterDescriptor*)ReturnDescriptor #参数类型 和 返回值类型 *代表0到多个参数
ParameterDescriptor: #参数类型的描述就是FieldType
FieldType
ReturnDescriptor: #返回值数据类型描述。如果为void 用VoidDescriptor描述,否则也是FieldType
VoidDescriptor: #v代表void
V
举个例子:
System.out.print(String str)函数,他的Method Descriptor将是:(Ljava/lang/String;)V
注意:发现没有?Method Descriptor是不包括函数名字的。这么做目的也是为了节省空间。
因为很多函数可能名字不同,但是Method Descriptor可能是一样的。
常量池实例剖析
直接编译一个Class文件来看看常量池的内容
Java源文件:
/**
* Created by zhaoyuanchao on 2020/3/16.
*/
public class Person {
private String name;
private String job;
private int age;
private double money;
private boolean isBoy;
public Person() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getJob() {
return job;
}
public void setJob(String job) {
this.job = job;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public boolean isBoy() {
return isBoy;
}
public void setBoy(boolean boy) {
isBoy = boy;
}
}
Person.java
1.javac Person.java
2.javap -verbose Person.class
解析后的Class文件中常量池的内容为:
Constant pool:
#1 = Methodref #8.#41 // java/lang/Object."<init>":()V
#2 = Fieldref #7.#42 // com/example/glideresource/Person.name:Ljava/lang/String;
#3 = Fieldref #7.#43 // com/example/glideresource/Person.job:Ljava/lang/String;
#4 = Fieldref #7.#44 // com/example/glideresource/Person.age:I
#5 = Fieldref #7.#45 // com/example/glideresource/Person.money:D
#6 = Fieldref #7.#46 // com/example/glideresource/Person.isBoy:Z
#7 = Class #47 // com/example/glideresource/Person
#8 = Class #48 // java/lang/Object
#9 = Utf8 name
#10 = Utf8 Ljava/lang/String;
#11 = Utf8 job
#12 = Utf8 age
#13 = Utf8 I
#14 = Utf8 money
#15 = Utf8 D
#16 = Utf8 isBoy
#17 = Utf8 Z
#18 = Utf8 <init>
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 getName
#23 = Utf8 ()Ljava/lang/String;
#24 = Utf8 setName
#25 = Utf8 (Ljava/lang/String;)V
#26 = Utf8 getJob
#27 = Utf8 setJob
#28 = Utf8 getAge
#29 = Utf8 ()I
#30 = Utf8 setAge
#31 = Utf8 (I)V
#32 = Utf8 getMoney
#33 = Utf8 ()D
#34 = Utf8 setMoney
#35 = Utf8 (D)V
#36 = Utf8 ()Z
#37 = Utf8 setBoy
#38 = Utf8 (Z)V
#39 = Utf8 SourceFile
#40 = Utf8 Person.java
#41 = NameAndType #18:#19 // "<init>":()V
#42 = NameAndType #9:#10 // name:Ljava/lang/String;
#43 = NameAndType #11:#10 // job:Ljava/lang/String;
#44 = NameAndType #12:#13 // age:I
#45 = NameAndType #14:#15 // money:D
#46 = NameAndType #16:#17 // isBoy:Z
#47 = Utf8 com/example/glideresource/Person
#48 = Utf8 java/lang/Object
一个常量池的例子,把前面所说的点都穿在了一起。
举个例子说一下吧:第三行
#2 = Fieldref #7.#42 // com/example/glideresource/Person.name:Ljava/lang/String;
对应前面的数据结构为
CONSTANT_Fieldref_info{
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
class_index的索引 指向 #7 为:
#7 = Class #47 // com/example/glideresource/Person
然后又索引 又指向#47 为:
#47 = Utf8 com/example/glideresource/Person 实际的具体值
由于name_and_type_index指向代表NameAndType元素的索引。 所以我们看下#42 指到哪里去了:
果然是NameAndType:
#42 = NameAndType #9:#10 // name:Ljava/lang/String;
CONSTANT_NameAndType_info{
u1 tag;
u2 name_index;
u2 descriptor_index;
}
name_index 的索引为 #9 定点爆破到:
#9 = Utf8 name
descriptor_index的索引为 #10 定点爆破到:
#10 = Utf8 Ljava/lang/String;
field_info 和 method_info
先来看下两种属性的数据结构吧:
field_info{
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
mothod_info{
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
(1) access_flags为访问标志,成员变量和成员函数的访问标志略有不同,下面会介绍到
(2) name_index 为指向成员变量或成员函数名字的Utf8_info常量项
(3) descriptor_index 也指向Utf8_info,分别是描述成员变量的FieldDescriptor和描述成员函数的MethodDescriptor.
(4) attribute 属性信息。
access_flags介绍
在Java中 类、类的成员函数、类的成员变量都有访问=控制设置,比如public private等。 这些都会转换成access_flags
Class的access_flags取值
Field的access_flag取值:
Method的access_flag取值:
属性介绍
属性概貌
属性可用attribute_info数据结构的伪代码表示
attribute_info{
u2 attribute_name_index; //属性名称,指向常量池中Utf8常量项的所以
u4 attribute_length; //该属性具体内容的长度,即info数组的长度
u1 info[attribute_length]; //属性的具体内容
}
属性是由其名称来区别的,即attribute_info中的attribute_name_index所指向的Utf8字符串
一些重要的属性名称和它们的作用:
(2)不同类型的属性可能出现在ClassFile中不同的成员里
(3)属性也可以包含子属性,比如Code属性能包含LocalVariableTable属性
Code属性
函数的源码转换后得到的字节码就存储在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];
}
attribute_name_index: 指向内容为Code的Utf8常量项。attribute_length表示接下来内容的长度
max_stack: JVM执行一个指令的时候,该指令的操作数存储在一个名叫"操作数栈"的地方,每一个操作数占用一个或两个栈顶。stack就是一块只能进行先入后出的内存。max_stack用于说明这个函数在执行过程中需要最深多少栈的空间。
max_locals: 表示该函数最多包括几个局部变量。
code_length 和 code: code 存储java编译后的指令码,code_length表明其长度。
exception_table_length 和 exception_table: 一个函数可以包含多个try/catch语句,一个try/catch语句对应exception_table数组中的一项。
start_pc: 描述try语句从哪条指令开始。 end_pc: 表示这个try语句到哪条指令结束 注意不包含catch
handler_pc: 表示catch语句从哪条指令开始
catch_type: 表示catch中截获的Exception或Error的名字,指向Utf8_info常量项。
另外Code_atrribute还可能包含其他属性:
(1) LineNumberTable:用于调试,比如指明哪条指令。对应源码的哪一行。
(2) LocalVariableTable:用于调试,可以用来计算本地变量的值。
(3) LocalVariableTypeTable: 功能和LocalVariableTable类似。
举个例子(为了凸显行号我直接截图了)
对应生成的Code属性如下:
{
public com.example.glideresource.Sample();
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 6: 0
public java.lang.String getTitle();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field title:Ljava/lang/String;
4: areturn
LineNumberTable:
line 11: 0
public void setTitle(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #2 // Field title:Ljava/lang/String;
5: return
LineNumberTable:
line 15: 0
line 16: 5
public int getAge();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #3 // Field age:I
4: ireturn
LineNumberTable:
line 19: 0
public void setAge(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: iload_1
2: putfield #3 // Field age:I
5: return
LineNumberTable:
line 23: 0
line 24: 5
}
以getTitle()方法为例子:
descriptor:()Ljava/lang/String; #返回值为String类型的无参方法
flags:ACC_PUBLIC #access_flags 为Public
stack = 1, locals = 1,args_size = 1 #函数栈空间需要1
#包含一个局部变量
LineNumberTable:
line 11:0 #对应start_pc 对应源码11行 retuen title;
结
《深入理解Android Java虚拟机ART》这本书应该是我买过最厚的一本android书了。看书整理笔记也是一个学习和巩固知识的途径吧,我不知道这本书我可以坚持更多少篇博客,但是尽可能坚持下去吧。提升也是个积累的过程。
下一篇-关于Dex的文件格式 待续