记录点点滴滴

Java(JVM)虚拟机结构基础(转)

上一篇 / 下一篇  2009-11-11 13:53:43 / 个人分类:JVM

 JVM执行的对象就是大家非常熟悉的class文件,我们也称为类文件,JVM规范定义的这个编译完成的代码文件(虽然并非强制要求是实际的文件)的格式非常的详实,但是我们这里只说一些宏观的内容,以后有机会再研究细节的内容吧。JVM要求的类文件的格式是和硬件和操作系统无关的一种二进制格式,它精确定义了类或者接口的表示,它甚至包含了字节顺序这样的细节,而字节顺序在特定平台的目标文件格式中一般都是固定的,不会进行说明。

  JVM所支持的数据类型和Java语言规范中定义的几乎一样,请注意是几乎一样!也就是原始类型和引用类型,他们可以被存储在变量表中,也可以作为参数传递、被方法返回,更通常的就是成为操作的对象。为什么和Java语言规范中定义的不完全一样呢?因为JVM中有一种Java语言所没有的原始类型:返回地址类型(returnAddress type)。该类型是jsr, ret以及jsr_w指令需要使用到的,它的值是JVM指令的操作码的指针,并且它的值是不能被运行中的程序所修改的。

  另外需要提到的就是布尔类型的值,虽然在Java语言中它是完全独立的值,但是在JVM中只提供了对它的有限支持,表现在:

  没有单独的操作布尔类型的指令,源代码中的布尔类型的操作在编译以后是作为int类型的值进行操作的。

  JVM直接支持布尔数组,newarray指令可以创建布尔数组,而它的访问和修改操作却是使用byte类型的数组的操作指令进行的:baload,bastore。(在JDK1.0,1,1以及1.2中,布尔数组被编码为byte数组,每个元素是8位)JVM用1代表true,用 0代表false,编译器将源代码中的布尔类型映射为JVM中的int类型,而且必须和JVM的要求一致。

  另外JVM规范中对于浮点类型的数据有大段的说明,我没有怎么看,主要是讨论JVM的浮点型和IEEE 754的关系的。

  关于类型的另外一个需要提一下的是类型检查。JVM期望几乎所有的类型检查已经在运行之前完成了(通常是由编译器进行检查的)而不用JVM自己来检查。原始类型的值不需要被标记或者在运行时被检查以确定他们的类型,同样他们也不用和引用类型的值进行区分,区分工作是由JVM的指令集来完成的,JVM的指令集使用不同指令来区分它要操作的值的类型,例如iadd, ladd, fadd以及dadd是用于将两个数字相加并产生数字类型结果的所有JVM指令,但是每个指令都是针对特定类型的,分别对应int, long, float以及double。

  JVM包含对对象的显式支持。类是动态分配的类实例或者是一个数组,JVM中的引用类型就是对一个对象的引用,引用类型的值可以想象为对象的指针,一个对象同时可能存在多个对它的引用,对象总是通过引用被操作、传递或者测试的。

  对于引用类型,需要提及的一点就是关于null,它最初是没有运行时类型的,但是它可以被转换为任何类型,而且对于null,JVM并没有要求任何具体的值与之对应。

  说完上面这些,我们就开始进入我学习JVM时最想了解的部分了,大家可要打起精神哦。

  JVM为运行一个程序定义了几种数据区(Data Area),包括:pc寄存器、JVM堆栈、堆、方法区(Method Area)、运行时常量池(Runtime Constant Pool)以及本机方法堆栈(Native Method Stacks),这些数据区根据其生存期可以分为两种,一种就是和JVM的生存期相同(包括堆和方法区),一种和线程的生存期相同(其它的),和JVM生存期相同的数据区在JVM启动的时候被创建并在JVM退出的时候被销毁,而和线程生存期相同的数据区是每个线程一个的,他们在线程创建的时候被创建,在线程被销毁的时候被销毁。

  由于JVM可以同时支持运行多个线程,因此每个线程必然需要各自的PC(program counter)寄存器,无论从什么角度讲,每个JVM线程只能在一个时间只能执行一个方法,该方法也就是线程的当前方法,如果该方法不是本机方法,那么 PC寄存器保存的就是当前指令(JVM的指令)的地址,如果是当前方法是本机方法,PC寄存器的值就没有被定义。JVM的PC寄存器的大小足够大,可以容纳一个returnAddress类型或者特定平台的本机指针。

  每个JVM线程还拥有一个私有的JVM堆栈,它存储帧(下一篇文章会讲到)。JVM堆栈和像C这样的传统编程语言中的堆栈是类似的,它保存局部变量和部分结果,并且在方法调用和返回中也担任一些职责。因为除了对帧的压入和弹出操作外,对JVM堆栈不能直接进行操作,因此帧可能是在堆上分配的。如果一个线程中计算所需的JVM堆栈大于允许的大小,JVM会抛出StackOverflowError错误,如果JVM堆栈是可以动态伸缩的,如果需要扩展,但是又没有足够的内存可用或者没有足够的内存为一个新线程创建JVM堆栈,JVM会抛出OutOfMemoryError错误。

  JVM只有一个为所有线程所共享的堆,所有的类实例和数组都是在堆中创建的。堆所存储的对象被一个自动存储管理系统回收(也就是我们所熟知的垃圾收集器 (gc))。对象不能被显式的释放,JVM假设没有特定类型的自动存储管理系统,存储管理技术可以根据实现者的系统需求进行选择。如果计算所需的内存堆大于自动存储管理系统可以使用的大小,JVM会抛出OutOfMemoryError错误。

  JVM只有一个为所有的线程所共享的方法区,方法区类似传统语言的已编译代码的存储区或者UNIX进程的“文本”段。它存储类结构,例如运行时常量池,成员和方法数据以及方法、构造方法的代码 (包括用于类和实例的初始化以及接口类型初始化的特定方法(这些特定方法以后会讲到))。虽然从逻辑上讲方法区是堆的一部分,但是JVM的简单实现可以选择不对方法区进行垃圾收集或者压缩(以笔者的理解就是类不能进行卸载)。最新版本(第二版)的JVM规范没有要求方法区的位置或者管理已编译代码的策略。如果方法区的内存不能满足一个分配请求,JVM会抛出OutOfMemoryError。

  运行时常量池是类文件中的常量池表的运行时表示,它包含几种常量,范围从编译时就已知的数字常量到运行时必须进行解析的方法和成员引用。运行时常量池扮演的功能类似于传统编程语言中的符号表(symbol table),但是它所包含的数据比典型的符号表更多。

  每个运行时常量池时从JVM的方法区中分配的,对于特定方法或者接口的运行时常量池是JVM在创建类或者接口的时候创建的。

  当创建一个类或者接口时,如果创建运行时常量池需要的内存比方法区中的可用内容更多的内存,JVM会抛出OutOfMemoryError。

  关于常量池创建的更多内容以后可能会更详细的讲解。

  JVM的实现可能使用传统的堆栈(更通常的讲就是C栈)以支持本机方法(不是使用JAVA语言编写的方法),本机方法堆栈也可以用于在像C语言这样的语言中为JVM指令集实现解析器,对于不能加载本机方法以及自身不依赖传统堆栈的JVM实现而言,它可以不提供本机方法堆栈,如果提供,本机方法堆栈通常在线程创建的时候为每个线程分配(以笔者的理解应该是需要使用本机方法的线程)。如果线程计算所需的内存比本机方法堆栈所允许的大,JVM会抛出 StackOverflowError错误,如果本机方法堆栈可以动态伸缩,而当需要扩展的时候又没有足够的内存时,或者没有足够的内容用于创建一个本机方法堆栈,JVM会抛出OutOfMemoryError。

  对于上面的这些数据区,JVM规范允许它们的大小是固定尺寸的,也可以是根据计算的需要动态伸缩的,如果是固定尺寸的,其尺寸可以在创建时自主选择。JVM的实现可以给程序员或者用户提供控制JVM堆栈的初始大小的方法,同样,在动态伸缩的情况下可以控制最大大小和最小大小,并且它们所使用的内存空间可以不是连续的。

下面通过对JVM堆栈的帧的详细介绍了解方法执行的一些内幕。

  帧通常用于存储数据和部分结果,同时还用于执行动态链接、返回方法的返回值以及分发异常。

  帧在方法调用的时候被创建,在方法完成的时候销毁。它是在创建它的线程的JVM堆栈中分配到空间的,每个帧都有它自己的局部变量数组、操作数堆栈和一个当前方法所在的类的运行时常量池的引用。

  它的局部变量数组和操作数堆栈的大小是在编译的时候就确定了的,而且它是和它所联系的方法的代码一起提供的,因此它的数据结构的尺寸仅仅依赖于JVM的实现和方法调用时同时可以分配的内存。

  对于正在执行的方法而言只有一个帧是活动的,这个帧就是所谓的当前帧,它的方法就是当前方法,当前方法所在的类被定义为当前类。局部变量和操作数堆栈的操作通常和当前帧有关。

  如果一个帧所在的方法调用了另外的方法或者方法结束,那么该帧不再是当前帧。如果是调用另外的方法,那么一个新的帧会被创建并且在控制权转换到新方法时成为当前帧;如果是方法结束,如果有方法返回,当前帧将它的方法调用的结果传递给前一个帧,当前一个帧成为当前帧时当前帧被丢弃。

  需要注意的是由一个线程创建的帧是局部于该线程的,其它的线程不能引用它。

  每个帧都包含变量数组,也就是我们所熟知的局部变量数组。一个局部变量可以保存一个boolean、 byte、char、short、int、float、引用或者returnAddress值,一对局部变量才能保存一个long或者double值。

  局部变量是根据索引进行寻址的,第一个局部变量的索引是0。如果一个整型值介于0和局部变量数组的长度之间并且也只有在这个区间的时候它才会被作为局部变量数组的索引。

  long型或者double型的值占用两个连续的局部变量,这样的值可能只能使用较小的那个索引值进行寻址,例如,局部变量数组中索引为n的 double变量值实际上占用n和n+1,但是局部变量n+1是不能读取的,它可以被写入,但是这样做会使得局部变量n的内容无效。JVM没有要求n是偶数,这就意味着double和long型值在局部变量数组中不必是64位对齐的,JVM的实现者可以决定使用适当的方式表示那样的值。

  JVM使用局部变量传递方法调用的参数,对于类方法调用(也就是static方法),所有的参数都是连续的存储在局部变量表中并且是从0开始的,对于实例方法调用,所有的参数也是连续的但是是从1开始的,局部变量0存储的是实例方法所在的类实例的引用。

  每个帧都包含一个后进先出的堆栈,也就是它的操作数堆栈。

  操作数堆栈在刚刚被创建的时候是空的,JVM提供指令从局部变量或者成员加载常量或者值到堆栈,其它的JVM指令从操作数堆栈提取操作数,操作它们并将结果放回操作数堆栈。操作数堆栈也用于准备传递给方法的参数以及接收方法的结果。

  例如一个iadd指令将两个int值相加,该指令要求它的前一条指令将它要相加的两个值压入操作数堆栈的最上面,它从操作数堆栈取出那两个值进行相加并将结果放回操作数堆栈。

  子计算可能是嵌套在操作数堆栈中的,产生的值可以被嵌入的计算使用。

  操作数堆栈的每一项都可以保存JVM的任何类型的值,包括long和double型的。

  操作数堆栈中的值必须根据其类型进行操作。下面的这些情况都是不可能的:压入两个int值而后续的操作将它们作为long型或者压入两个float值而后续的操作是iadd指令(该指令的操作对象是两个int型)。有一小部分JVM指令(例如dup和swap)将运行时数据区的值作为原始的值(raw value)进行操作而不考虑其类型,这些指令是以一种不能用于修改或者分解单独的值的方式定义的,这些对操作数堆栈操作的限制通过类文件验证进行了强制。

  在任何时候操作数堆栈都有其相应的深度,long或者double型的值是两个单位而其它的值是一个单位。

  每个帧都包含一个相应于当前方法的类型的运行时常量池的引用以支持方法代码的动态链接。类文件代码中的方法代码指的是被调用的方法以及通过符号引用可以访问的变量,动态链接将这些符号方法引用翻译为具体的方法引用、在必要的时候加载类以解析未定义的符号以及将变量访问翻译为那些变量的运行时位置在存储结构中的适当的偏移。方法和变量的晚期绑定使得方法使用到的其它类的变化可以破坏该代码的可能性更小。

  如果方法调用没有导致一个异常(无论是JVM抛出的还是代码显式抛出的)就被认为是方法调用正常结束。如果当前方法调用正常结束,那么一个值可能被返回给调用它的方法。

  在这种情况下,当前帧被用于恢复调用者的状态,包括它的局部变量和操作数堆栈以及适当增加程序计数器以跳过方法调用指令。方法调用者所在的帧的程序的执行正常的继续,如果有方法返回,返回值被压入帧的操作数堆栈。

  如果方法里面的一个JVM指令的执行引起JVM抛出一个异常并且那个异常在方法里面没有被处理就会导致方法调用突然结束,执行一个athrow指令也可以导致一个异常被显式的抛出并且如果那个异常没有被当前方法捕获也可以导致方法调用突然结束,一个突然结束的方法调用永远也不会向它的调用者返回一个值。

zmbbs=1;
  一个帧可能会被像调试信息这样的与实现相关的特定信息扩展。Java(JVM)虚拟机结构基础


TAG:

 

评分:0

我来说两句

Open Toolbar