编译与汇编
若是想了解方法执行流程就需要知道字节码汇编后内容。
- java文件编译后会生成class文件。
对于图中的代码编译后与编译前差别不大,区别在于:
- 源代码中若是没有构造方法,编译后会自动添加构造方法。
- 源代码中的注释编译后都没有了,注释原本就如此。
- 使用javap对class文件进行反汇编。
输入javap汇编后得到如下内容。
- 常量池。
此时的运行时常量池里面存储着Student类涉及到的方法,常量等等信息。 - 构造方法(构造函数)
javap汇编前:
public Student() {
}
javap汇编后:
public static void main(String[] args) {
Student student = new Student();
student.study();
student.hashCode();
}
javap汇编后:
public void study() {
int i = 9;
int j = 10;
int k = (i + j) * 11;
System.out.println(k);
}
javap汇编后:
方法执行与栈帧
以执行study()为例进行讲解。 代码如下:
public class Student {
public static void main(String[] args) {
Student student = new Student();
student.study();
student.hashCode();
}
public void study(){
int i = 9;
int j = 10;
int k = (i + j) * 11;
System.out.println(k);
}
}
首先需要了解study方法汇编后的的代码和涉及JVM的运行时数据区。
- 实例方法中,索引为0的位置默认为this,表示对该实例对象的持有。
- 由于先调用的main方法,所以栈帧-main位于栈底。
执行流程:
- study()方法在main中进行调用,调用study时main的代码偏移量为9。
汇编中:9: invokevirtual #10 对于的代码是 student.study(),所以此时的方法返回地址应该是9,程序计数器为9。程序计数器记录当前正在执行的方法代码偏移量,方法出栈后,下一个执行的方法执行的偏移量可能会和上一个方法最后的偏移量相同,所以出现重复计数属于正常情况。有些字符串看不懂可以参考java字节码指令。 - 执行study的 0: bipush 9,这其实是两行指令,所以下一行指令直接从2开始。0:bipush,1:9,意思是将9(i = 9)压入操作数栈中,此时程序执行到0位置,所以程序计数器为0。
- 执行 2: istore_1。此时将9从操作数栈中出栈,存储在局部变量表为1的位置,同时程序计数器为2。
- 执行3: bipush 10(j = 10),将10压入操作数栈,程序计数器为3.
- 执行5: istore_2。10出栈,进入局部变量表索引为2的位置,程序计数器为5.
- 执行6: iload_1。iload_1和前面的istore_1指的都是9,将9加载进操作数栈中,程序计数器为6.
6. 执行7: iload_2。iload_2和前面的istore_2指的都是10,只是操作不同。将10加载进操作数栈中,程序计数器为7.
操作数栈也是栈需要遵循栈的规则,后入栈的放在先入栈的上面。
-
执行iadd,也就是相加操作。将9和10依次出栈,可能在cpu中进行相加操作,操作数栈没有处理数据的功能,将9+10=19的结果再次压入操作数栈中,此时程序计数器为8。 -
执行9: bipush 11。将将要乘的11压入操作数栈中,程序计数器为9. -
执行imul,就是相乘操作。将11和19出栈,计算后将11*19=209压入操作数栈,程序计算器为11. -
执行12: istore_3。将操作数栈中的209出栈,存储在局部变量表索引为3的位置。 -
执行13: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream,就是调用 System.out.println()方法,此时程序计数器为13。 -
执行12: istore_3。将局部变量表中索引为3的209压入操作数栈,此时程序计数器为16. -
执行17: invokevirtual #23。将209出栈,然后执行System.out.println(209)打印输出,此时程序计数器为17. -
执行20:return。此时要返回方法返回地址9,该方法到此执行完毕,执行出栈操作,然后main从方法返回地址9的位置往下执行。
本地方法栈
调用getClass()或hashCode()都属于本地方法,使用native修饰的都属于本地方法。
@IntrinsicCandidate
public final native Class<?> getClass();
...
@IntrinsicCandidate
public native int hashCode();
本地方法栈和虚拟机栈结构相同,物理上共用同一块内存,但是本地方法汇编后和java汇编后不一样,所以程序计数器不能用来记录本地方法。
|