Java应用程序无处不在,它们在我们的手机上,平板上,电脑上。在很多编程语言中,这意味着为了让它运行在不同的操作系统上,要多次编译代码。对于我们开发人员,可能Java最酷的事情就是它被设计为平台无关的(正如老话所言:一次编写,到处运行),所以我们只需要编写和编译代码一次。
这怎么可能呢?下面我们深入研究一下Java虚拟机(JVM)来探个究竟。
JVM体系结构
JVM本身对Java编程语言是一无所知的,这可能听起来很让人惊讶。不过,它知道如何执行它自己的指令集。这个指令集称为Java字节码,字节码是组织在二进制.class文件中。Java代码被
javac
命令编译为Java字节码,然后在运行时字节码又被JVM翻译为机器指令。
线程
Java被设计为支持并发,也就是通过在同一进程内执行几个线程,可以同时执行不同的计算。当一个新的JVM进程启动时,JVM内会创建一个新线程(称为主线程)。从这个主线程,代码开始执行,并且其它线程可以被它生产出来。真正的应用程序可有数千个不同用途的执行线程。有些服务于用户请求,其它的执行异步后台任务等等。
栈和帧
每个Java线程被创建时,都会带有一个帧栈,用来保存方法帧,并控制方法调用和返回。方法帧用于存储数据和它所属的方法的部分计算。当方法返回时,它的帧就被丢弃。然后,该方法的返回值被传回给调用帧,调用帧现在就可以用这个返回值来完成它自己的计算。
JVM中用于执行方法的地方是方法帧。该帧由两个主要部分组成:
本地变量数组 – 方法的参数和局部变量存储在这里操作数栈 – 方法的计算在这里执行
几乎每个字节码命令都至少要操作这两个部分之一。下面我们来看看其工作机制。
工作机制
下面我们用一个简单的例子来了解不同的元素是如何共同来运行我们的程序。假设我们有如下简单程序,计算
2+3的值,并打印出结果:
classSimpleExample {publicstaticvoidmain(String[] args) {int result = add(2,3); System.out.println(result); }publicstaticintadd(int a, int b) {return a+b; }}
执行javac SimpleExample.java编译该类,会生成编译文件SimpleExample.class。我们已经知道这是一个包含字节码的二进制文件。那么,我们如何才能检查.class字节码呢?用javap。
javap是JDK自带的一个命令行工具,可以汇编.class文件。调用javap -c -p可以打印出反汇编出来的.class字节码(-c),包括私有成员和方法(-p):
Compiled from"SimpleExample.java"classSimpleExample { SimpleExample(); Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublicstaticvoidmain(java.lang.String[]); Code:0: iconst_21: iconst_32: invokestatic #2 // Method add:(II)I5: istore_16: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;9: iload_110: invokevirtual #4 // Method java/io/PrintStream.println:(I)V13: returnpublicstaticintadd(int, int); Code:0: iload_01: iload_12: iadd3: ireturn}
现在在运行时JVM中发生了什么呢?java SimpleExample启动了一个新的JVM进程,并创建主线程。为main()方法创建了一个新帧,然后该帧被压入到线程栈。
publicstatic void main(java.lang.String[]); Code:0: iconst_21: iconst_32: invokestatic #2// Method add:(II)I5: istore_16: getstatic #3// Field java/lang/System.out:Ljava/io/PrintStream;9: iload_110: invokevirtual #4// Method java/io/PrintStream.println:(I)V13: return
main()方法有两个变量:argsresult。二者都驻留在局部变量表中。main的头两个字节码命令iconst_2和iconst_3,分别将常量值2和3加载到操作数栈中。下一条命令invokestatic调用静态方法add()。由于该方法需要两个整数做参数,所以invokestatic从操作数栈中弹出两个元素,并将它们传给由JVM为add()创建的新栈。main()的操作数栈此时是空的。
publicstaticintadd(int, int); Code:0: iload_01: iload_12: iadd3: ireturn
在add栈中,这些参数被存储在局部变量数组中。头两个字节码命令iload_0和iload_1加载第一个和第二个局部变量到栈中。接下来,iadd从操作数栈中弹出栈顶的两个元素,对二者求和,并将结果推回到栈中。最后,ireturn弹出栈顶的元素,将其作为方法的返回值传给调用它的帧,然后本帧就被丢弃了。
publicstatic void main(java.lang.String[]); Code:0: iconst_21: iconst_32: invokestatic #2// Method add:(II)I5: istore_16: getstatic #3// Field java/lang/System.out:Ljava/io/PrintStream;9: iload_110: invokevirtual #4// Method java/io/PrintStream.println:(I)V13: returnmain()的栈现在保存了add()的返回值。istore_1将它弹出来,将其设置为索引位置为1的变量的值,也就是
result的值。getstatic将类型为java/io/PrintStream的静态字段java/lang/System.out压入栈中。
iload_1将索引位置为1处的变量(就是result的值,现在等于5)压入到栈中。所以此时栈保存了两个值:out
字段和值5。现在invokevirtual准备调用PrintSteam.println()方法。它从栈中弹出两个元素:第一个元素是对将要调用println()方法的对象的引用。第二个元素是一个要传递给println()方法的整型参数,它只需要一个参数。这是
main()方法打印相加的结果的地方。最后,return命令完成该方法。主帧被丢弃,JVM进程结束。
就是这样。总而言之,不是太复杂。
“一次编写,到处运行”
那么,是什么让Java与平台无关呢?这一切都在字节码中。
正如我们所见,所有Java程序都编译为标准Java字节码。然后JVM在运行时将它转换为特定的机器指令。我们不再需要确保我们的代码是机器兼容的。相反,我们的应用程序可以在任何配备了JVM的设备上运行,而JVM将为我们做到这一点。JVM维护者的工作是提供不同版本的JVM以支持不同的机器和操作系统。
这种体系结构让所有Java程序都可以运行在安装有JVM的任何设备上。因此,奇迹就发生了。
最后的想法
虽说Java开发人员无需理解JVM工作机制就能写出很不错的程序,不过,深入研究JVM体系结构,学习它的结构,并了解JVM如何解释代码,将有助于成为更好的开发人员。它也会帮助你时不时处理非常复杂的问题。
举报/反馈

朗妹儿爱学习

55获赞 95粉丝
关注互联网技术发展、人才培养、行业需求
关注
0
0
收藏
分享