Jvm学习笔记(三) class文件分析
Class文件结构
所有的java代码都会被编译出.class文件交给虚拟机装载执行,从而实现平台无关,一次编译到处执行的效果。
Class文件一组以字节为基础单位的二进制流,各个数据严格按照顺序紧凑的排列,没有任何分隔符,对于超过一个字节的数据采用大尾端的字节序。
表和无符号数
class文件中只有两种数据类型:表和无符号数。
无符号数属于基本数据单位,有u1\u2\u4\u8代表n个字节的无符号数,它可以用来描述数字、索引引用、数量值或者按照UTF-8构成的字符串值。
表是由其他表和无符号数组成的复合数据类型,习惯性以_info结尾,表用于描述复合层次的数据结构。
无论是表还是无符号数,当当需要的数量不确定时,一般会在前置处加一个表示count的无符号数(类似RIFF协议中表示data区域大小的size字节)。
这是整个.class文件的数据结构,class文件会严格按照表格中的数据格式组成。
类型 | 名称 | 数量 |
---|---|---|
u4 | magic(魔数) | 1 |
u2 | minor_version(次版本号) | 1 |
u2 | major_version(主版本号) | 1 |
u2 | constant_pool_count(常量池数据长度) | 1 |
cp_info | constant_pool(常量池) | constant_pool_count-1 |
u2 | access_flags(访问标志) | 1 |
u2 | this_class(类索引) | 1 |
u2 | super_class(父类索引) | 1 |
u2 | interfaces_count(接口数据长度) | 1 |
u2 | interfaces(接口索引) | interfaces_count |
u2 | fields_count(字段表长度) | 1 |
field_info | fields(字段表) | fields_count |
u2 | methods_count(方法表长度) | 1 |
method_info | methods(方法表) | methods_count |
u2 | attributes_count(属性表长度) | 1 |
attribute_info | attributes(属性表) | attributes_count |
除了自己解析二进制外,官方还给我们提供了一个分析class文件的工具 ,只要使用javap -verbose XXXX
就可以解析出该文件的组成:
public class Test{
public static void main(String[] args){
System.out.println("Hello world");
}
}
使用javap命令以后:
magic(魔数)
魔数相当于协议标志为,表示这个文件是一个class文件,class文件的前四个字节固定为0xCAFFBABE。者也是为什么java的商标是一杯咖啡的缘故。
minor_version/major_version (版本号)
接下来四个字节表示了Class文件的版本号,5-6是次版本号,7-8是主版本号。主版本号从45开始,次版本号范围是0-65535 。虚拟机可以执行低版本的class文件但是不能执行高版本的文件。
constant_pool_count / constant_pool (常量池)
跟着版本号的就是常量池入口。常量池相当于class文件的资源仓库,class文件中其他的项目很多最终都关联到了常量池,可以这样理解,class文件就是一个常量池+一堆指向常量池的索引组成。由于常量池中数量是不固定的,所以需要一个count指定有多少常量。这里有一个要注意的点,常量池索引是从1开始,第0位留出有另外的作用。
常量池内主要存放两种数据类型:字面量和符号引用。字面量类似于常量,符号引用属于编译原理的概念。由于常量池中的数据类型是在太复杂,有17种,每种又都不同,所以就不在这边展开讲述了,我就说一下我对其中的一些理解。
一个Class文件的常量池数据,第一项是一个u1数据,代表了这个常量的类型,而接下来的数据,代表了不同的含义,这些含义有的可能是普通无符号数据,有的代表了索引,要根据这个常量的类型去判断,然后根据字节数据解析,比如也许是字符串,那么就会索引向一个字符串的常量,如果是一个索引,则重新定位到常量池中去寻找对应的数据,并按照此时的数据结构解析下一轮,也就是说常量池中的数据会出现不停的索引直到真正的数据为止。
access_flag(访问标志)
表示这个class是一个类还是接口、是否public、是否声明为final等标志。
this_class(类索引)
表示这个类的全限定名,指向常量池中的一个CONSTANT_Class_info类型,在这个类型中能找到对应的类名称。
super_class(父类索引)
表示这个类父类的全限定名,同上。
interfaces(接口索引)
接口索引是一个集合,先根据一个count确定实现的接口数量,然后就是指向常量池中的接口全限定名。
fields(字段表)
字段表用于描述class文件中定义的类级变量和实例级变量,但是不包括方法中的局部变量。字段表的结构
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flag (访问标志,类似class中的访问标志) | 1 |
u2 | name_index (简单名称,指向常量池的索引) | 1 |
u2 | descriptor_index (描述符,指向常量池,表示变量或者方法的类型描述,用一个人大写字母表示) | 1 |
u2 | attrbutes_count | 1 |
attrbutes_info | attrbutes (属性表,后面会详细介绍) | attrbutes_count |
methods(方法表)
方法表和字段表非常类似,包括访问标志、简单名称、描述符、属性表等,只是在具体属性上会与字段表有一点点差异。
attributes(属性表)
属性表在整个class文件中出现过多次,它在class文件、字段表、方法表后面都会存在,不严格按照顺序和长度,任何编译器都可以在里面加入自己的属性,虚拟机会忽略掉它不认识的属性。
属性表的默认类型很多,具体介绍几种:
属性名称 | 使用 | 含义 |
---|---|---|
Code | 方法表 | 方法代码编译成的字节码指令 |
ConstantValue | 字段表 | 由final定义的常量值 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名内部类才拥有的属性,用于标识这个类所在的外围方法 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | 源码行数和字节码指令之间的对应关系,编译器根据这个定位异常行数 |
LocalVariableTable | Code属性 | 方法局部变量描述 |
Signature | 类、方法、字段 | 范型的类型签名,避免类型擦除以后的出现问题 |
Code属性
Code属性出现在方法表之后的属性表中,方法被编译后的字节指令就存放在code属性中,只有那些有具有实现的方法才会有code表,抽象方法编译后没有。Code表的结构是:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index(该属性名称,指向常量池索引) | 1 |
u4 | attribute_length (该属性长度) | 1 |
u2 | max_stack (操作数栈最大深度) | 1 |
u2 | max_locals (局部变量所需存储空间,单位是Slot,int之类的占有一个Slot) | 1 |
u4 | code_length (字节码长度) | 1 |
u1 | code (字节码指令) | code_length |
u2 | exception_info_length (异常列表长度) | 1 |
exception_info | exception_table (方法可能抛出的异常) | exception_info_length |
u2 | attributes_count | 1 |
attribute_info | attributes (属性表) | attributes_count |
可以看到,有一些数据会因为类型的限制产生影响,比如之类最多只有256种,指令字节码最多只有65535条等。再次通过javap命令解析class文件,这是源码:
public class Test{
public static void main(String[] args){
System.out.println("Hello ");
Test test=new Test();
test.hello(" World");
}
public void hello(String hello){
System.out.println(hello);
}
}
这是class文件:
可以看到hello方法表,其中args_size=2,和源码不符合,原因在于方法会把调用对象this作为第一个参数隐式传入,方法和函数的区别就在于此。其他的一些属性就不详细介绍了,大家可以利用javap -verbose
命令去解析class文件。
字节码指令
java虚拟机的指令是由一个字节、代表某种特定操作含义的数字(操作码)以及跟随其后的0到n个代表此操作需要的参数(操作数)构成。java虚拟机是基于操作数栈道。
指令对齐
由于java放弃了字节对齐,有时候需要一个以上字节表示的数据可以用位运算转换,比如2个字节的数字可以用 (byte1<<8)|byte1
来表示,牺牲了一点运行速度,但是节省了大量字节对齐导致的内存开销。
基础模型
字节码的运作模型可以按照以下的流程来看:
do{
自动计算PC寄存器+1;
根据PC寄存器位置从字节码流中取出操作码;
if(字节码存在操作数) 从字节码流中取出操作数;
执行操作码所定义的操作;
}while(字节码长度>0);
所有的操作码都定义了对应的操作数类型,详细还是请单独查询操作码的表。
结语
Class文件远比我介绍的复杂,其中可以展开的表格结构实在太多了,还有比如实例构造器()和类构造器()我都没有讲 谁让我是懒狗 这章主要还是粗粮引导一下class文件的结构以及怎么去查看分析一个class文件,书上可是说能够根据class文件分析问题是java开发的基础呢,离谱,感谢大家的观看,希望可以点个==赞==!
相关文章
转载自:https://juejin.cn/post/7216587974007242812