Java虚拟机是如何工作的

  引言

  在高级程序设计语言,如:C和C++中,我们用一种人类可读的格式写程序,然后由一个叫编译器的程序把它翻译成一种二进制格式的可执行代码,这种代码能被机器理解和执行。可执行代码依赖于我们用来执行程序的计算机;它是设备相关的。在Java中,程序的编写和执行的过程是非常相似的,但是有一个重要差别是它允许我们写出设备无关的代码。

  利用一个编译器,所有的Java程序都能被编译成一种叫字节码的中间级代码。我们可以将编译后的字节码运行在任何一台安装了Java运行时环境的计算机上。Java运行时环境由Java虚拟机和它的配套代码组成。

  Java虚拟机是一个模拟设备

  创建Java字节码的一个难点是这些字节码是为一个不存在的设备编译的。这个设备叫做Java虚拟机,它只存在于我们计算机的内存中。让Java编译器为一个不存在的设备创建字节码只是让Java架构变成中性(设备无关)的巧妙过程的一半。Java解释器必须让我们的电脑和字节码文件觉得它们是运行在一个真实的设备上。Java解释器在虚拟机和真实设备之间充当中间人来完成这个任务。(见下图)

  

  图1 – Java虚拟机在一个物理设备上模拟运行

  Java虚拟的负责翻译Java字节码,将其翻译翻译成行为或操作系统调用。例如:一个建立一个远程设备socket连接的请求会包含一个操作系统调用。不同的操作系统用不同的方式处理socket连接,但是程序员并不需要担心这些细节。处理这些翻译是Java虚拟机的任务,所以开发人员完全不必关心运行Java软件的计算机的操作系统和CPU架构的差异。(见下图)

  

  图 2 – Java虚拟机处理(字节码)翻译的过程

  Java虚拟机的基本组成部分

  在我们的电脑内存中创建一个虚拟机需要构造真实计算机没一个主要功能以及程序执行的环境。这些功能可以被分为七大基本部分:

  ● 一系列的寄存器

  ● 一个栈

  ● 一个执行环境

  ● 一个垃圾回收堆

  ● 一个常量池

  ● 一个方法存储区

  ● 一个指令集

  寄存器

  Java虚拟机的寄存器和我们计算机中的计算机是相似的。然而,由于虚拟机试基于栈的,它的寄存器不是用于传递和接收参数。在Java中,寄存器保存机器的状态以及在每行字节码执行执行后进行状态更新,保持状态。下面的四个寄存器保存了虚拟机的状态:

  ● 框架指针寄存器(frame):包含指向当前方法执行环境的指针。

  ● 操作数栈顶指针寄存器(optop):包含了指向操作对数栈的栈顶指针, 用于算数表达式求值。

  ● 程序计数器寄存器(pc):包含了下一个要被执行的字节码的地址。

  ● 变量寄存器(vars):包含了指向局部变量的指针。

  这些寄存器都是32位的,且能立即分配。这也许是因为编译器需要知道局部变量的大小和操作数栈,以及翻译器需要知道执行环境的大小。

  栈

  Java虚拟机用操作数栈为方法和操作提供参数,然后将结果返回。所有的字节码指令从栈中获取操作数,在操作数上进行操作(运算),并且将结果返回到栈中。和虚拟机的寄存器一样,操作数栈的位宽也是32位。

  操作数栈遵循后进先出的原则,并且要求栈中的操作数有一个特定的顺序。例如:字节码指令isub需要在栈顶存放两个整数,这也就意味着以前指令集的操作必须在栈中压入了两个整数。isub指令退出栈顶的两个操作数,对他们进行减法运算,然后将运算结果压入栈中。

  在Java中,整型是一种原始数据类型。每种原始数据类型都有特定的指令来对该类型的数据进行操作(运算)。例如:lsub指令用来执行长整型的减法,fsub用来执行浮点数的减法,dsub用来执行双精度浮点数的减法。真是因为这样,将两个整型数据放在栈顶,然后把他们当做一个长整型数据时非法的。然后将一个64位长度的长整型数据放入栈中,它在栈中占用两个32位的位置。

  我们Java程序中的每一个方法都有与之相对应的堆框架(stack frame),堆框架保存方法状态需要三中类型的数据:方法的局部变量、方法的执行环境以及方法的操作数栈。尽管局部变量区和执行环境数据集的大小一般是在方法调用之前就分配了,操作数栈的大小随着方法的字节码指令的执行而改变。由于Java栈是32位的,64位的操作数不能保证64位对齐。

  执行环境

  执行环境作为一个数据集保存在栈中,它用来处理动态链接、正常方法返回和异常的产生。为了处理动态链接,执行环境包含方法的符号引用、当前方法和当前类的变量。这些符号调用会通过动态链接到符号表翻译成实际的方法调用。

  每当一个方法正常完成的时候,调用方法将获得一个返回值。执行环境处理正常的方法返回时通过恢复调用者的寄存器和增加调用者的程序计数器值以跳过方法执行的指令来实现的。

  如果当前方法的运行正常完成,调用方法将会获得一个返回值。调用方法执行一个具有正确返回值的返回指令时完成这个操作。

  如果调用方法执行一个具有不正确返回值类型的返回指令时,方法会抛出一个异常或错误。异常可能发生在动态链接失败(如:无法找到类文件),或者运行时错误(如:数组引用超出数组范围)。当错误发生时,执行环境生成一个异常。

  垃圾回收堆

  每个运行在Java运行时环境的程序都会有一个垃圾回收堆分配给它。由于类的实例对象都是从堆里分配,这个堆也叫做内存分配池。在大多数系统中,堆的大小被默认设置为1MB。

  尽管堆的大小在我们启动程序的时候就设定了,但它可以扩大,例如:当一个新对象被分配是,为了保证堆不会变的过大,那些不在使用的对象将会自动销毁或者由Java虚拟机进行垃圾回收。

  Java后台线程自动执行垃圾回收,每个在Java运行时环境中运行的线程用于与之相关的两个栈:第一个栈用于Java代码,第二个栈用与垃圾收集代码(C code)。这些栈所用的内存从总系统内存池中获取。每当一个线程开始执行,它被分配一个最大栈用于Java代码和垃圾收集代码。在大多数系统中为Java代码分配的最大栈空间默认是400KB,为垃圾收集代码分配的最大栈空间默认是128KB。

  如果我们系统用内存限制,我们可以强制Java执行更激进的清理操作从而减少总的内存使用量。这个通过缩减Java代码和垃圾收集代码的最大空间来实现。如果我们的系统拥有大量的内存,我们可以迫使Java执行更少侵略性的清理,因此减少了后台处理的数量。这个通过增加Java代码和垃圾收集代码的最大空间来实现。

  常量池

  在堆中的每个类都有一个与之相关的常量池。由于常量池不会改变,它们通常在编译的时候创建,常量池中的项对特定类中的任何方法中用到的(常量)名称进行编码。这类里包含出现常量的个数,以及指定一连串特定常量在类描述里的偏移量。

  所有的常量相关信息遵循基于常量类型的特定格式。例如:类级别的常量通常用于表示一个类或一个接口,并且拥有如下的格式:

  CONSTANT_Class_info{ u1 tag; u2 name_index;}

  其中tag是常量类的值, name_index是类的名称. int[][]的类名称是[[I. Thread[]的类名称是[Ljava.lang.Thread;.

  方法(代码)区

  Java 的方法区类似于其它编程语言运行时环境中的编译后的代码区域。他存储与编译后的代码中的方法相关的字节码指令,以及需要用于动态链接的执行环境符号表。任何调试信息以及可能需要的方法有关的其它信息也存储在这个区域。

  字节码指令集

  尽管程序员更喜欢用高级格式写代码,我们的电脑不能直接执行这些代码,这就是我们为什么要在Java程序运行之前对其进行编译的愿意。一般来说,编译代码并不是机器可读的代码(机器代码)格式,也不是一种中级格式的代码,如:汇编语言或Java字节码。

  Java虚拟机所用的字节码指令类似于汇编指令。如果你曾经用过汇编语言,你就知道指令集为了更高的效率将自身分解到最小限度,对于在屏幕上输出之类的任务,要通过使用一系列的指令来完成。例如,Java语言允许我们只用一行指令就能在屏幕上进行输出,就像代码:

  System.out.println(“Hello world!”);

  在编译的时候,Java编译器将这行输入语句转换成以下的字节码:

  0 getstatic #6 <Field java.lang.System.out Ljava/io/PrintStream;>3 ldc #1 <String “Hello world!”>5 invokevirtual #7 <Method java.io.PrintStream.println(Ljava/lang/String;)V>8 return

  Java开发工具包(JDK)提供一种叫Java类文件反编译程序的查验字节码的工具。我们可以在命令行中输入javap命令来执行反编译。

  由于字节码指令时基于一种低级(语言)格式的,我们的程序执行速度接近于程序被编译成机器语言的执行速度。所有的机器指令都用一系列的0和1表示。在低级语言中,0和1的系列被一些合适的助记符代替,如字节码指令isub。类似于汇编语言,字节码指令的基本格式是:

  <operation> <operands(s)>

  因此,字节码指令集中的指令时有1字节的指定了要执行的操作的操作码,和操作所需要0个或者多个参数或数据组成。

  总结

  Java虚拟机只存在于我们电脑的内存之中。在电脑的内存中再创造一个设备需要七大关键组成:一系列寄存器、一个栈(stack)、一个执行环境、一个垃圾回收堆、一个常量池、一个方法存储区和一种将它们联系起来的机制。这种机制是字节码指令集。

  为了调查字节码,我们用java的class文件进行反编译。通过详细调查字节码指令,我们获取了关于Java虚拟机内部工作(原理)和Java本身的有价值见解。每个字节码指令执行一个特定范围的有限功能,例如:将一个对象压入栈中或从栈中取出一个对象。这些基本功能的组合表述了Java编程语言中定义的复杂的高级语句。这样看起来很棒,有些时候许多字节码指令只是为了实现一个简单Java语句操作。当我们应用这些字节码指令与Java虚拟机的七大组成时,Java获得了它的平台无关性并成为了世界上最强大和最通用的编程语言。

人爱美,不仅需要服饰居室之美,还需要心灵品德之美。

Java虚拟机是如何工作的

相关文章:

你感兴趣的文章:

标签云: