Java之所以可以“一次编译,到处运行”,一是因为JVM针对各种操作系统、平台都进行了定制,二是因为无论在什么平台,都可以编译生成固定格式的字节码(.class文件)供JVM使用。因此,也可以看出字节码对于Java生态的重要性。之所以被称之为字节码,是因为字节码文件由十六进制值组成,而JVM以两个十六进制值为一组,即以字节为单位进行读取。在Java中一般是用javac命令编译源代码为字节码文件。
image.png

一、字节码结构

.java文件通过javac编译后将得到一个.class文件,class文件中的信息是一项一项排列的, 每项数据都有它的固定长度, 有的占一个字节, 有的占两个字节, 还有的占四个字节或8个字节, 数据项的不同长度分别用u1, u2, u4, u8表示, 分别表示一种数据项在class文件中占据一个字节, 两个字节, 4个字节和8个字节。

比如编写一个简单的ByteCodeDemo类

  1. public class ByteCodeDemo {
  2. private int a = 1;
  3. public ByteCodeDemo() {
  4. }
  5. public int add() {
  6. int b = 2;
  7. int c = this.a + b;
  8. System.out.println(c);
  9. return c;
  10. }
  11. }

其16进制的字节码文件如下
image.png

编译后生成ByteCodeDemo.class文件,打开后是一堆十六进制数,按字节为单位进行分割后展示如图2右侧部分所示。上文提及过,JVM对于字节码是有规范要求的,那么看似杂乱的十六进制符合什么结构呢?JVM规范要求每一个字节码文件都要由十部分按照固定的顺序组成,整体结构如图3所示。

2.1、字节码深入研究 - 图3

二、魔数(Magic Number)

所有的.class文件的前四个字节都是魔数,魔数的固定值为:0xCAFEBABE。魔数放在文件开头,JVM可以根据文件的开头来判断这个文件是否可能是一个.class文件,如果是,才会继续进行之后的操作。

有趣的是,魔数的固定值是Java之父James Gosling制定的,为CafeBabe(咖啡宝贝),而Java的图标为一杯咖啡。很多文件存储标准中都用魔数进行身份标识,如图片gif,jpeg都在文件头部中存储着魔数。


三、版本号(Version)

版本号为魔数之后的4个字节,前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version)。上图2中版本号为“00 00 00 34”,次版本号转化为十进制为0,主版本号转化为十进制为52,在Oracle官网中查询序号52对应的主版本号为1.8,所以编译该文件的Java版本号为1.8.0。

四、常量池(Constant Pool)

紧接着主版本号之后的字节为常量池入口。常量池中存储两类常量:字面量与符号引用。

  • 字面量:为代码中声明为Final的常量值,文本字符串,8大基本数据类型
  • 符号引用:如类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符。


简单说明一下,字面量,变量、常量

  1. int a; //变量
  2. final int b = 10; //b为常量,10为字面量
  3. string str = hello world!”; // str 为变量,hello world!为字面量

符号引用:

参考R神的解答:https://www.zhihu.com/question/30300585

常量池整体上分为两部分:常量池计数器以及常量池数据区,如下图所示:

2.1、字节码深入研究 - 图4

常量池计数器(constant_pool_count):由于常量的数量不固定,所以需要先放置两个字节来表示常量池容量计数值。图2中示例代码的字节码前10个字节如下图5所示,将十六进制的24转化为十进制值为36,排除掉下标“0”,也就是说,这个类文件中共有35个常量。

image.png

常量池数据区:数据区是由(constant_pool_count-1)个cp_info结构组成,一**个cp_info**结构对应一个常量。在字节码中共有14种类型的cp_info(如下图6所示),每种类型的结构都是固定的。

2.1、字节码深入研究 - 图6

具体以CONSTANT_utf8_info为例,它的结构如下图7左侧所示。首先一个字节“tag”,它的值取自上图6中对应项的Tag,由于它的类型是utf8_info,所以值为“01”。接下来两个字节标识该字符串的长度Length,然后Length个字节为这个字符串具体的值。从图2中的字节码摘取一个cp_info结构,如下图7右侧所示。将它翻译过来后,其含义为:该常量类型为utf8字符串,长度为一字节,数据为“a”。

2.1、字节码深入研究 - 图7

4.1 字面量

字面量为为代码中声明为Final的常量值,文本字符串,8大基本数据类型

举例:**

  1. int a; //变量
  2. final int b = 10; //b为常量,10为字面量
  3. string str = hello world!”; // str 为变量,hello world!为字面量

4.2 符号引用

然后我们现在来看,具体的上面的例子中的第一个常量池

image.png

  • 0x0A的十进制值为10 ,代表的是CONSTANT_Methodref_info的tag
  • 0x0003和0x0011是该常量池项的两个部分:class_index和name_and_type_index。这两部分分别都是常量池下标,引用着另外两个常量池项。

image.png

顺着这条线索把能传递引用到的常量池项都找出来,会看到(按深度优先顺序排列):

  1. #1 = Methodref #6.#22 // java/lang/Object."<init>":()V
  2. #6 = Class #29 // java/lang/Object
  3. #9 = Utf8 <init>
  4. #10 = Utf8 ()V
  5. #22 = NameAndType #9:#10 // "<init>":()V
  6. #29 = Utf8 java/lang/Object

把引用关系画成一棵树的话:

  1. #1 = Methodref java/lang/Object."<init>":()V
  2. / \
  3. #6 = Class #22 = NameAndType <init>:()V
  4. | / \
  5. #29 = Utf8 #9 = Utf8 <init> #10 = Utf8 ()V
  6. java/lang/Object

由此可以看出,Class文件中指令操作数经过几层间接之后,最后都是由字符串来表示的。这就是Class文件里的“符号引用”的实态:带有类型(tag) / 结构(符号间引用层次)的字符串。

依据知乎上的优质的回答,用通俗的语言解释一下符号引用:

第一次运行的时候,发现指令没有被解析,根据指令去把常量池中有关系的所有项找出来,得到以“UTF-8”编码描述的此方法所属的“类,方法名,描述符”的常量池项,这就是“符号引用”
符号引用就是字符串,这个字符串包含足够的信息:以供实际使用时可以找到相应的位置。你比如说某个方法的符号引用,如:“java/io/PrintStream.println:(Ljava/lang/String;)V”。里面有类的信息,方法名,方法参数等信息。

五、访问标志(access_flag)

访问标志: 常量池结束之后的两个字节,描述该Class是类还是接口,以及是否被Public、Abstract、Final等修饰符修饰。

JVM规范规定了如下图9的访问标志(Access_Flag)。需要注意的是,JVM并没有穷举所有的访问标志,而是使用按位或操作来进行描述的,比如某个类的修饰符为Public Final,则对应的访问修饰符的值为ACC_PUBLIC | ACC_FINAL,即0x0001 | 0x0010=0x0011。

2.1、字节码深入研究 - 图10

5.1 访问标志转为16进制解释


访问标志:实际上就是一系列组合,因为有16位所以共有16个标志可以使用,但是目前就定义了8个,剩下的估计是给jdk9和10……预留的吧。这8个如图所示。

2.1、字节码深入研究 - 图11

2.1、字节码深入研究 - 图12

5.2 测试代码

java代码还是ByteCodeDemo:

  1. package JVM;
  2. public class TestDemo {
  3. }

反编译的class文件:image.png

我们发现这里flags为: ACC_PUBLIC, ACC_SUPER,那这么推算那么十六进制应该是0021,占用2个字节。查看16进行数据。
image.png

六、 类索引、父类索引、接口索引

6.1 概念

在 .class 文件中由这三项数据来确定这个的继承关系。

1、类索引:u2 数据类型,用于确定这个类的全限定名(包名+类名)。
2、父类索引:u2 数据类型,用于确定这个类的父类的全限定名(包名+类名)。
3、接口索引:u2 数据类型的集合,用于描述类实现了哪些接口,这些被实现的接口将按照 implements 语句后的顺序从左至右排列在接口索引集合中。

接口索引集合分为两部分,第一部分表示接口计数器(interfaces_count),是一个 u2 类型的数据,第二部分是接口索引表表示接口信息,紧跟在接口计数器之后。

若一个类实现的接口为 0,则接口计数器的值为 0,接口索引表不占用任何字节。

6.2 测试代码

  1. package JVM;
  2. public class TestDemo {
  3. }

同样生成class文件,然后查看16进制数据
image.png

我们看到该类的类索引在常量池0002位置 ,父类索引在常量池0003位置,接口为0000代表该类没有实现任何接口。

image.png

常量池0002就是当前类,0003父类为默认继承了老祖宗Object。

七、字段表(fields)

字段表:用于描述接口或者类中声明的变量,字段包括类级(static)以及实例级变量,但是不包括局部变量(方法内部变量)。字段表的范围需要注意


7.1 概念

字段表集合:包括了字段计数器和字段数据区如图:

2.1、字节码深入研究 - 图17

Field_info: 依次包含访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。

2.1、字节码深入研究 - 图18

字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常相似的,都是一个u2的数据类型.

2.1、字节码深入研究 - 图19

跟随access_flags标志的是两项索引值:name_indexdescriptor_index,它们都是对常量池的引用,分别代表着字段的简单名称以及字段方法和方法的描述符

描述符的作用:是用来描述字段的数据类型,方法的参数列表(包括数量,类型以及顺序)和返回值。
描述符规则: 基本数据类型以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符加L加对象名的全限定名来表示:

2.1、字节码深入研究 - 图20

7.2 属性表集合——-静态field字段的初始化

在定义属性字段的过程中,我们有时候会很自然地对 属性字段 直接赋值,如下所示:

  1. public static final int MAX=100;
  2. public int count=0;

对于虚拟机而言,上述的两个属性字段赋值的时机是不同的:

  • 对于非静态(即无static修饰)的属性字段的赋值将会出现在实例构造方法()
  • 对于静态的属性字段,有两个选择:1、在静态构造方法()中进行;2 、使用ConstantValue属性进行赋值。

Sun javac编译器对于 静态属性字段 的初始化赋值策略

  • 如果使用finalstatic**同时修饰一个属性字段,并且这个字段是基本类型或者String类型的,那么编译器在编译这个字段的时候,会在对应的field_info**结构体中增加一个ConstantValue类型的结构体,在赋值的时候使用这个ConstantValue进行赋值。
  • 如果该属性字段并没有被final修饰,或者不是基本类型或者String类型,那么将在类构造方法()中赋值。

对于上述的public static final init MAX=100; javac编译器在编译此属性字段构建field_info结构体时,除了访问标志、名称索引、描述符索引外,会增加一个ConstantValue类型的属性表。

2.1、字节码深入研究 - 图21

7.3 测试代码

  1. public class Simple {
  2. private transient static final String str ="This is a test";
  3. }

对应的字节码文件:
2.1、字节码深入研究 - 图22

说明:

1、字段计数器中的值为0x0001,表示这个类就定义了一个属性字段 2、 字段的访问标志0x009A,这个字段的标志符有:ACC_TRANSIENT、ACC_FINAL、ACC_STATIC、ACC_PRIVATE; 3、 名称索引中的值为0x0005,指向了常量池中的第5项,为“str”,表明这个属性字段的名称是str; 4、描述索引中的值为0x0006,指向了常量池中的第6项,为”Ljava/lang/String;“,表明这个field字段的数据类型是java.lang.String类型; 5、属性表计数器中的值为0x0001,表明field_info还有一个属性表; 6、属性表名称索引中的值为0x0007,指向常量池中的第7项,为“ConstantValue”,表明这个属性表的名称是ConstantValue,即属性表的类型是ConstantValue类型的; 7、属性长度中的值为0x0002,因为此属性表是ConstantValue类型,它的值固定为2; 8、常量值索引 中的值为0x0008,指向了常量池中的第8项,为CONSTANT_String_info类型的项,表示“This is a test” 的常量。在对此field赋值时,会使用此常量对field赋值。

第二个例子:

  1. public class HuangHe {
  2. public String name = "huanghe";
  3. private Integer age = 18;
  4. public static final String sex = "男";
  5. }

image.png
其中紫色的下划线的是字段集合相关的
点击查看【processon】

八、方法集合(methods)

8.1 概念

方法表集合:方法表集合和属性表集合其实很相似,都是由一个计数器(方法)若干个方法表构成,只不过方法表的结构相对复杂很多。
方法表的结构体访问标志(access_flags)、名称索引(name_index)、描述索引(descriptor_index)、属性表(attribute_info)集合组成。

  1. method_info {
  2. u2 access_flags;
  3. u2 name_index;
  4. u2 descriptor_index;
  5. u2 attributes_count;
  6. attribute_info attributes[attributes_count];
  7. }

1)、访问标志
不多说了,和属性中的其实差不多,只是有些修饰符不一样。

2.1、字节码深入研究 - 图24

2)、名称索引
就是指这个方法的名称。如:’public void getXX()’中,getXX就是名称索引。名称索引两个字节,这个方法的名称以UTF-8格式的字符串存储在这个常量池项中。

3)、描述索引
指这个方法的返回值,方法内参数信息。一个方法的描述包含若干个参数的数据类型和返回值的数据类型。

4)、 属性表(attribute_info)集合
下面讲

8.2 属性表集合

1、概述

在Class文件、字段表(在第7小节刚刚阐述了的)、方法表都可以携带自己的
属性表集合**,用于描述某些场景专有的信息。

在方法表中, 属性表集合记录了某个方法的一些属性信息,这些信息包括:

  • 这个方法的代码实现,即方法的可执行的机器指令
  • 这个方法声明的要抛出的异常信息
  • 这个方法是否被@deprecated注解表示
  • 这个方法是否是编译器自动生成的

属性表(attribute_info)结构体的一般结构如下所示:

2.1、字节码深入研究 - 图25

属性表占着非常大的一部分且定义了众多属性,上面只列举了4个,查看完成的:JDK1.7版本中21项属性表集合简要介绍
下面介绍两个重要的属性

8.3 Code属性

code属性比较复杂,它是经过编译器编译成字节码指令之后的数据。就是说java程序中的方法体经过javac编译器处理后,最终变成字节码存储在Code属性内

并非所有方法表都有这个属性,接口和抽象类就没有【没有方法体】。 Code属性是Class文件中最重要的一个属性,在Class文件中,Code属性用于描述代码,所有的其它数据项目都用来描述元数据,了解code属性对了解字 节码执行引擎来说是必要基础。

2.1、字节码深入研究 - 图26

上图中的局部变量表是指class文件的一个属性,而不是上文中所说的 java 栈中的每个栈帧的局部变量表,在class文件的局部变量表中,显示了每个变量的作用域范围,所在槽位的索引(index列)、变量名(name列)和数据类型列(J表示long型)。

Code属性表的组成部分:

1、机器指令code**

目前的JVM使用一个字节表示机器操作码,即对JVM底层而言,它能表示的机器操作码不多于28 次方,即 256个。class文件中的机器指令部分是class文件中最重要的部分,并且非常复杂。

2、异常处理跳转信息
**

如果代码中出现了try{}catch{}块,那么try{}块内的机器指令的地址范围记录下来,并且记录对应的catch{}块中的起始机器指令地址,当运行时在try块中有异常抛出的话,JVM会将catch{}块对应懂得其实机器指令地址传递给PC寄存器,从而实现指令跳转;

3、Java源码行号和机器指令的对应关系—-LineNumberTable属性表

编译器在将java源码编译成class文件时,会将源码中的语句行号跟编译好的机器指令关联起来,这样的class文件加载到内存中并运行时,如果抛出异常,JVM可以根据这个对应关系,抛出异常信息,告诉我们我们的源码的多少行有问题,方便我们定位问题。

4、Code属性表结构体
1、attribute_name_index : 属性名称索引,占有2个字节,其内的值指向了常量池中的某一项,该项表示字符. 串“Code”;
2、attribute_length : 属性长度,占有 4个字节,其内的值表示后面有多少个字节是属于此Code属性表的;
3、max_stack : 操作数栈深度的最大值,占有 2 个字节,在方法执行的任意时刻,操作数栈都不应该超过这个值,虚拟机的运行的时候,会根据这个值来设置该方法对应的栈帧(Stack Frame)中的操作数栈的深度;
4、max_locals最大局部变量数目,占有 2个字节,其内的值表示局部变量表所需要的存储空间大小;
5、code_length : 机器指令长度,占有 4 个字节,表示跟在其后的多少个字节表示的是机器指令;
6、code 机器指令区域,该区域占有的字节数目由 code_length中的值决定。JVM最底层的要执行的机器指令就存储在这里;
7、exception_table_length : 显式异常表长度,占有2个字节,如果在方法代码中出现了try{} catch()形式的结构,该值不会为空,紧跟其后会跟着若干个exception_table结构体,以表示异常捕获情况;
8、exception_table : 显式异常表,占有8 个字节,start_pc,end_pc,handler_pc中的值都表示的是PC计数器中的指令地址。exception_table表示的意思是:如果字节码从第start_pc行到第end_pc行之间出现了catch_type所描述的异常类型,那么将跳转到handler_pc行继续处理。
9、attribute_count : 属性计数器,占有 2 个字节,表示Code属性表的其他属性的数目
10、attribute_info : 表示Code属性表具有的属性表,它主要分为两个类型的属性表:“LineNumberTable”类型和“LocalVariableTable”类型。“LineNumberTable”类型的属性表记录着Java源码和机器指令之间的对应关系“LocalVariableTable”类型的属性表记录着局部变量描述

8.4 StackMapTable属性

StackMapTable属性属于Code属性的attributes表。它用于class文件的验证过程中的类型检查中。一个Code属性的attributes表中最多只可能有一个StackMapTable属性。

  1. StackMapTable_attribute {
  2. u2 attribute_name_index;
  3. u4 attribute_length;
  4. u2 number_of_entries;
  5. stack_map_frame entries[number_of_entries];
  6. }
  • attribute_name_index:对应的是常量池表的一个有效索引。也即CONSTANT_Utf8_info结构中表示“StackMapTable”的索引。
  • attribute_length:标识当前属性的长度(排除前六个字节)
  • number_of_entries:表示entries表的成员数量。entries表中的所有成员都是一个stack_map_frame结构。
  • 每一个entry元素都代表了一个方法的StackMapFrame。其包含了字节码的偏移量和局部变量表、操作数栈的验证类型
  • 其实第一个StackMapFrame是隐式的,并且是通过类型检查器的方法描述计算出来。


该表中的每个entry表示方法的一个栈映射帧(stack map frame)。表中栈映射帧的顺序是有意义的。
一个栈映射帧指定了它所应用处的字节码偏移量,以及偏移量对应的局部变量和操作数栈条目的验证类型。
每个栈映射帧依赖于它前面的帧,方法的第一个栈映射帧是隐含的,由类型检查器根据方法描述符进行计算,因此stack_map_frame结构的entries[0]描述了方法的第二个栈映射帧。
一个栈映射帧对应的字节码偏移采用帧中指定的偏移增量offset_delta,并且通过将offset_delta + 1加到前一个帧的字节码偏移量中计算出。除非前一个帧是初始化帧,这种情况下,该栈映射帧对应的字节码偏移就是帧中指定的offset_delta

java代码:

  1. public class Coffee {
  2. int bean;
  3. public void getBean(int var) {
  4. if (var > 0) {
  5. this.bean = var;
  6. } else {
  7. throw new IllegalArgumentException();
  8. }
  9. }
  10. }

编译后的字节码:

  1. public void getBean(int);
  2. Code:
  3. Stack=2, Locals=2, Args_size=2
  4. 0: iload_1
  5. 1: ifle 12
  6. 4: aload_0
  7. 5: iload_1
  8. 6: putfield #2; //Field bean:I
  9. 9: goto 20
  10. 12: new #3; //class java/lang/IllegalArgumentException
  11. 15: dup
  12. 16: invokespecial #4; //Method java/lang/IllegalArgumentException."<init>":()V
  13. 19: athrow
  14. 20: return
  15. LineNumberTable:
  16. line 11: 0
  17. line 12: 4
  18. line 14: 12
  19. line 16: 20
  20. StackMapTable: number_of_entries = 2
  21. frame_type = 12 /* same */
  22. frame_type = 7 /* same */

最后三行,看到了StackMapTable,没错这个就是栈图。StackMapTable包含了:

8.4 ConstantValue属性

之所以学习这个,是因为后面类加载机制有联系到这个属性

这个属性的作用是通知虚拟机为静态变量赋值,只要被static修饰的变量才有这个属性,【有该属性的字段必须有ACC_STATIC访问标志,反过来不一定】。


对于 “int x = 123” 和 “static int x =123”这类代码在日常编写中很常见,但虚拟机对这两种变量赋值的时刻却不同。

对于非static变量[实例变量],是在实例构造器进行 对于类变量,有两种方式选择

①在类构造器方法中赋值

②使用ConstantValue属性初始化 目前Sun javac编译器是这么做的【具体咋做不知道 = =】,
如果同时使用final和static修饰一个变量[这种修饰就相当于个常量],并且是String或基本类型,就使用②, 如果没有被final修饰或不是基本类型和String,就选择①在方法中初始化。最后几句话也对这个解释的很清楚。


8.4 示例代码

  1. public class Simple {
  2. public static synchronized final void greeting(){
  3. int a = 10;
  4. }
  5. }

1、访问标志

greeting()方法的修饰符有:public、static、synchronized、final 这几个修饰符修饰,那么相对地,
greeting()方法的访问标志中的ACC_PUBLICACC_STATICACC_SYNCHRONIZEDACC_FINAL标志位都应该是1
从上面第一张图可以得出,该
访问标志的值应该是十六进制0x0039**。

2、名称索引和描述符索引

紧接着
访问标志(access_flags)后面的两个字节,叫做名称索引(name_index),这两个字节中的值是指向了常量池中某个常量池项的索引,该常量池项表示这这个方法名称的字符串**。

方法描述符索引(descrptor_index**)是紧跟在名称索引后面的两个字节,这两个字节中的值跟名称索引中的值性质一样,都是指向了常量池中的某个常量池项。这两个字节中的指向的常量池项,是表示了方法描述符的字符串**。

2.1、字节码深入研究 - 图27

3、Simple.class文件
**
2.1、字节码深入研究 - 图28

注意 :方法表集合的头两个字节,即方法表计数器(method_count)的值是0x0002,它表示该类中有2 个方法。注意到,我们的Simple.java中就定义了一个greeting()方法,为什么class文件中会显示有两个方法呢?

如果我们在类中没有定义实例化构造方法,JVM编译器在将源码编译成class文件时,会自动地为这个类添加一个不带参数的实例化构造方法,这种添加是字节码级别的,JVM对所有的类实例化构造方法名采用了相同的名称:“”。如果我们显式地如下定义Simple()构造函数,这个类编译出来的class文件和上面的不带Simple构造方法的Simple类生成的class文件是完全相同的。

4、**Simple.class 中的() 方法

2.1、字节码深入研究 - 图29

解释:

1、方法访问标志(access_flags) : 占有 2个字节,值为0x0001,即标志位的第 16 位为 1,所以该()方法的修饰符是:ACC_PUBLIC;
2、 名称索引(name_index): 占有 2 个字节,值为 0x0004,指向常量池的第 4项,该项表示字符串’‘,即该方法的名称是’‘;
3、描述符索引(descriptor_index):占有 2 个字节,值为0x0005,指向常量池的第 5 项,该项表示字符串“()V”,即表示该方法不带参数,并且无返回值(构造函数确实也没有返回值);
4、属性计数器(attribute_count) :占有 2 个字节,值为0x0001,表示该方法表中含有一个属性表,后面会紧跟着一个属性表;


5、属性表的名称索引(attribute_name_index) :占有 2 个字节,值为0x0006,指向常量池中的第6 项,该项表示字符串“Code”,表示这个属性表是Code类型的属性表;
6、 属性长度(attribute_length):占有4个字节,值为0x0000 0011,即十进制的 17,表明后续的 17 个字节可以表示这个Code属性表的属性信息;


7、 操作数栈的最大深度(max_stack):占有2个字节,值为0x0001,表示栈帧中操作数栈的最大深度是1
8、局部变量表的最大容量(max_variable):占有2个字节,值为0x0001, JVM在调用该方法时,根据这个值设置栈帧中的局部变量表的大小;
9、 机器指令数目(code_length) :占有4个字节,值为0x0000 0005,表示后续的5 个字节 0x2A 、0xB7、 0x00、0x01、0xB1表示机器指令;
10、机器指令集(code[code_length]):这里共有 5个字节,值为0x2A 、0xB7、 0x00、0x01、0xB1
11、显式异常表集合(exception_table_count): 占有2 个字节,值为0x0000,表示方法中没有需要处理的异常信息;
12、Code属性表的属性表集合(attribute_count): 占有2 个字节,值为0x0000,表示它没有其他的属性表集合,因为我们使用了-g:none 禁止编译器生成Code**属性表LineNumberTable 和LocalVariableTable**;

解释下机器指令集:

`第一个字节
0x2A`:查询Java 虚拟机规范中关于操作码的解释,0x2A 对应的操作是”aload_0“,作用是将第一个引用类型局部变量推送至栈顶;

第二个字节 0xB7 :0xB7 对应的操作是:”invokespecial“,作用是调用超类构造方法、实例初始化方法或私有方法;它带有2个字节的参数,即后面的 0x00、0x01 是它的参数,这个参数是某个常量池中的索引,指向了常量池的第一项,该项表示一个方法引用项CONSTANT_Methodref_info结构体,表示java.lang.Object 类中的()方法,即 java/lang/Object.”“😦)V。这条指令的意思就是调用父类Object的构造方法()

第5个字符是0xB1 : 对应操作是:“Ireturn”,作用是表示无返回值的方法返回,结束方法调用,这条语句放在方法的机器码最后,表示方法结束调用,返回。

我们可以使用javap -v Simple > Simple.txt**,查看反编译信息是怎样显示这一信息的:**

2.1、字节码深入研究 - 图30

Simple.class 中的greeting() 方法

2.1、字节码深入研究 - 图31

1、方法访问标志(access_flags): 占有 2个字节,值为 0x0039 ,即二进制的00000000 00111001,即标志位的第11、12、13、16位为1,根据上面讲的方法标志位的表示,可以得到该greeting()方法的修饰符有:ACC_SYNCHRONIZED、ACC_FINAL、ACC_STATIC、ACC_PUBLIC;
2、 名称索引(name_index): 占有 2 个字节,值为 0x0007,指向常量池的第 7 项,该项表示字符串“greeting”,即该方法的名称是“greeting”;
3、描述符索引(descriptor_index): 占有 2 个字节,值为0x0005,指向常量池的第 5 项,该项表示字符串“()V”,即表示该方法不带参数,并且无返回值;
4、属性计数器(attribute_count): 占有 2 个字节,值为0x0001,表示该方法表中含有一个属性表,后面会紧跟着一个属性表;


5、属性表的名称索引(attribute_name_index) :占有 2 个字节,值为0x0006,指向常量池中的第6 项,该项表示字符串“Code”,表示这个属性表是Code类型的属性表;
6、属性长度(attribute_length):占有4个字节,值为0x0000 0010,即十进制的16,表明后续的16个字节可以表示这个Code属性表的属性信息;


6、属性长度(attribute_length):占有4个字节,值为0x0000 0010,即十进制的16,表明后续的16个字节可以表示这个Code属性表的属性信息;
7、操作数栈的最大深度(max_stack) :占有2个字节,值为0x0001,表示栈帧中操作数栈的最大深度是1
8、 局部变量表的最大容量(max_variable):占有2个字节,值为0x0001, JVM在调用该方法时,根据这个值设置栈帧中的局部变量表的大小;
9、器指令数目(code_length):占有4 个字节,值为0x0000 0004,表示后续的4个字节0x10、 0x0A、 0x3B、0xB1的是表示机器指令;
10、机器指令集(code[code_length]):这里共有4 个字节,值为0x10、 0x0A、 0x3B、0xB1
11、显式异常表集合(exception_table_count): 占有2 个字节,值为0x0000,表示方法中没有需要处理的异常信息;
12 Code属性表的属性表集合(attribute_count): 占有2 个字节,值为0x0000,表示它没有其他的属性表集合,因为我们使用了-g:none 禁止编译器生成Code**属性表LineNumberTable 和LocalVariableTable**;


指令集解释
第一个字节 0x10 : 查询Java虚拟机规范中关于操作码的解释,0x10 对应的操作是”bipush”,” 作用是将单字节的常量值(-128~127) 推送至栈顶,它要求一个参数,后面的 0x0A 即是需要推送到栈顶的单字节,注意这里的 0x0A 是16进制,就是我们在代码里写的”a=10”中的10。

第三个字节"3B" : “3B”对应的操作是:”istore_0”,作用是将栈顶int 型数值存入第一个局部变量。我们在greeting() 方法中就声明了一个局部变量a,JVM的运行的时候,将这个局部变量a解析,并放置到局部变量表中的第一个位置;上述的0x10 0x0A 指令已经将0x0A 推送到了栈顶了,然后 0x3B指令便将栈顶的0x0A 取出,赋值给局部变量表中的第一个参数,即局部变量a,这样就完成了对局部变量a的赋值;

第4个字符是0xB1 : 对应操作是:“Ireturn”,作用是表示无返回值的方法返回,结束方法调用,这条语句放在方法的机器码最后,表示方法结束调用,返回。

2.1、字节码深入研究 - 图32

参考文章

字节码增强技术探索—-《美团技术团队》 https://www.cnblogs.com/qdhxhz/p/10676337.html