Java字节码文件是二进制的,今天教你怎么直接读,是不是很炫酷呢?
1、字节码规范的重要性
上一篇文章咱们讲到java虚拟机的输入是class文件,不是源文件。并且目前除了java外有其他语言编译后的结果是class字节码文件,同样能用java虚拟机运行。
也就是说,只要满足java虚拟机class文件规范,就能使java虚拟机正常运行,由此可见,这个规范有多么重要了吧
2、Class文件格式
Class文件结构不像xml等描述语言那样松散。它没有任何分隔符号。所有数据项无论是顺序还是数量,都是被严格限定的。哪个字节代表什么含义,长度是多少,先后顺序怎么样,都不允许改变。下面的图是从JVM虚拟机规范一书中截出来的ClassFile结构
下面讲解下各个字段的含义:
magic: 标识class文件格式,值固定为0xCAFEBABE
minor_version,major_version:记录产生该class文件的编译器的副和主版本号
constant_pool_count: 常量池中的常量个数
constant_pool:常量池,存放常量的
access_flag:该类或接口声明中使用的修饰符
this_class: 该Class文件定义的类或者接口
super_class:该Class文件父类定义的类或者接口
interface_count:该类中接口的个数
interfaces[]:接口描述数组
fields_count:字段个数
fields[]:字段描述数组
methods_count:方法个数
methods[]:方法描述数组
attributes_count:属性个数(属性是用来提供附加描述性信息的,注意和field作区分)
attributes[]:属性描述数组
3、阅读字节码
3.1、字节码文件的模样
用以下的简单java代码为案例:
package com.restart;
public class Hello {
private static String msg = "Welcome";
public static void main(String[] args) {
System.out.println("msg=" + msg);
}
}
编译该文件,得到class文件。使用010 Editor打开class文件,如下:
接下来我们要做的就是一步步读懂这堆数据的含义,是不是挺期待,我们马上开始
3.2、魔数和版本号
前四个字节(u4) cafe就是Class文件的魔数。第5、6个字节(u2)是Class文件的副版本号,第7、8个字节是主版本号。十六进制34,也就是52,对应JDK1.8
3.3、常量池
在常量池表结构前的两个字节,代表常量池的长度。如上图中十六进制的34,即52。而因为常量池中默认会有一个常量(0:代表不引用常量)占据了一个位置,所以实际表中存放有数据的常量为51个。每一个表项的第一个字节是u1类型,代表数据类型。具体含义如下:
以第一项0A 000C 001D为例,0A代表该常量是CONSTANT_Methodref_info类型,紧接着一个u2类型指向声明方法的类描述符索引,000C代表指向了第12个常量, 再往后一个u2类型指向名称及类型描述符的索引,即第29个常量。
用二进制的方式查看第几个常量很不好索引,java提供了javap工具将class文件转为类似汇编的语言方便查看,下面看常量池部分的假汇编代码。
命令:javap -v class文件路径
看第一条,Methodref #12.#29,和刚才解读的一致哈,就是这么看的。而#12代表的第12个常量,按照刚才读的方法,第12个常量为:
07查表可知是Class类型,0029即十进制的41,代表该Class的名称为Object(看上面javap的图快速查第41个常量的内容得知的哈)。也就是说第一个方法是属于Object类的。那么方法的名称是什么呢?这里就不从十六进制文件里看了哈,快速从javap输出的文件中查看第29个常量,即init方法。也就是类的初始化方法。
至此,你会看常量了吗?
3.4、访问标志、类、父类、接口
常量池之后紧跟着的是类的访问标志,类,父类,接口,即上文提到的
access_flag:该类或接口声明中使用的修饰符
this_class: 该Class文件定义的类或者接口
super_class:该Class文件父类定义的类或者接口
interface_count:该类中接口的个数
这几个都是u2类型
0021代表类修饰符,可查表
0021代表public。
000B代表十进制11,查上面javap的常量池可知为Hello这个Class
000C代表十进制12,第12个常量为Object
0000代表接口数量为0
3.5、字段表
fields_count:字段个数
fields[]:字段描述数组
0001代表有1个字段,后面的要看字段结构才能知道其含义,字段结构如下:
000A代表字段的修饰符,那000A又是哪个修饰符?还是查表:
000A是0002和0008的和,也就是private static
接下来的000D是常量池中的第13个常量,即msg
000E查常量池中第14个常量,为Ljava/lang/String;意思是该字段类型是String类型。
最后的0000代表没有属性信息
至此我们得到的信息是private static String msg
怎么读字段信息,你学会了吗?
3.6、方法表
methods_count:方法个数
methods[]:方法描述数组
0003代表有3个方法。接下来的信息和读取字段类似,要先看方法结构体
0001代表第一个方法的修饰符,是public的
而后的000F 0010查常量表为和()V。再后面的0001代表有attribute。
后续的0011为attribute_name_index,查常量表为Code,代表后面是方法体了
0000002F为attribute_length,代表方法体的长度为47
具体内容方便查看可使用javap工具,得到如下结果:
4、小结
从上面的步骤看,读字节码是不是其实也并不困难?只是需要查表而已。那为什么我们要学习字节码格式规范呢,一方面我们是使用java的开发人员,了解的深入一些总是好的。另一方面,了解字节码有助于我们使用字节码处理工具,如ASM、JavaAssist、CGLIB等工具来手写字节码。