0 引言
以这份 Java 代码为例,来剖析一个 Java 程序的生命历程:
interface ClassName {
String getClassName();
}
class Company implements ClassName {
String className;
public Company(String className) {
this.className = className;
}
@Override
public String getClassName() {
return className;
}
}
public class Main {
public static void main(String[] args) {
String className;
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
className = scanner.next();
Company company = new Company(className);
System.out.println("name=" + company.getClassName());
}
}
}
复制代码
这份代码涉及到了接口,继承,对象的实例化,main 方法。值得我们花费一些功夫去从 JVM 层面上了解这个程序从编译、运行到结束都发生了哪些事情。
1 编译阶段
这段代码会生成 3 个 .class 文件。
.class 文件中保存了 魔数、版本符号、常量池、方法标志、类索引、父类索引、接口索引、字段表(有可能含有属性表)、方法表(有可能含有属性表) 等信息。我们可以通过字节码文件,清晰的描述出 Java 源码中有关类的所有信息。
在这里只以 Main 类为例,使用 javap 命令看一下生成的 .class 文件。
javap -verbose Main;
复制代码
Last modified 2017-12-13; size 852 bytes
MD5 checksum 0336fa14cc04a9c858c34cc016880c19
Compiled from "Main.java"
public class Main
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #18.#29 // java/lang/Object."<init>":()V
#2 = Class #30 // java/util/Scanner
#3 = Fieldref #31.#32 // java/lang/System.in:Ljava/io/InputStream;
#4 = Methodref #2.#33 // java/util/Scanner."<init>":(Ljava/io/InputStream;)V
#5 = Methodref #2.#34 // java/util/Scanner.hasNext:()Z
#6 = Methodref #2.#35 // java/util/Scanner.next:()Ljava/lang/String;
#7 = Class #36 // Company
#8 = Methodref #7.#37 // Company."<init>":(Ljava/lang/String;)V
#9 = Fieldref #31.#38 // java/lang/System.out:Ljava/io/PrintStream;
#10 = Class #39 // java/lang/StringBuilder
#11 = Methodref #10.#29 // java/lang/StringBuilder."<init>":()V
#12 = String #40 // name=
#13 = Methodref #10.#41 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#14 = Methodref #7.#42 // Company.getClassName:()Ljava/lang/String;
#15 = Methodref #10.#43 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#16 = Methodref #44.#45 // java/io/PrintStream.println:(Ljava/lang/String;)V
#17 = Class #46 // Main
#18 = Class #47 // java/lang/Object
#19 = Utf8 <init>
#20 = Utf8 ()V
#21 = Utf8 Code
#22 = Utf8 LineNumberTable
#23 = Utf8 main
#24 = Utf8 ([Ljava/lang/String;)V
#25 = Utf8 StackMapTable
#26 = Class #30 // java/util/Scanner
#27 = Utf8 SourceFile
#28 = Utf8 Main.java
#29 = NameAndType #19:#20 // "<init>":()V
#30 = Utf8 java/util/Scanner
#31 = Class #48 // java/lang/System
#32 = NameAndType #49:#50 // in:Ljava/io/InputStream;
#33 = NameAndType #19:#51 // "<init>":(Ljava/io/InputStream;)V
#34 = NameAndType #52:#53 // hasNext:()Z
#35 = NameAndType #54:#55 // next:()Ljava/lang/String;
#36 = Utf8 Company
#37 = NameAndType #19:#56 // "<init>":(Ljava/lang/String;)V
#38 = NameAndType #57:#58 // out:Ljava/io/PrintStream;
#39 = Utf8 java/lang/StringBuilder
#40 = Utf8 name=
#41 = NameAndType #59:#60 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#42 = NameAndType #61:#55 // getClassName:()Ljava/lang/String;
#43 = NameAndType #62:#55 // toString:()Ljava/lang/String;
#44 = Class #63 // java/io/PrintStream
#45 = NameAndType #64:#56 // println:(Ljava/lang/String;)V
#46 = Utf8 Main
#47 = Utf8 java/lang/Object
#48 = Utf8 java/lang/System
#49 = Utf8 in
#50 = Utf8 Ljava/io/InputStream;
#51 = Utf8 (Ljava/io/InputStream;)V
#52 = Utf8 hasNext
#53 = Utf8 ()Z
#54 = Utf8 next
#55 = Utf8 ()Ljava/lang/String;
#56 = Utf8 (Ljava/lang/String;)V
#57 = Utf8 out
#58 = Utf8 Ljava/io/PrintStream;
#59 = Utf8 append
#60 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#61 = Utf8 getClassName
#62 = Utf8 toString
#63 = Utf8 java/io/PrintStream
#64 = Utf8 println
{
public Main();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 27: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=4, args_size=1
0: new #2 // class java/util/Scanner
3: dup
4: getstatic #3 // Field java/lang/System.in:Ljava/io/InputStream;
7: invokespecial #4 // Method java/util/Scanner."<init>":(Ljava/io/InputStream;)V
10: astore_2
11: aload_2
12: invokevirtual #5 // Method java/util/Scanner.hasNext:()Z
15: ifeq 63
18: aload_2
19: invokevirtual #6 // Method java/util/Scanner.next:()Ljava/lang/String;
22: astore_1
23: new #7 // class Company
26: dup
27: aload_1
28: invokespecial #8 // Method Company."<init>":(Ljava/lang/String;)V
31: astore_3
32: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
35: new #10 // class java/lang/StringBuilder
38: dup
39: invokespecial #11 // Method java/lang/StringBuilder."<init>":()V
42: ldc #12 // String name=
44: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
47: aload_3
48: invokevirtual #14 // Method Company.getClassName:()Ljava/lang/String;
51: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
54: invokevirtual #15 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
57: invokevirtual #16 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
60: goto 11
63: return
LineNumberTable:
line 31: 0
line 32: 11
line 33: 18
line 34: 23
line 35: 32
line 36: 60
line 37: 63
StackMapTable: number_of_entries = 2
frame_type = 253 /* append */
offset_delta = 11
locals = [ top, class java/util/Scanner ]
frame_type = 51 /* same */
}
SourceFile: "Main.java"
复制代码
直接从常量池开始分析一下上面的输出结果:
#1 = Methodref #18.#29 // java/lang/Object."<init>":()V
复制代码
这是常量池里面的第一项数据,#1 代表索引,Methodref 告诉我们常量池中第一个索引表示的是一个方法引用,对这个方法引用的描述用常量池中第 18 项和第 29 项的内容可以进行描述,查一下常量池中第 18 项和第 29 项的内容,其实对应的就是后面注释的内容。它告诉了你这个方法所属的类是 Object,方法的简单名称是 <init>,方法的描述符是 ()V,也就是 Object 中的实例构造函数(对简单名称和描述符我在前面的博客中已经进行了说明)。
你也许想问,为什么 Main 类常量池中的第一项数据描述的是 Object 类中的无参构造函数?你可能忘了,所有类都应如此,Java 中所有的类都继承自 Object,常量池中也会保存他们父类的索引,因为在 Java 中,对象的初始化与实例化还有一条规则—先初始化与实例化父类,然后才是子类。
剩余的常量池分析与上面类似。
常量池下面的代码块,可以看到,一个 Main 类的默认构造函数,一个就是 main 方法了。关于这两个东西,等一下在字节码的执行阶段再说。
可以看到,.class 文件中,包含着详细的信息,有大量的信息都是你无法从源码中直接得到的。
2 类加载阶段
javac 对源文件编译完成,然后使用 java 命令开始运行这个 Main 类。java命令只能运行包含 main 方法的类。
java 命令一开始运行,JVM 开始对 Main 类进行加载。
JVM 在加载这个 Main 类的时候,使用类加载器(双亲委派模型)对其进行加载,经历了加载、验证阶段,由于在 Main 类中并没有类变量,也就相当于跳过了准备这一阶段,然后对字节码进行解析,由于 main 方法是静态方法,也就是非虚方法,开始静态链接,在字节码中直接将 main 方法的符号引用解析为直接引用。由于没有静态变量与静态语句块,所以初始化这一阶段也相当于是直接跳过,最后整个加载过程完毕,并在方法区中生成 Main 类所对应的 Class 对象。
由于这个例子中不涉及多态,也就不涉及分派,但这部分知识请务必掌握。
3 方法执行阶段
类中所有的信息已经在内存中加载完毕,JVM 开始进行方法调用。
方法调用:JVM 开始执行 main 方法,这部分工作是由虚拟机中字节码执行引擎完成的。main 方法会由一个线程进行调用。此线程会在虚拟机栈上为自己开辟一部分的栈空间,此后只要这个线程调用新的方法,这个方法便会被当作栈帧压入虚拟机栈的栈顶(作为当前栈帧)。这个方法中定义的局部变量会被存储进局部变量表,在JVM中,并不存储局部变量的名称,他们都是以局部变量表的相对偏移量来标识每个不同的局部变量。
我以 main 方法的 Code 属性再说明一下栈帧中的局部变量表以及操作数栈(.class 文件中方法表的Code 属性保存的是 Java 方法体中的字节码)。
Code:
stack=3, locals=4, args_size=1
复制代码
可以看到,main 方法在调用之前(实际上在编译阶段,它的局部变量表,操作数栈的大小都已确定),locals 为 4,也就是局部变量表的大小为 4:this,className,scanner,company;stack 为 3,也就是操作数栈的大小 3:className,scanner,company。
我们还可以在上面方法体的字节码当中看到许多指令,而这些指令就是方法在执行的过程中,JVM 需要解释运行的,如下:
0: new #2 // class java/util/Scanner
复制代码
前面的 0 表示的也是相对偏移量,而 new 指令就是新建一个对象,对应 Java 源码中的 new,后面的 #2 代表的是 new 指令的参数,表达的意思是常量池中索引为 2 的数据项,也就是上述代码后面注释中的 Scanner 类。
值得一提的是,这里的 Scanner 由于是对类的实例化,因此 JVM 会首先判断 Scanner 这个类是否已经被加载进内存,如果没有被加载进内存,JVM 开始对这个类进行类加载,过程如上述步骤,然后进行对象的初始化。
指令一条条的向下执行,最终执行到这一步:Company company = new Company(className);
。也是一个类的实例化,但它和上面的 Scanner 又有点不同,这个类不仅实现了 ClassName 接口,而且实例化的时候还进行了传参。那么何时会加载 ClassName 接口呢?我根据查阅的资料,猜测是在 Company 类加载中的验证阶段会触发其接口的加载,具体大家可以 Google、baidu。
那么如何实现参数的传递呢?在实例化 Company 类的过程中,执行了 Company 的构造方法,而构造方法本身又是一个方法,因此可以理解为实参先进入被调用方法的操作数栈中,然后再出栈赋值给被调用方法的局部变量表。
最后,代码执行完毕,程序退出。