第6章 类文件
6.3 Class类文件的结构
Class文件是一组以8位字节为基础单位的二进制流。
Class文件格式采用一种类似C语言结构伪结构存储数据,这种伪结构中只有两种数据类型:无符号数和表
无符号数属于基本的数据类型以u1,u2,u4,u8来分别代表一个字节、2个字节,4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字条串值。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表习惯性地以 _info 结尾。
表 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 | interface_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | field_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
1 魔数与Class的版本
public class Test { private int m; private int inc(){ return m + 1; } }
如上类编译后的类文件,通过UE打开后:
前四位为魔数 : CAFEBABE
第五六位 ( 00 00 )为 次版本号( Minor Version)
第7 8 位 (00 32) 为主版本号( Major Version) 十六进制 0x0032为十进制 50 表明可以被JDK1.6以上的版本执行(JDK1.1能支持45.0~45.65535,JDK1.2能支持 45.0~46.65535,JDK1.5一般为48, JDK1.6一般为50,需确认)
2 常量池
紧接着主版本后是 常量池个数u2 与常量池 cp_info, 常量池可以理解为Class文件之中的资源仓库。常量池中常量的数量为常量池个数-1。
常量池中主要存放两在类常量:字面量(Literal)和符号引用(Symbolic References)。每一个常量都是一个表,JDK1.7共有14种不同的表结构(CONSTANT_Utf8_info(1), CONSTANT_Integer_info(3),CONSTANT_Float_info(4),CONSTANT_Long_info(5),CONSTANT_Double_info(6), CONSTANT_Class_info(7)等),每一种常量都有自己的类型,每种类型有一个共同的特点,每个结构的第一位是一个u1类型的标志位(tag)。
如图所示:常量池个数 (00 16) 为16进制0x0016转为10进制22,代表常量池中有21个常量。
0x07为第一个常量的类型中的 tag ,查到类型为CONSTANT_Class_info(7) , CONSTANT_Class_info的结构如下:
类型 | 名称 | 数量 |
u1 | tag | 1 |
u2 | name_index | 1 |
0x07后面跟两个字节: 0x0002表示 name_index; name_inde是一个索引值,它指赂常量池中一个CONSTANT_Utf8_info的常量,此常量代表了这个类(或者接口)的全限定名。此处为 0x0002表示指向了常量池中的第二项常量。
下一字段为 01,为第二个常量的tag,0x01表示该常量为CONSTANT_Utf8_info类型,CONSTANT_Utf8_info类型结构如下:
类型 | 名称 | 数量 |
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
length表示该UTF-8编码的字条长度是多少字节,如下图
0x0004表示字节长度为 4字节 0x54657374表示为 Test
剩余的19个常量可以使用javap工具来分析:
21个常量中有很多是没有在Java代码中直接出现过的,它们会被后面即将讲到的字段表(field_info),方法表(method_info),属性表(attribute_info)引用到。
3 访问标志
再往下两字节为访问标志 access_flag,用于识别类或者接口层次的访问信息 包括:这个Class是类还是接口,是否定义public类型
4 类索引 父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合。
他们的数值都会指向常量池中的位置:
this_class的值为0x0001指向常量池中的第一个 const #1=class #2
super_class的值为0x0003指向常量池中的3
5字段表集合
用于描述接口或类中声明的变量,可以包括的信息有:字段的作用域(public, private, protected) 是实例变量还是类变量(static修饰符),可变性(final),并发可见性(volatile修饰,是否强制从主内在读写),可否被序列化,字段数据类型(基本类型、对象、数组),字段名称等。
字段结构:访问标志(access_flags) u2, 名称索引( name_index) u2, 描述符索引(descriptor_index)u2,属性个数(attributs_count) u2, 属性表集合(attributs) attributs_count
描述符的作用是用来描述字段的数据类型、方法参数列表(包括数量、类型以及顺序)和返回值。基本数据类型(byte,char, double, float, int, long, short, boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型用字符L加对象的全限定名来表示。
B-bype, C-char, D-double, F-float, I-int, J-long, S-short,Z-boolean, V-void, L-对象类型
对于数组,每一组度前置一个[来描述 java.lang.String[][] --> [[java/lang/String; int[] ---> [I
6方法表集合
与字段表集合基本相同,仅在访问标志和属性表集合的可选荐中有区别。
方法的java代码经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为 Code的属性里面。
7 属性表集合
在Class文件、字段表、方法表都可以携带自己的属性表集合,用于描述某些场景专有的信息
6.4 字节码指令简介
1 字节码与数据类型
在java虚拟机的指令集中,大多数指令都包含了其操作所对应的数据类型信息,例如:iload指令用于从局部变量表中加载int型数据到操作数栈.
操作码助记符中都有特殊的字条来表明专门为哪种数据类型服务, i:int, l:long, s:short, b:byte, c:char, f:float, d:double, a:reference.
Java虚拟机的操作码长度只有一个字节,如果每一种数据类型相关的指令都支持Java虚拟机所有运行时数据类型的话,那指令的数据恐怕就会超过一个字节所能表示的数量范围了。因此,Java虚拟机的指令集对于特定的操作只提供了有限的类型相关指令去支持它。
2 加载和存储指令
加载和存储指令用于将数据在栈帧中的局部变量表和操作数据栈之间来回传输
将一个局部变量加载到操作栈: iload, iload_<n>, lload, fload .....
将一个数值从操作数栈存储到局部变量表: istore, istore_<n>....
将一个常量加载到操作数栈: bipush, sipush, ldc.....
扩充局部变量表的访问索引的指令: wide.
3 运算指令
运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。由于没有直接支持byte,short, chart和boolean类型的算术指令,对于这类数据的运算,应使用操作int类型的指令代替。
加法: idadd, ladd, fadd, dadd.
减法:isub, lsub, fsub, dsub,
乘法:imul, lmul, fmul, dmul
除法:idiv, ldiv, fdiv, ddiv
求余: irem, lrem, frem, drem
取反: ineg, lneg, fneg, dneg
位移: ishl, ishr, iushr, lshl, lshr, lushr
按位或指令: ior, lor
按位与指令: iand, land
按位异或指令: ixor, lxor
局部变量自增指令: iinc
比较指令: dcmpg, dcmpl, fcmpg, fcmpl, lcmp
4 类型转换指令
类型转换指令可以将两种不同的数值类型进行想到转换
Java虚拟机直接支持(即转换无需显式的转换指令)以下数据类型的宽化类型转换(Widening Numeric Conversions,即小范围类型向大范围类型的安全转换。 1、int类型到long, float 或者 double类型, 2、long类型到float, double类型, 3、float类型到double类型
相对的,处理窄化类型转换( Narrowing Numeric Conversions)时,必须显式地使用转换指令来完成,这些转换指令包括:i2b, i2c, i2s........
5 对象创建与访问指令
创建类实例的指令: new
创建数组的指令: newarray, anewarray, multianewarray
访问类字段(static字段)和实例字段(非static字段,或者称为实例变量)的指令: getfield, putfield, getstatic, putstatic
把一个数组元素加载到操作数栈的指令: baload, caload, saload, iaload, laload......
将一个操作数栈的值存储到数组元素中的指令: bastore, castore,sastore....
取数组长度的指令: arraylength
检查类实例类型的指令: instanceof, checkcast
6 操作数栈管理指令
java虚拟机提供了直接操作操作数栈的指令。
将操作数栈的栈顶一个或两个元素出栈: pop, pop2
复制栈顶一个或两个数值并将复制值或双份的复制值 重新压入栈顶:dup, dup2, dpu_x1....
将栈最顶端的两个数值互换: swap
7 控制转移指令
条件分支:ifeq, iflt, ifle....
复合条件分支: tableswitch, lookpuswitch
无条件分支: goto, toto_w....
8方法调用和返回指令
invokevirtual指令用于调用对象的实例方法。
invokeinterface 用于调用接口方法
invokespecial
invokestatic
invokedynamic 用于在运行时动态解析出调用点限定符所引用的方法
9异常处理
在Java程序中显式异常的操作(throw语句)都由athrow指令实现
10 同步指令