上一篇文章我们分析了构造函数在字节码文件中的表示,普通的方法与构造函数基本一致,这里也简单分析一下。
回顾
首先看下源码以及常量池:
一个简单的测试类,包含一个私有字段以及它的get、set方法,上一篇文章已经分析了这个类的字节码有3个方法,第一个方法是默认的无参构造函数已经分析过了它的字节码,接下来我们分析剩余方法,字节码文件分析的位置如下图:
蓝色选中区域为上一篇最后分析的一段字节码,它是构造方法最后可以追溯的字节码,说明接下来的字节码在另一个方法的开始。
get方法
直接读取接下里的8个字节(原因看上一篇方法结构)“00 01 00 0E 00 0F 00 01”,他们分别表示访问标识、名称索引、描述符索引、属性数量,结合常量池(索引都是指向常量池)与标识表得出结果分别为:public、getVar1(00 0E对应常量池第14项)、()I(00 0F对应的常量池第15项)、1。其中“()I”上一篇已经解释过了,括号里表示方法的参数,括号后面的表示方法返回类型,这里表示方法无参数,返回一个I类型,大写i表示返回int类型。
最后的1表示只有一个属性表,接下来的6个字节表示属性名称与子属性长度,“00 09 00 00 00 2F”,00 09表示这又是一个Code结构并且有47(2F)个子属性。
接着8个字节”00 01 00 01 00 00 00 05“分表表示Code结构操作数栈最大值、局部变量所需存储空间、代码长度,结果分别是1、1、5;5表示接下来有5个字节码指令。
"2A B4 00 02 AC"这5个字节就是Code结构code属性,它表示的是方法真正的操作过程,对应虚拟机字节码指令表得到如下:
2A对应aload_0表示将第一个引用类型本地变量推送至栈顶;
B4对应getfield表示获取指定类的实例域,并将值压入栈顶,后面紧跟2个字节指向常量池中”00 02“,值为”com/dggcc/test/lei/ClassTest.var1:I“;
AC对应ireturn表示从当前方法返回int类型的值。
接下来是方法的异常数量“00 00”表示没有异常表,最后是Code结构的属性表“00 02”两个属性,紧接着6个字节表示属性名称与长度“00 0A 00 00 00 06”,“00 0A”在常量池中第10项“LineNumberTable”表示是LineNumberTable属性并且长度是6,再接着两个字节”00 01“只有一行,所以接下来4个字节”00 00 00 0C“表示Code第0个字节码指令对应源文件中第12(”00 0C“)行.
接下来2个字节”00 0B“指向常量池第11项结果为”LocalVariableTable“表示方法本地变量表,直接看接下来6个字节”00 00 00 0C 00 01“表示长度12,有一个line_number_info,读取这一个info的10个字节”00 00 00 05 00 0C 00 0D 00 00“,
表示这个变量作用域从0开始到5结束(刚刚分析code一共5个),名称索引对应常量池第12项”this“,描述对应13项”Lcom/dggcc/test/lei/ClassTest;“,在局部变量槽第0位;
这里说明这个方法加了参数this;
get方法就分析完成,方法的描述符是”()I“表示无参返回int。但是在方法的LocalVariableTable中包含this这个变量。再回过头来看字节码指令的三个步骤,首先加载第一个本地变量也就是this,然后获取this的var1放到栈顶,最后弹出结果。
set方法
get方法完成继续第三个方法,老规矩继续接下来的8个字节”00 01 00 10 00 11 00 01“表示访问标识、名称索引、描述符索引、属性数量,结合常量池(索引都是指向常量池)与标识表得出结果分别为:public、setVar1(00 10对应常量池第16项)、(I)v(00 11对应的常量池第17项)、1。其中“(I)V”表示方法需要一个int类型的参数并且返回void。
”00 09 00 00 00 3E“表示一个Code属性,Code属性是方法结构里面最主要、基础的属性。”00 02 00 02 00 00 00 06“表示操作数栈最大值、局部变量所需存储空间、代码长度,结果分别输2、2、6,接下来有6个字节码指令如下"2A 1B B5 00 02 B1",详解如下:
2A对应aload_0表示将第一个引用类型本地变量(this)推送至栈顶;
1B对应iload_1将第二个int型本地变量推送至栈顶;
B5对应putfield为指定类的实例域赋值,”00 02“常量池值为”com/dggcc/test/lei/ClassTest.var1:I“,表示是为this的var1赋值;
B1表示return表示从当前方法返回void;
set方法的执行过程分析完成,可以猜到第二步所谓的”第二个int型本地变量“肯定是对应传入的参数,我们接下来验证。
同样的方法的异常数量“00 00”,最后是Code结构的属性表“00 02”两个属性,紧接着6个字节表示属性名称与长度“00 0A 00 00 00 0A”,“00 0A”在常量池中第10项“LineNumberTable”表示是LineNumberTable属性并且长度是10,再接着两个字节”00 02“只有2行,所以接下来8个字节” 00 00 00 10 00 05 00 11 “表示Code第0个字节码子陵对应源文件中第16(”00 10“)行,所在行对应代码”this.var1=var1;“第5个字节码指令对应源文件中第17(”00 11“)行,对应方法结束的”}“行,是因为返回void,没有显示的return;
接下来2个字节”00 0B“指向常量池第11项结果为”LocalVariableTable“表示方法本地变量表,直接看接下来6个字节”00 00 00 16 00 02“表示长度12,有2个line_number_info。
读取第一个info的10个字节” 00 00 00 06 00 0C 00 0D 00 00“, 表示这个变量作用域从0开始到6结束(刚刚分析code一共6个),名称索引对应常量池第12项”this“,描述对应13项”Lcom/dggcc/test/lei/ClassTest;“,在局部变量槽第0位;
这里说明这个方法的参数this;
第二个info字节” 00 00 00 06 00 05 00 06 00 01“,表示这个变量作用域第1个(字节码指令1B)开始到第6个字节码结束,名称索引对应常量池第5项”var1“,描述对应6项”I“表示int,在局部变量槽第1位(验证了字节码第二个指令);
第二个方法完成,方法描述”(I)V“表示有一个int参数,但是在LocalVariableTable表中第一个参数仍然是this,所以说类的非静态方法的第一个参数一直是this。
最后一步
之前分析方法数量只有三个方法,截至目前构造方法、get、set方法刚好3个,最后再回顾class文件结构如下图:
可以看到还剩最后两个结构,目的是表达class文件的其他一些属性,而class文件剩余的字节码”00 01 00 12 00 00 00 02 00 13“,”00 01“说明class只有一个属性,”00 12“对应属性类型SourceFile,SourceFile用来class文件由哪个文件编译,结构如下图:
”00 00 00 02“表示只占用两个字节,最后”00 13“在常量池对应值”ClassTest.java“,表示class文件由ClassTest.java编译。
总结
通过对两个方法的字节码分析我们可以进一步学习到一些东西,比如构造方法实际上和普通方法在字节码层面是一样的。方法默认都传了一个参数this(静态方法不是),方法的执行过程实际上就是对变量表中的数据进行入栈、出栈、赋值等功能,不过目前分析的方法还很简单,但是明白了最基础的,复杂的方法都是在基础之上进行变化!
Java程序员日常学习笔记,如理解有误欢迎各位交流讨论!
领取专属 10元无门槛券
私享最新 技术干货