JVM实战与原理
目录
字节码执行引擎
章节目的:虚拟机是如何找到正确的方法,如何执行方法内的字节码,以及执行代码时涉及的内存结构
引言:前面我们已经知道虚拟机加载类的过程,并且开辟出怎么样的内存结构,那么接下来虚拟机是怎么执行类代码的呢?
我们以Person类为例,讲解其从main开发到结束,字节码执行引擎是如何进行的。
class Person {
public static void main(String[] args) {
System.out.println(methodA(true, 0));
}
public static String methodA(boolean isShow, int count) {
String str = new String("");
if(isShow) {
str = new String("show");
count = count + 1;
}
return str;
}
}
其对应的Class文件内容反编译结果如下
class Person
minor version: 0
major version: 52
flags: ACC_SUPER
Constant pool:
#1 = Methodref #10.#23 // java/lang/Object."<init>":()V
#2 = Fieldref #24.#25 // java/lang/System.out:Ljava/io/Print Stream;
#3 = Methodref #9.#26 // Person.methodA:(ZI)Ljava/lang/Strin g;
#4 = Methodref #27.#28 // java/io/PrintStream.println:(Ljava/ lang/String;)V
#5 = Class #29 // java/lang/String
#6 = String #30 //
#7 = Methodref #5.#31 // java/lang/String."<init>":(Ljava/la ng/String;)V
#8 = String #32 // show
#9 = Class #33 // Person
#10 = Class #34 // java/lang/Object
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 methodA
#18 = Utf8 (ZI)Ljava/lang/String;
#19 = Utf8 StackMapTable
#20 = Class #29 // java/lang/String
#21 = Utf8 SourceFile
#22 = Utf8 Person.java
#23 = NameAndType #11:#12 // "<init>":()V
#24 = Class #35 // java/lang/System
#25 = NameAndType #36:#37 // out:Ljava/io/PrintStream;
#26 = NameAndType #17:#18 // methodA:(ZI)Ljava/lang/String;
#27 = Class #38 // java/io/PrintStream
#28 = NameAndType #39:#40 // println:(Ljava/lang/String;)V
#29 = Utf8 java/lang/String
#30 = Utf8
#31 = NameAndType #11:#40 // "<init>":(Ljava/lang/String;)V
#32 = Utf8 show
#33 = Utf8 Person
#34 = Utf8 java/lang/Object
#35 = Utf8 java/lang/System
#36 = Utf8 out
#37 = Utf8 Ljava/io/PrintStream;
#38 = Utf8 java/io/PrintStream
#39 = Utf8 println
#40 = Utf8 (Ljava/lang/String;)V
{
Person();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init> ":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljav a/io/PrintStream;
3: iconst_1
4: iconst_0
5: invokestatic #3 // Method methodA:(ZI)Ljava/lang/S tring;
8: invokevirtual #4 // Method java/io/PrintStream.prin tln:(Ljava/lang/String;)V
11: return
LineNumberTable:
line 4: 0
line 5: 11
public static java.lang.String methodA(boolean, int);
descriptor: (ZI)Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=2
0: new #5 // class java/lang/String
3: dup
4: ldc #6 // String
6: invokespecial #7 // Method java/lang/String."<init> ":(Ljava/lang/String;)V
9: astore_2
10: iload_0
11: ifeq 28
14: new #5 // class java/lang/String
17: dup
18: ldc #8 // String show
20: invokespecial #7 // Method java/lang/String."<init> ":(Ljava/lang/String;)V
23: astore_2
24: iload_1
25: iconst_1
26: iadd
27: istore_1
28: aload_2
29: areturn
LineNumberTable:
line 8: 0
line 9: 10
line 10: 14
line 11: 24
line 13: 28
StackMapTable: number_of_entries = 1
frame_type = 252 /* append */
offset_delta = 28
locals = [ class java/lang/String ]
}
SourceFile: "Person.java"
1. 方法区
在类加载后,会将Class文件中的类信息、常量、静态变量以及每个方法的信息和对应的字节码指令放入方法区内存中,并将其中的static修饰的变量通过<clinit>方法进行初始化。
2. 栈帧
作用:支持虚拟机进行方法调用和方法执行的数据结构,存储了方法的局部变量表、操作数栈、动态连接和方法返回地址
一个方法对应着一个栈帧,同时当前方法位于栈顶,例如进入main方法,此时main方法在栈顶,此时main方法中调用了methodA方法,则methodA入栈,位于栈顶。
下面我们详细介绍栈帧的组成部分
2.1 局部变量表
作用:存放方法参数和方法内部定义的局部变量。
局部变量表以容量槽Slot为最小单位,其中boolean、byte、char、short、int、float、reference或returnAddress为一个Slot,long和double为两个Slot。一般来说一个Slot占用32位长度的内存空间,其中reference类型表示对一个对象实例的引用,通过引用,能查找到对象在堆中数据存放的起始地址索引。returnAddress已经很少见了,已被异常表代替。
实例:Person类的methodA方法中,isShow、count和str便是存放在局部变量表中,其中isShow为boolean类型,count为int类型,str则是reference类型。
如果执行的是实例(非static)方法,那局部变量表的第0位索引的Slot默认是方法所属对象实例的引用,在方法中用关键字this来访问到这个隐含的参数
2.2 操作数栈
作用:用于存放方法执行过程中,字节码指令进行入栈和出栈操作时,写入和提取的内容。
boolean、byte、char、short、int、float所占的栈容量为1,long和double所占的栈容量为2。
实例:Person类的methodA方法中,count = count + 1;便是在操作数栈中进行,先将count入栈,接着1入栈,然后执行栈顶头两个元素相加的指令,然后出栈。
2.3 动态连接
符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。
另外一部分将在每一次运行期间转化为直接引用,这部分成为动态连接。
2.4 方法返回地址
作用:方法退出之后,需要返回到方法被调用的位置,程序才能继续执行。
方法有两种退出方式
1. 正常完成出口:执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的调用者,此种退出方式,栈帧中会保存调用者PC计数器的值。
2. 异常完成出口:当方法执行过程遇到异常,且异常没有在方法体内得到处理,只要在方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,此种退出方式,返回地址是要通过异常处理器表来确定,栈帧中不保存这部分信息。
Person类的方法执行过程:
虚拟机会自动寻找到main方法进行执行。执行方法,则会将main方法生成栈帧压入虚拟机栈中。然后main方法的字节码如下
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: iconst_1
4: iconst_0
5: invokestatic #3 // Method methodA:(ZI)Ljava/lang/String;
8: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
11: return
main方法中调用了methodA方法,则会将methodA压入栈,此时methodA的局部变量表会存有isShow、count及str变量。
methodA的字节码如下
0: new #5 // class java/lang/String
3: dup
4: ldc #6 // String
6: invokespecial #7 // Method java/lang/String."<init>
9: astore_2
10: iload_0
11: ifeq 28
14: new #5 // class java/lang/String
17: dup
18: ldc #8 // String show
20: invokespecial #7 // Method java/lang/String."<init>
23: astore_2
24: iload_1
25: iconst_1
26: iadd
27: istore_1
28: aload_2
29: areturn
String类信息在加载阶段会被加载至方法区中,new指令会在堆中开辟空间存放对象实例,引用str压入操作数栈栈顶。
dup指令会将栈顶的str复制并压入栈顶
ldc将空字符串从方法区的常量池中推送至栈顶
invokespecial指令则会调用String类的初始化方法
astore_2指令将栈顶引用型数值存入第三个本地变量中
这样,经过以上四个指令,str则被初始化成功,其类信息在方法区中,引用在栈的局部变量表中,实例信息则在堆中。
最后的areturn,则是方法的退出指令,此时方法返回地址记录着main方法调用methodA方法的地址,在methodA方法结束返回后,便会根据这个返回地址返回到main方法中。
main方法中调用println打印后,调用return指令同样也会进行方法退出,至此,整个main方法便执行结束了。