Java虚拟机的设计和实现_第1页
Java虚拟机的设计和实现_第2页
Java虚拟机的设计和实现_第3页
Java虚拟机的设计和实现_第4页
Java虚拟机的设计和实现_第5页
已阅读5页,还剩55页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

目录第一章绪论 6Java及Java虚拟机 6Java虚拟机的体系结构 7MiniJavaVM的功能 10MiniJavaVM的运行环境及开发工具 12第二章系统设计 13唯一的虚拟机MiniJavaVM 13MiniJavaVM的构成要素 13虚拟机总体框架 13命令参数解析模块 15类的装载和解析模块 15内存管理模块 16执行引擎模块 17方法调用模块 18异常处理模块 19第三章虚拟机框架的实现 21JVM工程 22JavaVM工程 23Java虚拟机的数据类型和字长考量 23JavaVM类 24JavaNativeCall工程 29第四章类的装载和解析 30JavaClass文件 30Class文件在MiniJavaVM中的数据结构表示 32类的装载和解析 36第五章内存管理 41对象、堆、方法区的管理 41MiniJavaVM的垃圾回收过程 44第六章执行引擎——Java操作码实现 47Java虚拟机中的操作码功能分类 47操作码功能实现——JavaOperatorExecute类 49第七章方法调用的实现 50Java中的方法调用 50非本地方法的实现 51本地方法的实现 52Java中的本地方法 52NativeMethod_access类 54如何处理本地方法可变参数问题 54第八章异常的实现 56Java中的异常 56异常在MiniJavaVM中的实现 56第九章验证MiniJavaVM的正确性 58MiniJavaVM的使用方法 58测试操作码实现的正确性 61方法调用的正确性 61数学运算的正确性 64控制流语句的正确性 65测试本地方法调用 66测试异常处理 67第十章不足与后续工作 69本地方法 69I/O操作 69多线程 69效率 69致谢 71参考文献 72摘要本文叙述了Java虚拟机(JVM)的概念及如何设计和实现一个Java虚拟机——MiniJavaVM。着重介绍了虚拟机的体系结构及如何设计和实现这个体系结构。在探讨虚拟机的设计过程中详细介绍了MiniJavaVM虚拟机各部分的设计,包括类的装载和解析,内存管理,执行引擎,方法调用和异常处理部分。最后通过测试MiniJavaVM来验证设计和实现的正确性。关键词Java虚拟机(JVM) 字节码类装载执行引擎本地方法MiniJavaVM–adesignandimplementationofaJavaVirtualMachineAbstractThispaperdescribestheconceptionofJavaVirtualMachine(JVM)andhowtodesignandimplementaJavaVirtualMachine–MiniJavaVM.ItemphasizesthearchitectureofJVMandhowtodesignandimplementthearchitecture.ItdescribesthedetailsabouteachpartofJVMwhendiscussinghowtodesigntheJVM,includingclass-loadingandresolution,memorymanagement,executionengine,methodinvokingandexception-handling.Atlast,thecorrectnessofthedesignandimplementationisvalidatedbytestingMiniJavaVM.KeywordsJavaVirtualMachine(JVM),bytecode,Class-loading,executionengine,NativeMethod.绪论Java及Java虚拟机说起Java,人们首先想到的是Java编程语言,然而事实上,Java是一种技术,它由四方面组成:Java编程语言、Java类文件格式、Java虚拟机和Java应用程序接口(JavaAPI)。它们的关系如下图所示:[1]运行期环境代表着Java平台,开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件)。最后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行。从上图也可以看出Java平台由Java虚拟机和Java应用程序接口搭建,Java语言则是进入这个平台的通道,用Java语言编写并编译的程序可以运行在这个平台上。这个平台的结构如下图所示:[1]在Java平台的结构中,可以看出,Java虚拟机(JVM)处在核心的位置,是程序与底层操作系统和硬件无关的关键。它的下方是移植接口,移植接口由两部分组成:适配器和Java操作系统,其中依赖于平台的部分称为适配器;JVM通过移植接口在具体的平台和操作系统上实现;在JVM的上方是Java的基本类库和扩展类库以及它们的API,利用JavaAPI编写的应用程序(application)和小程序(Javaapplet)可以在任何Java平台上运行而无需考虑底层平台,就是因为有Java虚拟机(JVM)实现了程序与操作系统的分离,从而实现了Java的平台无关性。[1]什么是Java虚拟机?Java虚拟机是运行所有Java程序的抽象计算机,它仅仅是由一个规范来定义的抽象的计算机。当提及“Java虚拟机”时,可能指的是如下三种不同的东西:抽象规范一个具体的实现一个运行中的虚拟机实例[2]Java虚拟机负责Java程序设计语言的内存安全、平台无关和安全特性。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机(JVM)在多个平台上实现统一语言。Java之所以得以大行其道,除了它是一门面向对象、构造精美的语言之外,更重要的原因在于:它摆脱了具体机器的束缚,使跨越不同平台编写程序成为可能。Java虚拟机的体系结构在Java虚拟机规范中,一个虚拟机实例的行为是分别按照子系统、内存区、数据类型以及指令这几个术语来描述的。这些组成部分一起展示了抽象的虚拟机的内部抽象体系结构。但是规范中对它们的定义并非是要强制规定Java虚拟机实现内部的体系结构,更多的是为了严格地定义这些实现的内部特征。规范本身通过定义这些抽象的组成部分以及它们之间的交互,来定义任何Java虚拟机实现都必须遵守的行为。每个JVM都有两种机制,一个是装载具有合适名称的类(类或是接口),叫做类装载子系统;另外的一个负责执行包含在已装载的类或接口中的指令,叫做运行引擎。每个JVM又包括方法区、堆、Java栈、程序计数器和本地方法栈这五个部分,这几个部分和类装载机制与运行引擎机制一起组成了Java虚拟机的体系结构。图描述了Java虚拟机的结构框图,包括在规范中描述的主要子系统和内存区。每个Java虚拟机都有一个类装载器子系统,它根据给定的全限定名来装入类型(类或接口)。同样,每个Java虚拟机都有一个执行引擎,它负责执行那些包含在被装载类的方法中的指令。Java虚拟机的运行时数据区存储了许多运行时数据,例如,字节码,从已装载的class文件中得到的其他信息,程序创建的对象,传递给方法的参数,返回值,局部变量,以及运算的中间结果等。Java虚拟机把这些东西都组织到几个“运行时数据区”中,以便于管理。某些运行时数据区是由程序中所有线程共享的,还有一些则只能由一个线程拥有。每个Java虚拟机实例都有一个方法区及一个堆,它们是由该虚拟机实例中所有线程共享的。当虚拟机装载一个class文件时,它会从这个class文件包含的二进制数据中解析类型信息。然后把这些类型信息放到方法区中。当程序运行时,虚拟机会把所有该程序在运行时创建的对象都放到堆中。图描述了这些内存区域。当每一个线程被创建时,它都将得到它自己的PC寄存器以及一个Java栈。如果线程正在执行的是一个Java方法(非本地方法),那么PC寄存器的值将总是指示下一条将被执行的指令,而它的Java栈则总是存储该线程中Java方法调用的状态——包括它的局部变量,被调用时传进来的参数,它的返回值,以及运算的中间结果等。而本地方法调用的状态,则是以某种依赖于具体实现的方式存储在本地方法栈中,也可能是在寄存器或其他某些与特定实现相关的内存区中。Java栈是由许多栈帧(stackframe)组成的,一个栈帧包含一个Java方法调用的状态。当线程调用一个Java方法时,虚拟机压入一个新的栈帧到该线程的Java栈中;当该方法返回时,这个栈帧被从Java栈中弹出并抛弃。Java虚拟机没有寄存器,其指令集使用Java栈来存储中间数据。这样设计的原因是为了保持Java虚拟机的指令集尽量紧凑,同时也便于Java虚拟机在那些只有很少通用寄存器的平台上实现。另外,Java虚拟机的这种基于栈的体系结构,也有助于运行时某些虚拟机实现的动态编译器和即时编译器的代码优化。图描绘了Java虚拟机为每个线程创建的内存区,这些内存区是私有的,任何线程都不能访问另一个线程的PC寄存器或者Java栈。[3]MiniJavaVM的功能能够装载并解析javaclass文件对于已经编译好的javaclass文件,能够读取该class文件的内容,装载该类,并保存在程序内部的数据结构中。当在程序运行的过程中需要解析该类时,进行解析,并替换符号引用为直接引用在完成虚拟机的初始化后,能够找到main函数并执行程序REF_Ref106006195h[13]对于指定的入口类,在虚拟机完成了初始化后,寻找该类的main()方法,如果找到,则执行该方法,否则抛出异常,虚拟机运行中止。支持Java虚拟机规范中规定的200多个操作码的功能实现了Java虚拟机的200多个操作码的功能,由此使MiniJavaVM这个虚拟机模拟Java虚拟机的功能成为可能,这200多个操作码包括:栈和局部变量操作指令将常量池入指令从栈中的局部变量中装载值指令将栈中的值存入局部变量指令通用栈操作指令类型转换指令整数运算指令逻辑运算指令移位操作指令按位布尔运算指令浮点运算指令对象和数组指令对象操作指令数组操作指令控制流指令条件分支指令比较指令无条件转移指令表跳转指令异常指令finally子句指令方法调用与返回指令方法调用指令方法返回指令线程同步指令具有内存管理和垃圾收集机制Java虚拟机对内存的管理使得java程序具有很高的安全性,程序员不用担心内存访问越界问题,也不用为在合适的时候释放分配的空间而费心。垃圾收集机制的存在解决何时回收不用的内存和如何回收内存的问题。支持非本地方法调用按照Java虚拟机规范中的要求来设置非本地方法的调用情况,包括参数压栈,分配局部变量空间,压入方法调用的栈桢等。支持本地方法调用Java虚拟机中所有与本地方法相关的部分都重新写过,以动态链接库的形式为MiniJavaVM工程提供支持。MiniJavaVM的本地方法只实现最基本的功能,不再负责虚拟机的安全机制。支持异常处理有了异常处理,就能够在程序运行时平稳处理意外情况。根据Javaclass文件中的异常表,MiniJavaVM程序支持所有的异常处理,并在不能解决异常时输出异常信息,虚拟机停止运行。能够运行与I/O无关的完整Java程序,并提供参数供查看运行效果提供了-version,-showversion,–help,-?,–verbose命令。-version命令显示MiniJavaVM的版本信息,然后退出-showversion命令显示MiniJavaVM的版本信息,然后继续运行Java程序-help,-?命令显示帮助信息-verbose命令输出详细数据显示运行过程MiniJavaVM的运行环境及开发工具开发平台:WindowsXP/2003开发语言:ANSIC/C++开发工具:运行平台:WindowsXP/2000/2003系统设计唯一的虚拟机MiniJavaVM一个运行的Java虚拟机实例的天职就是:负责运行一个Java程序。当启动一个Java程序时,一个虚拟机实例也就诞生了。当该程序关闭退出,这个虚拟机实例也就随之消亡。如果在同一台计算机上同时运行三个Java程序,将得到三个Java虚拟机实例。每个Java程序实例都运行于它自己的Java虚拟机实例中。REF_Ref106003078h[2]为此,需要有一种机制保证在运行过程中只有一个Java虚拟机的实例产生。首先要定义表示Java虚拟机的类,然后保证这个类有只一个实例。一种可以供选择的方案是使用Singleton设计模式,保证Java虚拟机实例在整个程序运行过程中只有一个;另一种是可以考虑MFC(MicrosoftFundamentalClass)中唯一的全局变量theApp来表示程序运行的实例,通过AfxGetApp()的方法来得到该唯一的全局变量的指针,然后在此基础上进行操作。在这里,我们选择MFC方式定义唯一的全局变量来表示Java虚拟机的运行实例,可以在程序进入main()函数前完成虚拟机的初始化。通过定义一个全局函数来返回虚拟机实例的指针。MiniJavaVM的构成要素虚拟机总体框架我们的MiniJavaVM的框架合理地组织了虚拟机运行时所需的各模块,将各模块的输入与输出有效地结合在一起,使这些模块组合在一起完成了Java虚拟机的功能。这些模块包括:命令参数解析模块、类的装载和解析模块、内存管理模块、执行引擎模块、方法调用模块、异常处理模块、多线程处理模块(未完成)。我们的MiniJavaVM总的组织方式如图所示。其中除命令参数解析模块外,其他模块一起构成了完整的MiniJavaVM虚拟机,这些模块之间协同合作,完成了虚拟机的功能。其中命令参数解析模块负责解析命令,根据MiniJavaVM后的参数来设定虚拟机的运行模式及输出信息;类的装载和解析模块能从class文件或是文件中装载指定名称的Java类,并采用迟解析的方式在需要时解析该类,类的信息维护在虚拟机的一个数据结构中;内存管理模块负责为类的实例及静态字段分配空间,并在虚拟机内维护类的实例和静态字段,当虚拟机空间不足时会启动垃圾回收机制来回收内存;执行引擎模块负责解释执行200多个操作码,解释的过程包括对栈桢、栈、PC、局部变量区的修改;多线程处理模块负责维护虚拟机内的表示线程的数据结构,在语言级提供多线程支持;方法调用模块负责处理方法调用过程,对于非本地方法,包括找到调用方法的指针,新建栈桢、将方法参数设置在新栈桢的局部变量区,调用方法并将返回值压栈的过程,对于本地方法,包括找到调用方法的指针,将方法参数用汇编的方式压栈,调用本地方法并将返回值压栈的过程;异常处理模块负责处理虚拟机抛出的异常,记录异常产生处的异常信息,并试图通过查找当前方法的异常表来处理异常信息,如果能够通过异常表找到处理异常的代码,则修改PC的值使虚拟机处理当前异常,否则,当虚拟机不能处理该异常时,输出异常信息,然后终止虚拟机的运行。通过这几个模块的协同合作,我们的MiniJavaVM虚拟机能够很好地模拟Java虚拟机的功能。命令参数解析模块命令参数解析模块负责解析命令行,根据MiniJavaVM后的参数来设定虚拟机的运行模式及输出信息。该模块的设计如图所示:命令参数解析模块在解析完命令行参数后,通过得到虚拟机的唯一实例的指针调用设置参数的方法来设置虚拟机运行时的参数。类的装载和解析模块类的装载和解析模块负责从javaclass文件或是文件中装载指定名称的Java类,并采用迟解析的方式在需要时解析该类,类的信息维护在虚拟机的一个数据结构中。我们的MiniJavaVM类的装载和解析模块设计如图所示:其中要装载的类文件有两种渠道获得,一种是直接查找相应类的class文件,一种是从文件中得到类的class文件的数据,为了统一这两种方式,可以先从这两种方法中生成类文件的字节流,再交由下一步程序处理。当生成class文件的字节流后,通过指定的模块读取字节流中的信息,生成该Java类在虚拟机中对应的数据。这样,类装载的部分算是完成。在虚拟机运行的过程中,会需要解析类中的常量池,将符号引用替换为直接引用。内存管理模块内存管理模块负责为类的实例及静态字段分配空间,并在虚拟机内维护类的实例和静态字段,当虚拟机空间不足时会启动垃圾回收机制来回收内存。在Java虚拟机中,关于被装载类型的信息存储在一个逻辑上称为方法区的内存中,所有线程共享方法区,因此它们对方法区数据的访问必须被设计为是线程安全的。方法区的大小不必是固定的,虚拟机可以根据应用的需要动态调整。同样,方法区也不必是连续的,方法区可以在一个堆(甚至是虚拟机自己的堆)中自由分配。另外,虚拟机也可以允许用户或者程序员指定方法区的初始大小以及最小和最大尺寸等。REF_Ref106003797h[4]方法区也可以被垃圾收集。因为虚拟机允许用户定义的类装载器来动态扩展Java程序,因此一些类也会成为程序“不再引用”的类。当某个类变为不再被引用的类时,Java虚拟机可以卸载这个类(垃圾收集),从而使方法区占据的内存保持最小。为了简单,我们的MiniJavaVM的方法区使用虚拟机自己的堆,不参与垃圾回收,同时,类的静态字段及一些特殊的类的实例(如与每个类相关的Class类的实例)也不参与垃圾回收。我们的MiniJavaVM内存管理模块设计如图所示:当MiniJavaVM虚拟机请求一个可以被回收的内存空间(比如普通类的实例空间)时,则通过内存管理模块分配可回收的内存,所分配的内存的实地址经过映射后返回给虚拟机,同时已分配的内存地址记录在一张哈希表中,供快速查找所用。如果虚拟机已经没有可分配的空间,则运行垃圾收集,垃圾收集完成后再分配内存。当虚拟机请求一个不可被回收的内存空间(比如类的静态字段空间,表示已装载类的Class类的实例等)时,通过内存管理模块分配不可回收内存,所分配的内存的实地址经过映射后返回给虚拟机,同时已分配的内存地址记录在一张哈希表中,供快速查找所用。当垃圾收集完成后仍没有可分配的内存地址可用时,虚拟机退出。执行引擎模块任何Java虚拟机实现的核心都是它的执行引擎。在Java虚拟机规范中,执行引擎的行为使用指令集来定义。对于每条指令,Java虚拟机规范都详细规定了当实现执行到该指令时应该处理什么。REF_Ref106003906h[5]执行引擎模块负责解释执行Java200多个操作码,解释的过程包括对Java栈桢、Java栈、PC、局部变量区的修改。我们的MiniJavaVM执行引擎模块设计如图所示:在调用执行引擎模块时,会传入一字节码流,执行引擎模块负责解释这一字节码流。在解释字节码过程中,Java虚拟机可能会装载新的Java类,分配某个Java类的实例,进行数学运算,修改当前java栈桢、Java栈、PC、局部变量区等。方法调用模块方法调用模块负责处理方法调用过程,对于非本地方法,包括找到调用方法的指针,新建栈桢、将方法参数设置在新栈桢的局部变量区,调用方法并将返回值压栈的过程,对于本地方法,包括找到调用方法的指针,将方法参数用汇编的方式压栈,调用本地方法并将返回值压栈的过程。我们的MiniJavaVM方法调用模块的设计如图所示:虚拟机为每一个调用的Java(非本地)方法一个新的栈帧。栈帧包括:为方法的局部变量所预留的空间,该方法的操作数栈,以及特定虚拟机实现需要的其他所有信息。局部变量和操作数栈的大小在编译时计算出来,并放置到class文件中去,然后虚拟机就能够了解到方法的栈帧需要多少内存。当虚拟机调用一个方法的时候,它为该方法创建恰当大小的栈帧,再将新的栈帧压入Java栈。处理实例方法时,虚拟机从所调用方法栈帧内的操作数栈中弹出objectref和args。虚拟机把objectref作为全局变量0放到新的栈帧中,把所有的args作为局部变量1,2,……等处理。Objectref是隐式传给所有实例方法的this指针。对于类方法,虚拟机只从所调用的方法栈帧中的操作数栈中弹出参数,并将它们放到新的栈帧中去作为局部变量0,1,2……当objectref和args(对于类方法则只有args)被赋给新栈帧中的局部变量后,虚拟机把新的栈帧作为当前栈帧,然后将程序计数器指向新方法的第一条指令。REF_Ref106004117h[6]虚拟机使用一种“与实现相关”的风格调用本地方法。当调用本地方法时,虚拟机不会将一个新的栈帧压入Java栈。当线程进入到本地方法的那一刻,它就将Java栈抛在身后。直到本地方法返回以后,Java栈才被重新调用。这里本虚拟机的实现将使用调用动态链接库中的方法来实现本地方法调用的过程。异常处理模块异常处理模块负责处理虚拟机抛出的异常,记录异常产生处的异常信息,并试图通过查找当前方法的异常表来处理异常信息,如果能够通过异常表找到处理异常的代码,则修改PC的值使虚拟机处理当前异常,否则,当虚拟机不能处理该异常时,输出异常信息,然后终止虚拟机的运行。我们的MiniJavaVM异常处理模块的设计如图所示:虚拟机框架的实现Java虚拟机的体系结构如图所示:实现虚拟机框架,需要考虑以下几点:虚拟机的数据类型和字长如何实现栈结构如何装载和解析类如何调用本地和非本地方法如何实现执行引擎如何实现多线程机如何组织方法区和堆如何进行垃圾回收虚拟机如何处理异常为了解决这些问题,这里我们把MiniJavaVM虚拟机的实现总体分为三部分。第一部分为主体部分,JavaVM工程。这个工程相当于实现了一个Java虚拟机的所有功能,包括定义虚拟机的数据类型和字长,实现虚拟机栈结构,组织方法区和堆,装载和解析类,实现执行引擎,调用非本地方法,实现多线程,处理异常等。但这个工程不负责虚拟机在执行过程中需要调用的本地方法的实现。由于本地方法总是与Java虚拟机实现的底层平台相关的,因此这个工程只负责处理与底层平台实现不相关的部分,而将与底层平台实现相关的部分交给另外一个工程JavaNativeCall工程。JavaVM工程将开发为动态链接库的形式,有利于别的工程调用这个工程中重要的导出类和导出方法。第二部分为本地方法实现部分,JavaNativeCall工程。此工程负责本地方法接口(JNI)的声明和实现。该工程将开发为动态链接库的形式,有利于JavaVM工程调用该工程中的导出函数。第三部分为主程序,JVM工程。该工程负责解析并设置虚拟机运行参数,调用JavaVM工程的虚拟机实现类来启动虚拟机。该工程为可执行文件。我们将主程序与虚拟机工程分离,底层平台实现相关与实现无关部分代码分离,使MiniJavaVM虚拟机的实现更具层次感。同时,也具有了更好的可扩充性JVM工程JVM工程负责解析并设置虚拟机运行参数,调用JavaVM工程的虚拟机实现类来启动虚拟机。JVM工程所要完成的任务可以用图表示:JVM工程只是调用JavaVM工程中导出的JavaVM类,设置其参数及入口类,所有Java虚拟机的运行工程交由JavaVM类完成。JavaVM工程JavaVM工程实现了一个Java虚拟机的所有功能,包括定义虚拟机的数据类型和字长,实现虚拟机栈结构,组织方法区和堆,装载和解析类,实现执行引擎,调用非本地方法,实现多线程,处理异常等。但这个工程不负责虚拟机在执行过程中需要调用的本地方法的实现。由于本地方法总是与Java虚拟机实现的底层平台相关的,因此这个工程只负责处理与底层平台实现不相关的部分,而将与底层平台实现相关的部分交给另外一个工程JavaNativeCall工程。Java虚拟机的数据类型和字长考量Java虚拟机是通过某些数据类型来执行计算的,数据类型及其运算都是由Java虚拟机规范严格定义的。数据类型可以分为两种:基本类型和引用类型。基本类型的变量持有原始值,而引用类型的变量持有引用值,图描述了Java虚拟机中的数据类型:Java虚拟机规范定义了每一种数据类型的取值范围,但是没有定义它们的位宽。存储这些类型的值所需的占位宽度,是由具体的虚拟机实现的设计者决定的。Java语言中的所有基本类型同样也是Java虚拟机中的基本类型。但boolean有点特别,当编译器把Java源码编译为字节码时,它会用int或byte来表示boolean。在Java虚拟机中,false是由整数零来表示的,所有非零整数都表示true。涉及boolean值的操作则会使用int。表3.2.1 Java虚拟机数据类型的取值范围类型范围Byte8比特,带符号,二进制补码Short16比特,带符号,二进制补码Int32比特,带符号,二进制补码Long64比特,带符号,二进制补码Char16比特,不带符号,Unicode字符Float32比特,IEEE754标准单精度浮点数Double64比特,IEEE754标准单精度浮点数ReturnAddress同一方法中某操作码的地址Reference堆中对某对象的引用,或者是nullJava虚拟机中,最基本的数据单元就是字(word),它的大小是由每个虚拟机实现的设计者来决定的。字长必须足够大,至少是一个字单元就足以持有byte、short、int、char、float、returnAddress或者reference类型的值,而两个字单元就足以持有long或者double类型的值。因此,虚拟机实现的设计者至少得选择32位作为字长,或者选择更为高效的字长大小。通常根据底层主机平台的指针长度来选择字长。Java虚拟机规范中,关于运行时数据区的大部分内容,都是基于“字”这个抽象概念的。比如,关于栈帧的两个部分——局部变量和操作数栈——都是按照“字”来定义的。这个内容区域能够容纳任何虚拟机数据类型的值,当把这些值放到局部变量或者操作数栈中时,它将占用一个或两个字单元。REF_Ref106004256h[7]在我们的JavaVM工程中,对Java虚拟机的数据类型和字长的定义如下:JavaVM类JavaVM类是JavaVM工程中最重要的一个类,这个类定义了Java虚拟机应该实现的功能,所有Java虚拟机相关的功能对应于通过调用该类的相应方法来实现。为了定义JavaVM类,我们必须定义其他一些辅助类。这些辅助类包括:JavaThread类:负责管理虚拟机线程CClassFile类:负责读取JavaClass文件的内容JavaClassInfo类:负责根据CClassFile类的内容生成该java类的类信息JavaArrayInfo类:负责生成某一类型的数组信息JavaClassFileLoader类:负责找出要装载的javaclass文件,并将文件内容作为二进制流传给CClassFile类Cp_xxxxxxxx类:负责维护Class文件中常量池的内容Attr_xxxxxxxx类:负责维护Class文件常量池中attr_info相关的内容Memxxxxxxx类:负责虚拟机的内存管理,包括分配内存和进行垃圾收集JavaVM类的接口定义如下:我们的JavaVM类内部组织如图所示:其中线程部分设计如图所示:通过上面设计,使用JavaVM类来表示Java虚拟机,使用一个唯一的JavaVM的全局变量来代表运行中的Java虚拟机;使用链表来存储在Java虚拟机上运行的所有线程,并保留一个指向当前线程的指针;使用链表来存储已经装载的class文件的信息,并保留一个指向入口class的指针,以找到正确的main()入口;在JavaVM类中定义内存管理模块的接口,通过调用这些接口来分配并读写内存,并把类信息同相应的返回的引用值联系起来;在JavaVM类中定义垃圾收集模块的接口,通过这些接口来进行垃圾收集。JavaNativeCall工程JavaNativeCall工程为本地方法实现部分工程。此工程负责本地方法接口(JNI)的声明和实现。该工程开发为动态链接库的形式,有利于JavaVM工程调用该工程中的导出函数,真正做到本地方法调用的功能。为了实现这个工程,这里参考了Kaffe中关于JNI实现部分的代码。Kaffe是一个功能强大的Java虚拟机,它包括Java2平台的子集,标准JavaAPI和工具来提供Java运行时环境。REF_Ref106005584h[12]MiniJavaVM的本地方法接口(JNI)部分主要是参考了Kaffe中对本地方法接口的定义。我们的MiniJavaVM的本地方法接口的实现定义了如下类型:所有JNI函数的声明均按照“_MiniJavaVM+包名+类名+方法名+参数列表”的格式声明。如类的本地方法registerNatives()的声明如下:java_void_MiniJava_java_lang_System_registerNatives(JavaVM*pJVM,java_classclsref);具体调用时,只要在装载该方法所在的动态链接库后再得到该方法的地址,将参数压栈,调用这个方法,就能完成这个方法的调用,也就实现了Java本地方法。类的装载和解析JavaClass文件Javaclass文件中包含了Java虚拟机所需知道的、关于类或接口的所有信息。Javaclass文件是8位字节的二进制流。数据项按顺序存储在class文件中,相邻的项之间没有任何间隔,这样可以使class文件更紧凑。占据多个字节空间的项按照高位在前的顺序分为几个连续的字节存放。REF_Ref106004394h[8]和Java类的可以包含多个不同的字段、方法、方法参数、局部变量等一样,Javaclass文件也能够包含许多不同大小的项。在class文件中,可变长度项的大小和长度位于其实际数据之前。这个特性使得class文件流可以从头到尾被顺序解析,首先读出项的大小,然后读出项的数据。Class文件的基本类型如下,所有存储在u2,u4,u8项中的值,在class文件中以高位在前的形式出现。表class文件“基本类型”u11个字节,无符号类型u22个字节,无符号类型u44个字节,无符号类型u88个字节,无符号类型可变长度的ClassFile表中的项,如表所示:表ClassFile表的格式类型名称数量U4Magic1U2Minor_version1U2Major_version1U2Constant_pool_count1Cp_infoConstant_poolConstant_pool_count-1U2Access_flags1U2This_class1U2Super_class1U2Interfaces_count1U2InterfacesInterfaces_countU2Fields_count1Field_infoFieldsFields_countU2Methods_count1Method_infoMethodsMethods_countU2Attributes_count1Attribute_infoAttributesAttributes_countJavaClass文件中的常量池包括了与文件中类和接口相关的常量,其中存储了诸如文字字符串、final变量值、类名和方法名的常量。常量池中的许多入口都指向其他的常量池入口,而且class文件中紧随着常量池的许多条目也会指向常量池中的入口在整个class文件中,指示常量池入口在常量池列表中位置的整数索引都指向这些常量池入口。列表中的第一项索引值为1,第二项索引值为2,以此类推。尽管constant_pool列表中没有索引值为0的入口,但缺失的这一入口也被constant_pool_count计数在内。第个常量池入口都从一个长度为一个字节的标志开始,这个标志指出了列表中该位置的常量类型,一旦Java虚拟机获取并解析这个标志,Java虚拟机就会知道在标志后的常量类型是什么。表列出了所有常量池标志的名字和值。表常量池标志入口类型标志值描述CONSTANT_Utf81UTF-8编码的Unicode字符串CONSTANT_Integer3Int类型字面值CONSTANT_Float4Float类型字面值CONSTANT_Long5Long类型字面值CONSTANT_Double6Double类型字面值CONSTANT_Class7对一个类或接口的符号引用CONSTANT_String8String类型字面值CONSTANT_Fieldref9对一个字段的符号引用CONSTANT_Methodref10对一个类中声明的方法的符号引用CONSTANT_InterfaceMethodref11对一个接口中声明的方法的符号引用CONSTANT_NameAndType12对一个字段或方法的部分符号引用在类或者接口中声明的每一个字段(类变量或者实例变量)都由class文件中的一个名为field_info的可变长度的表进行描述。在一个class文件中,不会存在两个具有相同名字和描述的字段。表field_info的格式如下表field_info表的格式类型名称数量U2Access_flags1U2Name_index1U2Descriptor_index1U2Attributes_count1Attribute_infoAttributesAttributes_count在class文件中,每个在类和接口中声明的方法,或者由编译器产生的方法,都由一个可变长度的method_info来描述。同一个类中不能存在两个名字及描述符完全相同的方法。有可能在class文件中出现的两种编译器产生的方法是:实例初始化方法(名为<init>)和类与接口初始化方法(名为<clinit>)。Method_info表的格式如下:表method_info表的格式类型名称数量U2Access_flags1U2Name_index1U2Descriptor_index1U2Attributes_count1Attributes_infoAttributesAttributes_count属性在Javaclass文件中多处出现。它们可以出现在ClassFile、field_info、method_info和Code_attribute表中。Java虚拟机规范定义了9种属性,为了正确解释Javaclass文件,所有Java虚拟机实现都必须能够识别下列三种属性:Code,ConstatValue和Exception。为了正确地实现Java和Java2平台,虚拟机实现必须能够识别InnerClasses和Synthetic属性,但可以自主选择空间是识别还是忽略其他一些预定义的属性。由规范定义的attribute_info表的属性如下表:表由规范定义的attribute_info表的类型名称使用者描述CodeMethod_info方法的字节码和其他数据ConstantValueField_infoFianl变量的值DeprecatedField_info、method_info字段或者方法被禁用的指示符ExceptionsMethod_info方法可能抛出的可被检测的异常InnerClassesClassFile内部、外部类的列表LineNumberTableCode_attribute方法的行号与字节码的映射LocalVariableTableCode_attribute方法的局部变量的描述SourceFileClassFile源文件名SyntheticField_info、method_info编译器产生的字段或者方法的指示符Class文件在MiniJavaVM中的数据结构表示JavaVM工程的CClassFile类负责读取javaClass文件的内容,将class文件的二进制表示形式保存为虚拟机可以理解的数据结构。CClassFile类的设计如图所示:对于CClassFile类,最重要的就是读取指定Java类名的class文件,这个最重要的任务交给ReadClassFile()方法做。这个方法实现了三个功能:首先,找到该java类的class文件或是从文件中找到该java类的class文件,读取这个类文件的二进制数据,并提交下一步处理;第二,根据二进制数据填充ClassFile类中的变量,这些变量的定义与ClassFile表一致;最后,根据ClassFile类的变量整理出类的信息,填充到新的结构JavaClassInfo中。Execute()函数对于入口class有用,通过调用该class文件的Execute()函数,Java虚拟机能从这个函数的main()函数开始执行。ClassFile类的变量定义如图所示:Class文件常量池数据结构设计如下,所有的11种常量池表示类均从一个父类继承,并通过调用父类的静态成员函数GenerateCPInfo()来生成某一特殊类型的常量池:在cp_info的静态成员函数GenerateCPInfo()中,先通过读取第一个字节来得知这个常量池的类型,然后根据这个类型来生成11个常量池类型中的一种,其类型值在m_nTag中保存,并返回它的指针(统一为cp_info的指针在ClassFile类中保存)。当要使用常量池中的某一值时,通过m_nTag的值将这一值转为正确的类型并使用。Field_info类设计如下,这里必须保留ClassFile类的指针来获得对常量池信息的访问:Method_info类设计如下,这里必须保留ClassFile类的指针来获得对常量池信息的访问,同时,需要保留一个attr_Code的指针来指向这个方法的执行码。Attribute_info类设计如下,其中m_Type的值指定了这个attribute_info表示上述9种类型中的哪一种,ClassFile的指针用来访问这个class文件的常量池,m_pAttr这个attr_base的指针指向从attr_base派生出来的某一个子类。Attr_base与其子类的关系同上面cp_info与其子类。如下图所示:通过以上的设计和实现,JavaClass文件的格式已经被我们的MiniJavaVM虚拟机完全表示出来。类的装载和解析在Java虚拟机中,负责查找并装载类型的那部分被称为类装载器子系统。Java虚拟机有两种类装载器:启动类装载器和用户自定义类型装载器。前者是Java虚拟机实现的一部分,后者则是Java程序的一部分。由不同的类装载器装载的类将被放在虚拟机内部的不同命名空间中。对于每一个被装载的类型,Java虚拟机都会为它创建一个类的实例来代表该类型。和所有其他对象一样,用户自定义的类装载器以及Class类的实例都放在内存中的堆区,而装载的类型信息则都位为方法区。REF_Ref106004256h[7]类装载器子系统除了要定位和导入二进制class文件外,还必须负责验证被导入类的正确性,为类变量分配并初始化内存,以及帮助解析符号引用。这些动作必须严格按以下顺序进行:装载——查找并装载类型的二进制数据连接——执行验证,准备,以及解析(可选)验证确保被导入类型的正确性准备为类变量分配内存,并将其初始化为默认值解析把类型中的符号引用转换为直接引用初始化——把类变量初始化为正确初始值对于每一个被装载的类型,虚拟机都会在方法区中存储以下类型信息:这个类型的全限定名这个类型的直接超类的全限定名(除非这个类型是,它没有超类)这个类型是类类型还是接口类型这个类型的访问修饰符(public、abstract或final的某个子集)任何直接超接口的全限定名的有序列表除了上面的基本类型信息外,虚拟机还得为每个被装载的类型存储以下信息:该类型的常量池字段信息方法信息除了常量以外的所有类(静态)变量一个到类ClassLoader的引用一个到Class类的引用虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用常量的一个有序集合,包括直接常量(string、integer和floatingpoint常量)和对其他类型、字段和方法的符号引用。池中的数据项就像数组一样是通过索引访问的。因为常量池存储了相应类型所用到的所有类型、字段和方法的符号引用,所以它在Java程序的动态连接中起着核心的作用。对于类型中声明的每一个字段,方法区中必须保存下面的信息。除此之外,字段在类或者接口中的声明顺序也必须保存。下面是字段信息的清单:字段名字段的类型字段的修饰符对于类型中声明的每一个方法,方法区中必须保存下面的信息。和字段一样,这些方法在类或者接口中的声明顺序也必须保存。下面是方法信息的清单:方法名方法的返回类型(或void)方法的数量和类型(按声明顺序)方法的修饰符(public、private、protected、static、final、synchronized、native、abstract的某个子集)除上面的清单中列出的条目之外,如果某个方法不是抽象的和本地的,它还必须保存下列信息:方法的字节码(bytecodes)操作数栈和该方法的栈帧中的局部变量区的大小异常表类变量是由所有类实例共享的,但是即使没有任何类实例,它也可以被访问。这些变量只与类有关——而非类的实例,因此它们总是作为类型信息的一部分而存储在方法区。除了在类中声明的编译时常量外,虚拟机在使用某个类之前,必须在方法区中为这些变量分配空间。而编译时常量(就是那些用final声明以及用编译时已知的值初始化的类变量)则和一般的类变量的处理方式不同,每个使用编译时常量的类型都会复制它的所有常量到自己的常量池中,或嵌入到它的字节码流中。作为常量池或字节码流的一部分,编译时常量保存在方法区中——就和一般的类变量一样。但是当一般的类变量作为声明它们的类型的一部分数据而保存的时候,编译时常量作为使用它们的类型的一部分而保存。我们的MiniJavaVM内部类变量的处理方式如下:而对于类的非静态变量,由于非静态变量是对应于类的实例的,所以处理方法与类变量不同,类的非静态变量在我们的MiniJavaVM中的处理方式如下:每个类型被装载的时候,虚拟机必须跟踪它是由启动类装载器还是由用户自定义类装载器装载的。如果是用户自定义类装载器装载的,那么虚拟机必须在类型信息中存储对该装载器的引用。这是作为方法表中的类型数据的一部分保存的。虚拟机会在动态连接期间使用这个信息。当某个类型引用另一个类型的时候,虚拟机会请求装载发起引用类型的类装载器来装载被引用的类型。这个动态连接的过程,对于虚拟机分离命名空间的方式也是至关重要的。为了能够正确地执行动态连接以及维护多个命名空间,虚拟机需要在方法表中得知每个类都是由哪个类装载器装载的。对于每一个被装载的类型(不管是类还是接口),虚拟机都会相应地为它创建一个类的实例,而且虚拟机还必须以某种方式把这个实例和存储在方法区中的类型数据关联起来。在我们的MiniJavaVM中实例与每一个装载类的关系如下,其中虚拟机内部用哈希表来管理已装载类与类的实例引用的关系:内存管理对象、堆、方法区的管理在Java虚拟机中,关于被装载类型的信息存储在一个逻辑上称为方法区的内存中,所有线程共享方法区,因此它们对方法区数据的访问必须被设计为是线程安全的。方法区的大小不必是固定的,虚拟机可以根据应用的需要动态调整。同样,方法区也不必是连续的,方法区可以在一个堆(甚至是虚拟机自己的堆)中自由分配。另外,虚拟机也可以允许用户或者程序员指定方法区的初始大小以及最小和最大尺寸等。REF_Ref106003797h[4]Java虚拟机有一条在堆中分配新对象的指令,却没有释放内存的指令。正如无法用Java代码去明确释放一个对象一样,字节码指令也没有对应的功能。虚拟机自己负责如何以及何时释放不再被运行的程序引用的对象所占据的内存。程序本身不用去考虑何时需回收对象所占的内存,通常虚拟机把这个任务交给垃圾收集器。方法区也可以被垃圾收集。因为虚拟机允许用户定义的类装载器来动态扩展Java程序,因此一些类也会成为程序“不再引用”的类。当某个类变为不再被引用的类时,Java虚拟机可以卸载这个类(垃圾收集),从而使方法区占据的内存保持最小。为了简单,MiniJavaVM的方法区使用虚拟机自己的堆,不参与垃圾回收,同时,类的静态字段及一些特殊的类的实例(如与每个类相关的Class类的实例)也不参与垃圾回收。Java对象中包含的基本数据由它所属的类及其所有超类声明的实例变量组成。只要有一个对象引用,虚拟机就必须能够快速地定位对象实例的数据。另外,它也必须能通过该对象引用访问相应的类数据(存储于方法区的类型信息)。因此在对象中通常会有一个指向方法区的指针。一种可能的堆空间设计就是,把堆分成两部分:一个句柄池,一个对象池,而一个对象引用就是一个指向句柄池的本地指针。句柄池的每个条目有两部分:一个指向对象实例变量的指针,一个指向方法区类型数据的指针。这种设计的好处是有利于堆碎片整理,当移动对象池中的对象时,句柄部分只需要一更改一下指针指向的新地址就可以了——就是在句柄池中的那个指针。缺点是每次访问对象的实例变量都要经过再次指针传递。如图所示:另一种设计方式是使对象指针直接指向一组数据而该数据包括对象实例数据及指向方法区中类数据的指针。这个设计的优缺点正好与前面的方法相反,它只需要一个指针就可以访问对象的实例数据,但是移动对象就变得更加复杂。当使用这种堆的虚拟机为了减少内存碎片而移动对象的时候,它必须在整个运行时数据区中更新指向被移动对象的引用。见图所示:有如下几个理由要求虚拟机必须能够通过对象引用得到类(类型)数据:当程序在运行时需要转换某个对象引用为另一种类型时,虚拟机必须要检查这种转换是否被允许,被转换的对象是否的确是被引用的对象或者它的超类型。当程序在执行instanceof操作时,虚拟机也进行了同样的检查。在这两种情况下,虚拟机都需要查看被引用的对象的类型数据。最后,当程序中调用某个实例方法时,虚拟机必须进行动态绑定,换句话说,它不能按照引用的类型来决定将要调用的方法,而必须根据对象的实际类。为此,虚拟机必须再次通过对象的引用去访问类数据。REF_Ref106004869h[9]我们的MiniJavaVM内部通过实例的引用对象能快速查找到类数据是通过哈希表和第一种方法的结合实现的,其具体实现如图所示:这种组织的好处是对象的引用可以直接作为实例数据的起始地址使用,不再使用二重查找,这种组织对于数组结构更有效。同时,用哈希表来解决根据一个对象的引用找到指向类数据的指针的的问题,最大限度地解决了查找的时间问题。这种表示方法带来的好处在java数组表示中更明显。在我们的MiniJavaVM内部Java数组类型表示如图所示:这种设计同样用来完成我们的MiniJavaVM对象引用与类方法表的对应关系,如图所示:MiniJavaVM的垃圾回收过程当虚拟机在运行过程中堆已没有可用分配空间时,虚拟机会启动垃圾收集来回收可用空间。当一个对象不再被程序所引用时,它所使用的堆空间可以被回收,以便被后续的新对象所使用。垃圾收集器必须能判断哪些对象是不再被引用的,并且能够把它们所占据的堆空间释放出来。在释放不再被引用的对象的过程中,垃圾收集器运行将要被释放的对象的终结方法(finalize)。我们的MiniJavaVM的垃圾收集主要通过JavaVM工程的JavaVM类的如下接口实现:MiniJavaVM运行垃圾收集的流程如图所示:执行引擎——Java操作码实现Java虚拟机中的操作码功能分类执行引擎的抽象规范是根据指令集来定义的,Java中的操作码分为栈和局部变量操作指令,类型转换指令,整数运算指令,逻辑运算指令,浮点运算指令,对象和数组指令,控制流指令,异常处理指令,finally子句指令,方法调用与返回指令,线程同步指令。REF_Ref106004950h[10]栈和局部变量操作指令将常量池入指令aconst_null,iconst_m1,iconst_0,iconst_1,iconst_2,iconst_3,iconst_4,iconst_5,lconst_0,lconst_1,fconst_0,fconst_1,fconst_2,dconst_0,dconst_1,bipush,sipush,ldc,ldc_w,ldc_w从栈中的局部变量中装载值指令iload,lload,fload,dload,aload,iload_0,iload_1,iload_2,iload_3,lload_0,lload_1,lload_2,lload_3,fload_0,fload_1,fload_2,fload_3,dload_0,dload_1,dload_2,dload_3,aload_0,aload_1,aload_2,aload_3,iaload,laload,faload,daload,aaload,baload,caload,saload将栈中的值存入局部变量指令istore,lstore,fstore,astore,istore_0,istore_1,istore_2,istore_3,lstore_0,lstore_1,lstore_2,lstore_3,fstore_0,fstore_1,fstore_2,fstore_3,dstore_0,dstore_1,dstore_2,dstore_3,astore_0,astore_1,astore_2,astore_3,iastore,lastore,fastore,dastore,aastore,bastore,castore,sastore通用栈操作指令nop,pop,pop2,dup,dup_x1,dup2,dup2_x1,dup2_x2,swap类型转换指令i2l,i2f,i2d,l2i,l2f,l2d,f2i,f2l,f2d,d2i,d2l,d2f,i2b,i2c,i2s整数运算指令iadd,ladd,isub,lsub,imul,lmul,idiv,ldiv,irem,lrem,ineg,lneg,iinc逻辑运算指令移位操作指令ishl,lshl,ishr,lshr,iushr,lushr,按位布尔运算指令iand,land,ior,lor,ixor,lxor浮点运算指令fadd,dadd,fsub,dsub,fmul,dmul,fdiv,ddiv,frem,drem,fneg,dneg对象和数组指令对象操作指令new,checkcast,getfield,putfield,getstatic,putstatic,instanceof数组操作指令newarray,anewarray,arraylength,multinewarray控制流指令条件分支指令ifeq,ifne,iflt,ifge,ifgt,if_icmpeq,if_icmpne,if_icmplt,if_icmpge,if_icmpgt,if_icmple,ifnull,ifnonnull,if_acmpeq,if_acmpne比较指令lcmp,fcmpl,fcmpg,dcmpl,dcmpg无条件转移指令goto,goto_w表跳转指令tableswitch,lookupswitch异常指令athrowfinally子句指令jsr,jsr_w,ret方法调用与返回指令方法调用指令invokevirtual,invokespecial,invokestatic,invokeinterface方法返回指令ireturn,lreturn,freturn,dreturn,areturn,return线程同步指令monitorenter,monitorexit操作码功能实现——JavaOperatorExecute类我们的JavaVM工程中的JavaOperatorExecute类负责解释Java虚拟机中200多个操作码的功能。Java虚拟机的每一个或每几个类似功能的操作码都对应于JavaOperatorExecute类的一个方法,如bipush指令,对应于JavaOperatorExecute类中的function_bipush()方法,而iload,lload,dload,fload,aload指令,对应于JavaOperatorExecute类中的function_xload()方法。这些方法主要实现两个功能:一、按照操作码的功能说明实现这个操作码的功能,二、执行完这个操作码的功能后修改当前PC的值。在执行操作码的过程中,如果有抛出异常,则设置抛出异常的标志,JavaOperatorExecute类根据这个标志处理异常。对于一个非本地方法的操作码序列,在JavaOperatorExecute类的构造函数中得到这个方法的操作码序列和操作码长度,再通过一个大的switch-case结构找到这个操作码应该对应的JavaOperatorExecute中的方法,执行完这个方法后,如果还存在未执行的操作码或还没有抛出异常,则继续执行,否则该方法执行完毕。JavaOperatorExecute类的执行过程如下图所示:方法调用的实现Java中的方法调用Java程序设计语言提供了两种基本的方法:实例方法和类(或静态)方法。这两种方法的区别在于:REF_Ref106004117h[6]实例方法在被调用之前,需要一个实例,而类方法不需要实例方法使用动态(迟)绑定,而类方法使用静态(早)绑定当Java虚拟机调用一个类方法时,它会基于对象引用的类型(通常在编译时可知)来选择所调用的方法。相反,当虚拟机调用一个实例方法时,它会基于对象实际的类(只能在运行时得知)来选择所调用的方法。Java虚拟机使用两种不同的指令分别调用这两种方法。对于实例方法,使用invokevirtual指令,对于类方法,使用invokestatic指令。这两种指令如表所示:表方法调用操作码操作数说明InvokevirtualIndexbyte1,indexbyte2把objectref(对象引用)和args(参数)从栈中弹出,调用常量池索引指向的实例方法InvokestaticIndexbyte1,indexbyte2把args从栈中弹出,调用常量池索引指向的类方法尽管通常使用invokevirtual指令调用实例方法,但在某些特定的情况中,也会使用另外两种操作码——invokespecial和invokeinterface,如表所示表7.1.2 方法调用操作码操作数说明InvokespecialIndexbyte1,indexbyte2把objectref和args从栈中弹出,调用常量池索引指向的实例方法InvokeinterfaceIndexbyte1,indexbyte2把objectref和args从栈中弹出,调用常量池索引指向的实例方法当根据引用的类型来调用实例方法

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论