Android工程师学习JVM(二)-教你阅读Java字节码

1,203 阅读5分钟

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等工具来手写字节码。