在《【编译引擎】学习阅读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)

image-20201213164014831

至此,我们可以得到一个结论:JVM指令序列是表示函数实现源代码的重要信息之一

1.2.维护局部变量列表

我们至少知道3个经验:

如果函数内一条语句使用了前面已经定义过的局部变量,则该语句能获得此局部变量最新的值。

如果函数内一条语句使用了一个局部变量,但局部变量没有定义,IDE就会出现语法错误。

即使是一个空的函数实现,这个函数里面,IDE至少能联想出一个变量this。

通过这3个经验,我们知道JVM至少具备如下能力:

函数内定义了哪些局部变量,这些局部变量最新的值是多少。

这些局部变量的作用范围从哪里开始,到哪里结束。

JVM通过实现局部变量表存储上述信息:

image-20201213165522458

因此,我们可以得到另一个结论:局部变量表也是表示函数实现源代码的必要信息

1.3.源代码与JVM指令序列的映射关系

从1.1可知,调试程序本质是调试运行时指令序列,我们必然需要知道调试的指令序列对应于源代码中的位置。

JVM通过行号表存储了映射关系:

image-20201213210037072

同样的,我们得到另一个结论:行号表也是表示函数实现源代码的必要信息

1.4.属性树

综合前述的分析,JVM需要定义一个表达属性的树表结构才能表示一个函数的内部实现,如下图所示:

image-20201213225139342

Code属性自身具有一些基础信息(例如:操作数栈最大深度),Code属性还包含子属性(例如:行号表子属性、局部变量表子属性)。

这样,就形成了一个属性树。

1.5.属性

JVM:Attributes are used in the ClassFile, field_info, method_info, and Code_attribute structures of the class file format

JVM将Code抽象的称为属性,将行号表也抽象为属性,将局部变量表也抽象地称为属性。在JVM中,还有其它更多的属性

但无论哪种属性,JVM都采用了如下数据结构表示属性的共性

2个字节是属性名在常量池中的索引。

4个字节表示这个属性的字节码长度N。

紧接着N个字节表示这个属性具体的字节码。

image-20201213230831717

深一步思考,又有一个问题:不同的属性应该有不同的数据结构,JVM怎么区分表达属性的特有信息呢?

JVM就是根据属性名来区分的不同类型的属性,每种属性又有自己独有的数据结构,如下图所示:

image-20201213232241686

好,有了这些前置知识,我们接下来详细解读Code、LineNumberTable、LocalVariableTable属性。

2.Code属性

2.1.Code属性名

hello方法下挂的属性名索引=00 0C,如下图:

image-20201213230043475

00 0C=12,对应第12个常量,即Code属性

image-20201214002838199

2.2.Code属性字节码长度

hello方法下挂的Code属性长度=00 00 00 47,如下图:

image-20201214002946890

说明Code属性占据的字节码长度为00 00 00 47=71,如下图红框:

image-20201214003317799

2.3.Code属性.操作数栈最大长度

JVM规范:The Code attribute is a variable-length attribute in the attributes table of a method_info structure . A Code 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属性的数据结构如下:

image-20201214090813324

从max_stack开始,2个字节表示操作数栈最大深度。

00 02 = 2,表示针对Demo2代码,操作数栈的最大深度为2

另外,这里涉及到对虚拟机栈的理解,笔者将会在后续文章中展开。

image-20201214071931530

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

image-20201214072317654

00 02 = 2,局部变量表最大长度为2

image-20201214072400159

2.5.Code属性.指令序列长度

JVM规范:The value of the code_length item gives the number of bytes in the code array for this method.

4个字节表示紧接着的有多少个字节存储JVM指令序列。

image-20201214072528544

00 00 00 0B=11,表示Demo2的hello方法转化的JVM指令序列,需要11个字节存储。

这就很神奇了,一段这样的代码,竟然被转换成了11个字节:

image-20201214073202146

2.6.Code属性.指令序列

JVM规范:The code array gives the actual bytes of Java Virtual Machine code that implement the method.

image-20201214073245663

下图是Demo2的hello方法转换后的JVM指令序列:

image-20201214073409407

根据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,表示此函数执行完成后返回

image-20201214095755437

我们对比一下Demo2的源代码:

image-20201214100012611

3.LineNumberTable属性

3.1.LineNumberTable属性名

JVM规范:The value of the attribute_name_index item must be a valid index into the constant_pool table.

2个字节表示属性名。

image-20201214101143092

00 0D=13,表示第13个常量。

image-20201214103511715

根据常量池映射关系,第13个常量表示LineNumberTable字符串

image-20201214103741137

3.2.LineNumberTable属性字节码长度

JVM规范:The value of the attribute_length item indicates the length of the attribute, excluding the initial six bytes.

4个字节表示属性占用的字节码长度。

image-20201214101226009

00 00 00 0E=14,表示行号表占据字节码14个字节长度。

image-20201214103837705

因此,行号表的字节码如下图红框所示:

image-20201214104013281

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 the code array. Each line_number_table entry must contain the following two items:

  • start_pc

    The value of the start_pc item must indicate the index into the code array at which the code for a new line in the original source file begins.The value of start_pc must be less than the value of the code_length item of the Code attribute of which this LineNumberTable 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)

image-20201214101431970

我们将前文解读的JVM指令序列、Demo2的hello方法的源代码,与行号表的字节码对比,可以看到:

行号表第1行数据:表示JVM指令序列第0行与源代码第7行对应。

行号表第2行数据:表示JVM指令序列第2行与源代码第8行对应。

行号表第3行数据:表示JVM指令序列第10行与源代码第9行对应。

image-20201214102952547

4.LocalVariableTable属性

4.1.LocalVariableTable属性名

JVM规范:The value of the attribute_name_index item must be a valid index into the constant_pool table.

2个字节表示属性名。

image-20201214105409665

00 0E=14,表示第14个常量。

image-20201214105753233

根据常量池映射关系,第14个常量表示LocalVariableTable字符串。

image-20201214110018137

4.2.LocalVariableTable属性字节码长度

JVM规范:The value of the attribute_length item indicates the length of the attribute, excluding the initial six bytes.

4个字节表示属性占用的字节码长度。

image-20201214105443811

00 00 00 16=22,表示局部变量表占据字节码22个字节长度。

image-20201214110048114

因此,行号表的字节码如下图红框所示:

image-20201214111222724

4.3.LocalVariableTable属性的行数据

JVM规范:Each entry in the local_variable_table array indicates a range of code 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, between start_pc inclusive and start_pc + length exclusive.The value of start_pc must be a valid index into the code array of this Code attribute and must be the index of the opcode of an instruction.The value of start_pc + length must either be a valid index into the code array of this Code attribute and be the index of the opcode of an instruction, or it must be the first index beyond the end of that code array.

  • name_index

    The value of the name_index item must be a valid index into the constant_pool table. The constant_pool entry at that index must contain a CONSTANT_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 the constant_pool table. The constant_pool entry at that index must contain a CONSTANT_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 at index is of type double or long, it occupies both index and index + 1.

行号表分为X列:

列1是局部变量在JVM指令序列的索引(start_pc)

列2是局部变量在JVM指令序列偏移量(length)

列3是局部变量名在常量池中的索引

列4是局部变量类型

列5是局部变量槽位——这里涉及到槽位重用问题,笔者将在讲解虚拟机栈的后续章节展开。

image-20201214105609730

我们将前文解读的JVM指令序列、Demo2的hello方法的源代码,与行号表的字节码对比,可以看到:

局部变量表第1行数据:表示局部变量this,类型为Demo2,作用域hello整个函数。

image-20201214112254032

局部变量表第2行数据:表示局部变量i,类型为int,作用域hello函数的**“int i =1"语句后**,**“return”语句前**。

image-20201214112737327

5.总结:JVM如何描述一个方法的内部实现?

根据Demo2演练代码,JVM描述了如下信息:

5.1.三大重要信息的转换与存储

  • 虚拟机栈中,为hello方法开辟了一个栈帧
  • 将源代码转换为JVM指令序列,存储在栈帧的Code属性
  • 将局部变量存储在栈帧的局部变量表
  • 将源代码和JVM指令序列位置映射关系存储在栈帧的行号表

image-20201214120538585

5.2.随着JVM指令序列执行,动态刷新内存(栈帧)

  • 当逻辑代码int i =1执行时,栈帧中JVM指令序列执行到3C处,局部变量i被赋值为1;

image-20201214121148759

  • 当逻辑代码System.out.println(“hello world”)语句执行时,栈帧中JVM指令序列执行到05处,打印了常量池中"hello world"字符串;

image-20201214121349494

上述过程,引出了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源码中,有很多细节代码,下图摘自知乎的一个帖子:

image-20201214123137927

如果不了解字节码,您肯定想不到第2行的代码,居然会对性能优化产生一定的帮助。

谢谢各位读者!