- 论坛徽章:
- 0
|
作为一种定位于网络使用的语言,Java具有许多适宜于互连网异构平台环境的特点,包括易移植性、安全性、健壮性和动态性等,其中最突出的是易移植性和安全性。Java的易移植性通过将源程序先编译为虚设的中立平台——Java虚拟机JVM(Java Virtual Machine)的中间指令,再由解释器解释为各具体机器平台可执行的机器码运行来实现。Java的安全性措施包括字节码检验、运行时存储设置、文件获取限制等,贯穿在从源程序编译到最终执行的整个过程中。
这一章我们将从较深层次出发,首先介绍Java虚拟机JVM的体系结构,然后说明Java的类文件(.class)的组成以及Java程序从编译到执行的完整过程,并说明这一过程中的安全处理措施。
2.1 JVM体系结构
2.1.1 JVM目的和原理
Java的设计目的是应用于当前使用日益广泛、影响日益扩大的互连网络。这是一个异构平台环境,可能存在多种不机型,如Intel公司的x86系列,Apple/IBM/Motorola公司的PowerPC机等等。每一种机型都有其特定的中央处理机(CPU)芯片,各芯片的处理过程是不同的。因而通用软件通常需要为每一种类型的机器特别编写版本,以保证正确运行。为了克服这一困难,实现语言的通用性和易移植性,Java的设计者采取的方法是选择一种机器作为编译的目标机,再将编译结果在其它机型上解释执行。但选中的目标机并非上述任何一种实用机型,而是一个假设的处理机平台,称为Java虚拟机JVM(Java Virtual Machine)。
那么,什么是Java虚拟机呢?我们可以把它定义为:运行编译生成的Java目标代码(即.class类文件)的计算机的实现。JVM实际上是建立在实际处理机基础上的假想计算机。这一假想机可以通过软件仿真实现,也可以通过硬件实现。鉴于目前大多数Java虚拟机还是用软件方法实现的,我们这里介绍的内容也多基于软件实现。
Java编译器产生的字节码由JVM指令构成,而JVM是虚设的,不接近于任何一种实用机型,这样,一方面编译结果具有平台中立性,不同机型的解释器都可以将字节码文件转换为本机型CPU芯片的适宜机器码来执行。对JVM这一层次而言,操作系统和硬件层都是透明的,用户编写的Java程序,可以在任何平台上运行而无需修改。另一方面,编译生成的字节码接近源生码(native code),可以在任何硬件平台上以较高速度解释运行,实现较高的效率。这样,Java通过虚拟机JVM来试图达到分布式系统的两个相抵触的重要特性:易移植性和高效性之间的平衡和协调。
接着,我们了解一下JVM的体系结构,包括JVM的指令集、操作码和操作数语法以及取值范围等。
2.1.2 JVM的结构
虚拟机JVM由寄存器、栈、废区收集堆、存储区和指令集五部分组成。下面我们将逐一作出介绍。
1.寄存器(Registers)
同其他微处理器的寄存器一样,JVM的寄存器用来存放当前系统状态。然而,基于移植性要求,JVM拥有的寄存器数目不能过多。否则,对于任何本身的寄存器个数小于JVM的移植目标机,要用常规存储来模拟高速寄存器,是比较困难的。同时JVM是基于栈(Stack)的,这也使得它拥有的寄存器较少。
JVM的寄存器包括下面四个:
(1)PC程序计数寄存器
(2)optop操作数栈栈顶地址寄存器。
(3)frame当前执行环境地址寄存器。
(4)vars局部变量首地址寄存器。
这些寄存器长度均为32位。其中PC用来记录程序执行步骤,其余optop,frame,vars都存放JVM栈中对应地址,用来快速获取当前执行所需的信息。
2.栈(Stack)
JVM是以栈为基本存储机制的处理机。栈的特点是先进后出(FILO)。对每个类的每个方法,JVM都定义一定的栈空间,包含下面三种信息:
(1)Local Variables局部变量
这是一个记录各方法局部变量的数组,其初始地址存放在vars寄存器中。每一个数组元素的长度均为32位。若变量长度超过32位,如双精度浮点变量或长整型变量,则占据两个元素的空间64位。
(2)Execution Enviroment执行环境
包含代表当前方法的栈的当前状态。存储的信息有:
*激活的前一个方法。
*指向局部变量区的指针。
*指向操作数栈顶和栈底的指针。
执行环境是执行方法的控制中心,为解释执行和重新编译提供必要的信息。例如,解释器执行JVM的指令iadd,将两整型数相加,执行分为若干步。首先,解释器从寄存器frame中获得当前执行环境。然后,在当前执行环境中指向操作数栈顶的指针,取出要相加的两数。最后还要将所加得的结果回送入栈。
(3)Operand Stack操作栈
这是一个以32位为单位长度,用来存储JVM指令的参数的区域。
3.废区收集堆(Garbage-Collected Heap)
所有的类被实例化时,所获得的存储空间都是从收集堆中分配的。此外,这个堆还要负责无用空间的回收使用。出于移植性和安全性考虑,Java不赋予程序设计员管理内存空间的权力。因而,在编译用new命令申请新对象存储空间后,由解释器负责跟踪记录这一块内存的使用情况。当使用结束时,回收空间送回堆中。在Sun公司的Java和HotJava环境中,这样的“废区收集”都是作为后台线程运行的,保证了系统运行的高效性。
4.存储区(Memory Area)
JVM有两个重要的存储区域,即方法区(method area)和常数池区(constant pool area)。
方法区存放的是类中定义的各方法的二进制字节码。常数池区存放的则是方法名、类名、域名以及字符串常数。
5.指令集(Instruction Set)
指令集是JVM执行的操作码的集合。Java编译器就是将Java源程序转换成JVM的程序:一组JVM指令。
JVM指令都由一个操作码(opcode)带上零个、一个或两个操作数(operand)组成。操作数长度不尽相同,以8位为基本长度,超过8位时按Big Endian的顺序截断组合,即高位存放在低地址字节中,而低位存放在高地址字节中。操作码长度均为8位。这限制了指令种类最多只能为256(28)种。目前已经被定义使用的操作码有160种,它们包括栈操作、数组操作、算术运算、逻辑运算、数据类型转换、控制流程操作、断点和异常处理等丰富而详尽的内容,这里不再一一赘述。
2.2 深入了解.class文件
2.2.1 .class文件的结构
Java的.class文件是用户通过编译命令对源程序进行编译后生成的。由以字节(byte)为单位的字节码(bytecode)组成。每个字节等于8个字位(bit)。由于编译器javac的目标机是虚拟机JVM,因而生成的这此字节码代表的是JVM指令构成的操作码(opcode)和所带的操作数(operand),而非某一特定平台的机器码。
这里我们要重点说明.class文件的整体结构。一个.class文件通常由15种不同的域组成。这些域的长度有些是固定的,有些则是不变的。其中可变的域都各自有相应的范围字段(size field)指明其长度。而这些范围字段本身的长度固定为2字节。
下面将给出这15种域的域名和存储内容简介。
*magic 存储一固定值0xCAFFBABE。
*version 存储生成该.class文件的编译器的版本号。
*constant_pool_count 指明域constant_pool中数组元素个数,即域constant_pool长度。
*constant_pool[constant_pool_count-1] 存储含有constant_pool_count-1个元素的数组,包括类名、域名及各常量信息。
*access_flags 长度为2字节,指明各域、类和方法的16种不同存取限制。
*this_class 长度为2字节,存储一个索引,根据该索引值可在constant_pool域找到相应的有关当前类的信息。
*super_class 与域this_class类似,存储当前类的父类信息。
*interfaces_count 指明域interfaces长度。
*interfaces[interfaces_count] 存储有interfaces_count个元素的数组,每一元素均为一索引值,在域constant_pool的相应索引值位置存储当前类完成的接口的有关信息。
*fields_count 指明域fields的长度。
*filds[fileds_count] 存储一含有fields_count个元素的数组,记录有关类的各域的完整信息。其中每个元素包含一个2字节的access_flags域,两个2字节的constant_pool的索引,一个2字节的attributes_count域以及一个attributes数组。
*attributes_count 指明域以及attributes的长度。
*attributes[attributes_count] 存储一个含有attributes_count个元素的属性结构数组。属性可能的类型包括源文件(SourceFile)、常量值(ConstantValue)、代码(Code)和异常(Exceptions)等等。
2.2.2 执行过程与安全措施
Java源程序经编译生成JVM字节码程序后,执行的工作是由解释器通过下面三个步骤完成的:类的装入、正确性检验和代码执行。
1、类的装入
类的装入工作是由类装入器(Class Loader)完成的。它可以实现从本地机或远地装入当前应用的类和所继承的类。这是执行Java字节码的第一步工作。由于类装入器的支持,Java程序可以自动地装入和运行所需的类,包括从网络上不同结点处装入运行。这一特点所带来的安全性问题是让网络管理员大为担心的,然而接下来我们将看到Java的设计者针对这一特点所设置的详尽的安全措施。
首先,类装入器将从其它结点装入的类存放在它们特定的存储空间,与本地机定义的类的存储空间分隔开,互不干扰。这样既保证了本地类执行的效率,又保护它们不受外部引入的破坏与干扰。
其次,Java还对文件获取作一定的限制。从外部装入类,除了被客户机或用户允许的特定情形外,是不允许获取本地机的文件系统的,这也保证了系统的安全性和保密性。
2.正确性检验
解释器要进行的第二步工作是对字节码进行正确性检验。这一步骤的目的是防止在程序编译之后,运行之前对字节码进行的有意或无意的改动带来的问题。在这一步中,所有的字节码依次通过一个字节码检验器(bytecode verifier),由检验器逐行检查。通常检验器可以查出的错误包括:
*伪指针。
*违反存取限制。
*对象不匹配。
*操作数栈的上溢和下溢。
*参数错误。
*非法数据转换。
字节码的正确性检验既能防止可能造成系统崩溃的错误过程的运行,又能保证下一步骤的代码运行的连续性,避免中途停顿以检查和处理异常。
3.代码执行
解释器的最后一项工作是代码的执行。在此之前,先要完成类的存储设置。Java在执行时才进行存储设置的特点,使网络“黑客”们无法预先得知类在硬件层次上的分配结构,也就无法藉此探知整个系统结构和获取路径,从而有效地保护整个系统不受外来侵入。
字节码的解释执行实际上是解释器将字节码转化成客户机系统平台可进行的操作,然后由客户机平台自行运行的。这样就同时保证了字节码的可移植性和高效性。无论客户机采用何种操作系统,只要配有Java解释器,就能实现字节码的移植执行。而将字节码转换成各平台相应执行的机器码,又保证了执行的效率。
本章小结
这一章通过对Java虚拟机JVM及Java程序实际执行的全过程的介绍。阐明了Java语言突出的易移植性和安全性特点,使读者对Java程序的编译和解释执行了有更深层次的理解。
本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u/18/showart_15410.html |
|