在《【编译引擎】学习阅读Class文件结构(16进制版)-上》、《【编译引擎】学习阅读Class文件结构(16进制版)-中》两篇文章中,笔者讲解了如何以16进制的方式,解读Demo2.java对应字节码的常量池、字段表、方法表。
我们继续解读Demo2.class字节码的方法表的Code属性片段。
1.复杂信息的树状表达:属性树
如果说前面两篇文章描述的信息相对简单,那么当我们需要用一种形式表达一个函数的实现就显得更加复杂了。
我们逆向思考一下,假设我们就是JVM,JVM需要完成哪些重要的任务:
1.1.源代码转化为指令序列
在编译阶段,JVM能够获得的信息形式是——函数实现的源代码X
而在运行阶段,JVM需要输出给CPU的信息形式是——CPU能够理解并执行的CPU指令序列Z
因此,JVM需要将源代码X转换为指令序列Z
Z = G(X)
为了实现转换函数G,首先,在编译阶段,JVM将源代码X转换为JVM指令序列Y,即
Y = F1(X)
然后,在运行阶段,JVM动态地将JVM指令序列Y转换为CPU指令序列Z,即
Z = F2(Y)
至此,我们可以得到一个结论:JVM指令序列是表示函数实现源代码的重要信息之一。
1.2.维护局部变量列表
我们至少知道3个经验:
如果函数内一条语句使用了前面已经定义过的局部变量,则该语句能获得此局部变量最新的值。
如果函数内一条语句使用了一个局部变量,但局部变量没有定义,IDE就会出现语法错误。
即使是一个空的函数实现,这个函数里面,IDE至少能联想出一个变量this。
通过这3个经验,我们知道JVM至少具备如下能力:
函数内定义了哪些局部变量,这些局部变量最新的值是多少。
这些局部变量的作用范围从哪里开始,到哪里结束。
JVM通过实现局部变量表存储上述信息:
因此,我们可以得到另一个结论:局部变量表也是表示函数实现源代码的必要信息。
1.3.源代码与JVM指令序列的映射关系
从1.1可知,调试程序本质是调试运行时指令序列,我们必然需要知道调试的指令序列对应于源代码中的位置。
JVM通过行号表存储了映射关系:
同样的,我们得到另一个结论:行号表也是表示函数实现源代码的必要信息。
1.4.属性树
综合前述的分析,JVM需要定义一个表达属性的树表结构才能表示一个函数的内部实现,如下图所示:
Code属性自身具有一些基础信息(例如:操作数栈最大深度),Code属性还包含子属性(例如:行号表子属性、局部变量表子属性)。
这样,就形成了一个属性树。
1.5.属性
JVM:Attributes are used in the
ClassFile
,field_info
,method_info
, andCode_attribute
structures of theclass
file format
JVM将Code抽象的称为属性,将行号表也抽象为属性,将局部变量表也抽象地称为属性。在JVM中,还有其它更多的属性。
但无论哪种属性,JVM都采用了如下数据结构表示属性的共性:
2个字节是属性名在常量池中的索引。
4个字节表示这个属性的字节码长度N。
紧接着N个字节表示这个属性具体的字节码。
深一步思考,又有一个问题:不同的属性应该有不同的数据结构,JVM怎么区分表达属性的特有信息呢?
JVM就是根据属性名来区分的不同类型的属性,每种属性又有自己独有的数据结构,如下图所示:
好,有了这些前置知识,我们接下来详细解读Code、LineNumberTable、LocalVariableTable属性。
2.Code属性
2.1.Code属性名
hello方法下挂的属性名索引=00 0C,如下图:
00 0C=12,对应第12个常量,即Code属性
2.2.Code属性字节码长度
hello方法下挂的Code属性长度=00 00 00 47,如下图:
说明Code属性占据的字节码长度为00 00 00 47=71,如下图红框:
2.3.Code属性.操作数栈最大长度
JVM规范:The
Code
attribute is a variable-length attribute in theattributes
table of amethod_info
structure . ACode
attribute contains the Java Virtual Machine instructions and auxiliary information for a method, including an instance initialization method or a class or interface initialization method
进一步,查阅JVM规范,Code属性的数据结构如下:
从max_stack开始,2个字节表示操作数栈最大深度。
00 02 = 2,表示针对Demo2代码,操作数栈的最大深度为2。
另外,这里涉及到对虚拟机栈的理解,笔者将会在后续文章中展开。
2.4.Code属性.局部变量表长度
JVM规范:The value of the
max_locals
item gives the number of local variables in the local variable array allocated upon invocation of this method
00 02 = 2,局部变量表最大长度为2
2.5.Code属性.指令序列长度
JVM规范:The value of the
code_length
item gives the number of bytes in thecode
array for this method.
4个字节表示紧接着的有多少个字节存储JVM指令序列。
00 00 00 0B=11,表示Demo2的hello方法转化的JVM指令序列,需要11个字节存储。
这就很神奇了,一段这样的代码,竟然被转换成了11个字节:
2.6.Code属性.指令序列
JVM规范:The
code
array gives the actual bytes of Java Virtual Machine code that implement the method.
下图是Demo2的hello方法转换后的JVM指令序列:
根据JVM规范,笔者将16进制字节码人工翻译成JVM指令序列:
04 => iconst_1,表示将1加载到虚拟机栈中。
3C => istore_1,表示将上一步的数据存储到局部变量表的第1位。
B2 00 03 => getstatic 3,表示调用第3个常量映射的java.io.PrintStream类。
12 04 => Idc 4,表示将第4个常量映射的**“hello world"字符串**加载到虚拟机栈中。
B6 00 05=>invokevirtual 5,表示调用第5个常量映射的println方法。
B1=>return,表示此函数执行完成后返回。
我们对比一下Demo2的源代码:
3.LineNumberTable属性
3.1.LineNumberTable属性名
JVM规范:The value of the
attribute_name_index
item must be a valid index into theconstant_pool
table.
2个字节表示属性名。
00 0D=13,表示第13个常量。
根据常量池映射关系,第13个常量表示LineNumberTable字符串
3.2.LineNumberTable属性字节码长度
JVM规范:The value of the
attribute_length
item indicates the length of the attribute, excluding the initial six bytes.
4个字节表示属性占用的字节码长度。
00 00 00 0E=14,表示行号表占据字节码14个字节长度。
因此,行号表的字节码如下图红框所示:
3.3.LineNumberTable属性的行数据
JVM规范:Each entry in the
line_number_table
array indicates that the line number in the original source file changes at a given point in thecode
array. Eachline_number_table
entry must contain the following two items:
start_pc
The value of the
start_pc
item must indicate the index into thecode
array at which the code for a new line in the original source file begins.The value ofstart_pc
must be less than the value of thecode_length
item of theCode
attribute of which thisLineNumberTable
is an attribute.line_number
The value of the
line_number
item must give the corresponding line number in the original source file.
行号表分为两列,一列是在JVM指令序列的索引(start_pc),一列是源代码的行号(line_number)。
我们将前文解读的JVM指令序列、Demo2的hello方法的源代码,与行号表的字节码对比,可以看到:
行号表第1行数据:表示JVM指令序列第0行与源代码第7行对应。
行号表第2行数据:表示JVM指令序列第2行与源代码第8行对应。
行号表第3行数据:表示JVM指令序列第10行与源代码第9行对应。
4.LocalVariableTable属性
4.1.LocalVariableTable属性名
JVM规范:The value of the
attribute_name_index
item must be a valid index into theconstant_pool
table.
2个字节表示属性名。
00 0E=14,表示第14个常量。
根据常量池映射关系,第14个常量表示LocalVariableTable字符串。
4.2.LocalVariableTable属性字节码长度
JVM规范:The value of the
attribute_length
item indicates the length of the attribute, excluding the initial six bytes.
4个字节表示属性占用的字节码长度。
00 00 00 16=22,表示局部变量表占据字节码22个字节长度。
因此,行号表的字节码如下图红框所示:
4.3.LocalVariableTable属性的行数据
JVM规范:Each entry in the
local_variable_table
array indicates a range ofcode
array offsets within which a local variable has a value. It also indicates the index into the local variable array of the current frame at which that local variable can be found. Each entry must contain the following five items:
start_pc, length
The given local variable must have a value at indices into the
code
array in the interval [start_pc
,start_pc + length
), that is, betweenstart_pc
inclusive andstart_pc + length
exclusive.The value ofstart_pc
must be a valid index into thecode
array of thisCode
attribute and must be the index of the opcode of an instruction.The value ofstart_pc + length
must either be a valid index into thecode
array of thisCode
attribute and be the index of the opcode of an instruction, or it must be the first index beyond the end of thatcode
array.name_index
The value of the
name_index
item must be a valid index into theconstant_pool
table. Theconstant_pool
entry at that index must contain aCONSTANT_Utf8_info
structure (§4.4.7) representing a valid unqualified name denoting a local variable (§4.2.2).descriptor_index
The value of the
descriptor_index
item must be a valid index into theconstant_pool
table. Theconstant_pool
entry at that index must contain aCONSTANT_Utf8_info
structure (§4.4.7) representing a field descriptor which encodes the type of a local variable in the source program (§4.3.2).index
The given local variable must be at
index
in the local variable array of the current frame.If the local variable atindex
is of typedouble
orlong
, it occupies bothindex
andindex + 1
.
行号表分为X列:
列1是局部变量在JVM指令序列的索引(start_pc)
列2是局部变量在JVM指令序列偏移量(length)
列3是局部变量名在常量池中的索引
列4是局部变量类型
列5是局部变量槽位——这里涉及到槽位重用问题,笔者将在讲解虚拟机栈的后续章节展开。
我们将前文解读的JVM指令序列、Demo2的hello方法的源代码,与行号表的字节码对比,可以看到:
局部变量表第1行数据:表示局部变量this,类型为Demo2,作用域hello整个函数。
局部变量表第2行数据:表示局部变量i,类型为int,作用域hello函数的**“int i =1"语句后**,**“return”语句前**。
5.总结:JVM如何描述一个方法的内部实现?
根据Demo2演练代码,JVM描述了如下信息:
5.1.三大重要信息的转换与存储
- 在虚拟机栈中,为hello方法开辟了一个栈帧
- 将源代码转换为JVM指令序列,存储在栈帧的Code属性中
- 将局部变量存储在栈帧的局部变量表中
- 将源代码和JVM指令序列位置映射关系,存储在栈帧的行号表中
5.2.随着JVM指令序列执行,动态刷新内存(栈帧)
- 当逻辑代码int i =1执行时,栈帧中JVM指令序列执行到3C处,局部变量i被赋值为1;
- 当逻辑代码System.out.println(“hello world”)语句执行时,栈帧中JVM指令序列执行到05处,打印了常量池中"hello world"字符串;
上述过程,引出了JVM运行时数据区中的栈区以及内存结构。读者可以参考笔者这篇文章《【运行时数据区】用仓库管理员的视角理解运行时数据区》
6.再看阅读字节码
笔者在文章开头写了那个"牛逼程序猿看着一堆16进制致敬女神"的段子,但是头条不让开车就删掉了O(∩_∩)O~
虽然我们不追求段子中的效果,但阅读字节码,的确是程序猿技术深度的重要体现。
如果您耐心的将笔者的4篇文章阅读完,
- 《【编译引擎】学习阅读Class文件结构的意义》
- 《【编译引擎】学习阅读Class文件结构(16进制版)-上》
- 《【编译引擎】学习阅读Class文件结构(16进制版)-中》
- 《【编译引擎】学习阅读Class文件结构(16进制版)-下》
我们可以再来看看能够阅读字节码的Java程序猿,将具备哪些"一般码农"缺乏的能力:
6.1.能够从更深的深度,理解Java的语言语法特性
当您看到《Effective Java》、《阿里编程规范》等编程规范、高效编程实践的时候,
不了解Java字节码的码农,是这样的思维:
因为规范禁止我们这样做,所以我在实战中的代码一定不会这样写。
而了解Java字节码的程序猿,是这样的思维:
为什么规范要禁止我们这样做,我要看看JVM指令序列,看看JVM是如何处理的。
例如:规范中禁止count=count++这种语句,
不了解Java字节码的码农,只能死记硬背;
而了解Java字节码的程序猿,就可以查看JVM指令序列。
6.2.能够提供性能优化的高维打击手段
在Spring源码中,有很多细节代码,下图摘自知乎的一个帖子:
如果不了解字节码,您肯定想不到第2行的代码,居然会对性能优化产生一定的帮助。
谢谢各位读者!