反编译java文件看字节码执行顺序
目录
反编译信息
字节码执行
入栈出栈
退出临界区
上一篇日志开头里讲到,.java文件加载前首先被Java编译器编译成.class文件,也就是字节码文件,类似于C程序编译链接后形成的可执行文件,但是.class文件里存放的不是操作系统能直接执行的指令,而是JVM能识别的指令,它需要装载到JVM里解释运行。.class文件里的结构很复杂,保存的二进制流是一些如class的接口,方法列表,常量池,版本号,魔数等,这篇日志用一段简单的代码通过对其反编译来看看里面的每一句话被转化成了什么指令,它们的执行顺序是怎么样的。在之前的日志有总结过.class文件信息,但那时候只分析了文件里最前面的一小部分,版本号,常量,JDK版本等,现在往下看看.class里方法体部分的信息。
反编译信息
字节码指令对JVM来说就像机器指令之于操作系统,一条指令对应一个助记符,例如_istore指令从操作数栈中弹出一个元素,sipush指令将元素压入操作数栈,这些在后面会看到,来看一个例子,这是一段简单的代码:
public class Test {
public int printSth() {
int num1 = 95;
int num2 = 27;
return (num1 + num2);
}
}
类里面有一个方法,输出s后return这个字符串,我们用javap –v(或者-verbose)对class文件进行反编译,-v参数表示输出附加的信息,看看里面显示了什么:
映入眼帘的是源文件名,次版本号0和主版本号53,53对应的JDK版本是JDK 9,熟悉的常量池信息,一共20项常量,第三个表示它的父类是java/lang/Object,还有下面utf-8编码的变量num1和num2,这些不一一分析,往下看类里面的方法:
可以看到类中有两个方法,一个是我们定义的printSth(),还有一个是类的构造方法,在printSth()方法里显示了栈的大小等于2,局部变量表大小等,重点是下面的指令,这就是程序执行的字节码。
字节码执行
入栈出栈
每一条指令的冒号前都有一个数字,表示的是字节码的偏移量,也就是当前这条字节码指令所在的位置,第一条字节码从0开始,bipush指令是将一个字节的参数压入操作数栈中,后面的参数是95,对应了我们代码中num1的值,bipush指令本身占一个字节,接收一个字节大小的参数,一共两个字节大小,所以下一条指令位置从2开始。istore_1指令是从操作数栈中弹出一个元素放到局部变量表里,下划线1表示弹出的是栈中第一个位置的元素,可以看到下面还有一条指令istore_2则是弹出第二个位置的元素。
此时局部变量表中有三个元素,位置0上的this表示的是当前的对象,对于非静态方法的调用,局部变量表中都会在0号位置防放置this。我们的代码是将两个变量相加后返回,在字节码中把两个元素95和27放到局部变量表后(bipush能够处理的数据范围是-128到127之间,如果参数不在这个范围内,可以换成sipush指令,它的数据范围在-32768到32767之间),往下看两条指令。iload_1和iload_2,显而易见是将局部变量表里第1和第2个位置上的参数压入操作数栈中,准备做运算,注意这里和上面有一点不同,一开始将参数从操作数栈里放到局部变量表之后,操作数栈就变为空了,而这里将局部变量表里的参数放入到操作数栈里时,局部变量表里的两个参数并没有移出,仍在表内。从上图中可以看到局部变量表1和2位置就是95跟27,放入操作数栈里,栈顶元素就是95,接着iadd指令将操作数栈里的两个参数进行加法运算,并将结果返回到操作数栈里,此时操作数栈中 只有一个结果元素。
退出临界区
最后的一条指令是ireturn,作用很简单,将当前方法的操作数栈栈顶元素弹出,并压入调用者操作数栈(或者说方法栈中),调用者操作数栈和上面的操作数栈区别在于,假设我们的main()方法中调用了printSth()方法,那么main()方法就是调用者,调用者操作数栈属于main()方法,被调用这是printSth(),上面的操作数栈就属于它,ireturn指令将两个参数运算后的结果从自己的操作数栈里返回到调用者操作数栈中,退出临界区,这里用临界区来说明,是因为如果我们的方法是synchronized方法,那么方法里所有逻辑部分都是就是加了锁的,ireturn会执行monitorexit指令,推出临界区,最后销毁自己的操作数栈,恢复到调用者栈。
还没有评论,来说两句吧...