目录:
我们都知道,java代码经过编译之后,会转换为字节码。那么
- 字节码是什么?他的结构是怎么样的?
- 字节码是怎么被执行的?
这些问题本文都尝试去回答。
1.字节码是什么
字节码是什么,当我们编译java程序之后,便会生成.class文件,使用文本编辑器打开.class文件。
你会看到一大堆数字,不过你仔细观察会发现前8个字符居然是“cafe babe”,咖啡宝贝。这让人想起java的logo也是咖啡,而且java本身其实是一种咖啡的名字。事实上“cafe babe”是字节码文件的标识符,也被称为魔数,因为实际上他是一个16进制的数,而不是字符。在Linux系统中,识别一个文件并不是靠检验文件的后缀名,而是检验他的标识符。
另外值得注意的是,并不是一个.java文件编译成一个.class文件,而是一个类编译成一个.class文件。
那么除了开头的魔数,.class文件的结构是怎么样的呢?
class的文件结构
总体结构
class文件其实只有一种数据结构,就是无符号数,但是多个无符号数可以凑成不同的类型。根据无符号数的个数,分为U1,U2,U3,U4的等四种类型。另外由多个无符号数类型(指U1,U2,U3,U4)组成的数据结构称之为表。所以实际上整个class文件就是一个表。
- 魔数和版本
魔数在上面已经介绍过了。
版本号标记的是字节码的版本,并不是当前文件的版本。低版本的执行器会拒绝执行高版本的字节码文件。 - 常量池
常量池用于存放字面量值,如int a=1;那么1就会被存放在常量池中。另外字符串池存放的位置是在堆中。
此外,还会存放符号引用,所谓符号引用就是被引用的类的全称。符号引用会在类加载的时候被解析成实际引用类的地址。 - 访问标记
- 表示是类,接口,抽象类,枚举,注解
- 是否为public
- 是否为final
多个结果使用&合并。
- 当前类索引(this)
当使用this时,指向的就是该属性记录的地址,指向当前类的地址。 - 父类索引(super)
指向父类的地址,使用super时指向的就是该地址。 - 接口集合
指向实现的接口的地址索引。因为接口可多实现,所以是集合。
字段表集合
描述类中声明的变量,以及实例变量,不包括方法内的局部变量。
字段表结构
名称 | 类型 | 说明 |
---|---|---|
访问标记符 | U2 | public/private/protected/static/final/volatile/transient/enum |
name_index | U2 | 对常量池的引用,简单名称 |
descriptor_index | U2 | 描述符,表示字段的类型,B:byte,C:char,D:double,F:float,I:int,J:long,S:short,Z:boolean,V:void,L:对象类型,如Ljava/lang/Object |
attributes_count | U2 | 属性的个数 |
attributes | attribute_info | 属性表,与class的属性表集合是一样的 |
方法表集合
名称 | 类型 | 说明 |
---|---|---|
access_flags | U2 | 访问标记符,比字段的访问标记符多出synchronized,native,strictfp(精确浮点数),abstract,少了volatile,transient |
name_index | U2 | 名称索引,指向方法具体实现 |
descriptor_index | U2 | 先参数列表属性,再描述返回值属性 |
attributes_count | u2 | 属性个数 |
attributes | attributes | 属性几个 |
方法集合里标记了方法的访问标记,返回值参数类型描述,但是方法的代码存再哪里?方法实际存储在属性表里的“Code”属性里。
属性表集合
可以看出,在很多结构里都使用到了属性表这一个属性,属性表没有顺序要求,只要不与已有的属性名重复即可。
名称 | 使用位置 | 说明 |
---|---|---|
Code | 方法表 | 存放具体的方法实现,实际是java代码编译的字节码指令 |
ConstantValue | 字段表 | 存放final标记的常量 |
Deprecated | 类,方法表,字段表 | 存放被标记遗弃的字段方法或类 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当类为局部类或者匿名类时拥有的属性,用于标记该类所在的外围方法。 |
其他过多的属性不列出
2.字节码指令
字节码的执行
在上面的Code属性表中,实际存放代码的属性只有Code一个U1的属性。这令人有点迷惑,一个字节怎么存放代码呢?
实际上完整的指令由1个字节长度表示操作指令,以及紧跟其后的0个或n个参数构成。
java是面向操作栈的结构,大多数指令并不包括操作的数据。
如果不考虑异常,其执行代码的逻辑大致为以下的伪代码。
do{
自动计算PC寄存器的值加1;
根据PC寄存器指示的位置,从字节码流中取出操作指令;
if(字节码存在参数)
从字节码流中取出参数;
执行操作码所定义的操作;
}while(字节码流长度>0);
由于Code属性的类型是U1,所以实际上,操作码的总数不能操作256条。
字节码类型
字节码使用一个字母来表示类型
- l:long
- s: short
- b: byte
- c: char
- f: float
- d: double
- a: reference,表示引用类型
九种类型的操作指令
类型 | 说明 | 例子 |
---|---|---|
加载 | 用于将局部变量表和操作数栈之间的数据传输 | iload(i表示int型,加载到操作数栈),istore(存储的局部变量),bipush,sipush等等 |
运算 | 计算数值 | iadd,isub,imul,irem(取余),ior,iand,ixor,iinc(自增)等等 |
类型转换 | 转换类型 | i2s(int转shrot),i2b(int转byte)等等 |
对象创建和访问 | new(创建实例),newarray(创建数组)等等 | |
操作数栈管理和访问指令 | pop,dup,swap(交换栈顶的两个数)等等 | |
控制转移指令 | 条件分支的跳转,如if语句 | ifeq(相等),iflt(小于),goto等等 |
方法调用和返回指令 | invokevirtual(调用实例方法),invokeinterface(接口方法),invokespecial(特殊方法,如初始化),invokestatic(静态方法),invokedynamic(动态方法)。ireturn(返回int) | |
异常处理指令 | 抛出异常,catch并不使用指令而是使用异常表查找,Code属性中的expection_info | athrow |
同步指令 | 当一个方法被标记为同步方法时,或者标记一段代码为同步代码块时,执行到同步代码时,线程会从方法表结构中的ACC_SYNCHRONIZED,标志是否被设置,如果被设置了,则必须等待,直到成功设置获得管程。执行代码后会释放管程。 | monitorenter,monitorexit |