最近有时会聊到java类的字节码,做了这么多年android开发,还真不了解它,于是想探索一下。
先写了一个简单的java类MyClass,代码如下:
1 | public class MyClass { |
然后分析它的class文件。找到生成的MyClass.class之后,用vim -b MyClass.class
打开。打开后,是这样的,一脸懵逼。
不要紧,在vim中输入命令:%! xxd
,展现这样的内容:
把字节码取出来,逐个分析。
cafe babe
每个Class文件的头四个字节称为魔数,它的唯一作用是用来确定该文件是否为一个能被虚拟机接受的Class文件
0000 0035
对应java class的版本号。
1 | u2 minor_version;//次版本号 |
次版本号在前,主版本号在后。minor_version是0,major_version是16进制的35。
0015
1 | u2 constant_pool_count;//常量池容量计数 |
换算成10进制,常量的个数是21,这就代表常量池中有20项常量,索引值范围为1~20。(计数从1开始,其他计数从0开始,因为0有其他作用)
如果用javap -p -v MyClass.class
看,则是
接下来分析一下每一项的含义
0a 00 04 00 11
该常量类型是CONSTANT_Methodref_info
它的结构体是
1 | CONSTANT_Methodref_info { |
items | 描述 |
---|---|
tag | CONSTANT_Methodref_info结构的tag值为10 |
class_index | CONSTANT_Methodref_info结构的class_index必须是一个类类型,而不是接口类型 |
name_and_type_index | name_and_type_index的值必须是constant_pool表中的一个有效索引;这索引值上的对应的常量池条目(The constant_pool entry)也一定是CONSTANT_NameAndType_info结构;这个条目也是具有字段或方法作为成员的类或接口类型 |
它的class_index为4,对应的是第四项,它的name_and_type_index是17。我们先分析下第四项是什么。
07 00 14
这是第4项,07代表CONSTANT_class,它的结构是
1 | CONSTANT_Class_info { |
index为14,代表的是索引值是20的那一项,我们再顺着分析下第20项。
第20项是CONSTANT_Utf8_info,tag是1,它的结构是
1 | CONSTANT_Utf8_info { |
第20项的值是01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74
,其中tag是1,length是16,bytes是java/lang/Object
(表示的字符串是 java/lang/Object)
回过头来我们分析下第17项的name_and_type_index
0c 00 07 00 08
这是第17项,它的结构是
1 | CONSTANT_Name_AndType_info { |
在这里,两个index分别是7和8,于是我们又转向第7项和第8项,第7项和第8项可以看出来,他们的值分别是01 00 06 3c 69 6e 69 74 3e
和01 00 03 28 29 56
。它们又都是CONSTANT_Utf8_info结构,字符串分别是<init>
和()V
。
方法名称<init>
和方法描述符()V
指的是什么呢?指的是一个实例初始方法,没有参数,返回类型也一定是 void。
回过头来我理一下它们的关联,用图来表示如下。
所以#1 Methodref表示的含义是java/lang/Object."<init>":()V
09 00 03 00 12
同样道理,分析第2项是。用图来表示如下。
所以#2 Fieldref表示的含义是MyClass.i:I
,MyClass类的字段i,类型是整型。
至于别的常量,后边再分析。
常量池后边的00 21
MyClass是一个普通Java类,不是接口、枚举或者注解,被public关键字修饰但没有被声明为final和abstract,并且它使用了JDK 1.2之后的编译器进行编译,因此它的ACC_PUBLIC、ACC_SUPER标志应当为真,而ACC_FINAL、ACC_INTERFACE、ACC_ABSTRACT、ACC_SYNTHETIC、ACC_ANNOTATION、CC_ENUM这6个标志应当为假,因此它的access_flags的值应为:0x0001|0x0020=0x0021。
类索引、父类索引与接口索引集合
00 21
之后就是00 03 00 04
,00 03
代表上边常量池中的第3项MyClass,这是类索引,00 04
代表上边常量池中第4项java/lang/Object,这是父类索引。
紧接着是00 00
表示接口索引的个数。因为MyClass没有实现接口,所以个数是0。
field
field count
紧接着是字段个数 00 01
,表示有一个字段。那当然是i了,接下来看字段i在字节码中是如何表示的。
field 结构
这8个字节表示的结构是:
1 | field_info { |
access_flags是2,表示ACC_PRIVATE,私有成员。name_index是5,是常量池中的第5项i,descriptor_index是6,是常量池中的第6项,I,代表整型。
留下来个问题,attributes_count为什么是0,attribute_info在这里是什么含义?
methods
接下来的字节码存储的是java的方法
methods count
00 02
表示两个方法。为什么是两个方法,当然是
第一个方法
第一个方法在字节码中的存储是
首先映入眼帘的是00 01 00 07 00 08 00 01
,00 01
表示ACC_PUBLIC,说明是public方法,00 07
是常量池第7项索引,表示方法名<init>
,00 08
是常量池第8项索引,表示方法描述符()V
。00 01
表示attributes_count, 对应的结构是
1 | method_info { |
那么剩下的就是属性信息了,对应的结构是
1 | attribute_info { |
00 09
表示常量池中属性名称索引是第9项,常量池中第9项是Code,说明属性名称是Code。
u1 info[attribute_length]
对应的字节码信息是:
这些字节码存储的信息是:
其中字节码指令就是2a b7 00 01 b1
,分析这些指令。
- 读入2a,查表得0x2A对应的指令为aload_0,这个指令的含义是将第0个Slot中为reference类型的本地变量推送到操作数栈顶。
- 读入b7,查表得0xB7对应的指令为invokespecial,这条指令的作用是以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private方法或者它的父类的方法。这个方法有一个u2类型的参数说明具体调用哪一个方法,它指向常量池中的一个CONSTANT_Methodref_info类型常量,即此方法的方法符号引用。
- 读入00 01,这是invokespecial的参数,查常量池得0x0001对应的常量为实例构造器<init>方法的符号引用。
- 读入b1,查表得0xB1对应的指令为return,含义是返回此方法,并且返回值为void。这条指令执行后,当前方法结束。
通过javap -p -v MyClass.class可以看到
剩下的字节码是
1 | 000a |
剩下的字节码表示attributes,这些attributes是什么呢?attributes一共有2个属性,LineNumberTable和LocalVariableTable。
其中000a 0000 0006 0001 0000 0001
代表LineNumberTable_attribute这个结构,这个结构是
1 | LineNumberTable_attribute { |
LineNumberTable的作用在于,如果选择不生成LineNumberTable属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。
其中000b 0000 000c 0001 0000 0005 000c 000d 0000
表示LocalVariableTable_attribute,这个结构是
1 | LocalVariableTable_attribute { |
LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。可以看到里边有个local_variable_table。它的含义如下。
start_pc和length 代表了这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围。
name_index和descriptor_index
指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表了局部变量的名称以及这个局部变量的描述符
index
这个局部变量在栈帧局部变量表中Slot的位置。当这个变量数据类型是64位类型时(double和long),它占用的Slot为index和index+1两个
第二个方法
我们再来分析第2个方法,它的字节码一共是
简单分析入下:0001 000e 0008 0001
表示的是
1 | method_info { |
0009 0000 0039
表示的是
1 | attribute_info { |
再来分析info[57],是
字节码 | 名字 | 含义 |
---|---|---|
0003 | max_stack | 操作数栈(Operand Stacks)深度的最大值 |
0001 | max_locals | max_locals代表了局部变量表所需的存储空间 |
0000 000b | code_length | 代表字节指令码的长度 |
2a59 b400 0204 60b5 0002 b1 | code | code指令码 |
00 00 | exception_table_length | 异常表长度 |
无 | exception_table | |
00 02 | attributes_count | 属性数量 |
00 0a00 0000 0a00 0200 0000 0500 0a00 06 | LineNumberTable | |
00 0b00 0000 0c00 0100 0000 0b00 0c00 0d00 00 | LocalVariableTable |
其中00 0a00 0000 0a00 0200 0000 0500 0a00 06
对应的是
1 | LineNumberTable_attribute { |
上边内容已经分析过它的含义,这里不再分析。
其中00 0b00 0000 0c00 0100 0000 0b00 0c00 0d00 00
对应的是
1 | LocalVariableTable_attribute { |
注意,注意,其中关键的code码指令2a59 b400 0204 60b5 0002 b1
,我们没有分析。
code码 | 指令 | 描述 |
---|---|---|
2a | aload_0 | 第0个Slot中为reference类型的本地变量(通常是this)推送到操作数栈顶 |
59 | dup | 复制栈顶。相当于把操作数栈顶元素pop出来,再把它push进去2次 |
b4 00 02 | getfield | 获取对象的字段,将其值压入栈顶 |
04 | iconst_1 | int型常量1进栈 |
60 | iadd | 加法 |
b5 00 02 | putfield | 给对象的字段赋值 |
b1 | return | 返回此方法,并且返回值为void |
最后边的字节码
00 0100 0f00 0000 0200 10
表示什么含义呢?
00 01 表示attribute count为1
00 0f00 0000 0200 10 表示attribute_info
1 | attribute_info { |
这是ClassFile最后的1个属性。
至此,大概分析了一下。
感谢https://blog.csdn.net/qq_31156277/article/details/80108277,整个分析过程是靠这篇博客的讲解来的。