在Java虚拟机规范中,一个虚拟机实例的行为是分别按照子系统、内存区、数据类型、指令这几个术语描述的。
这些组成部分一起展示了抽象的虚拟机的内部抽象体系结构。
每个Java虚拟机都有一个类装载器子系统,它根据给定的全限定名来装入类型(类或接口)。
每个Java虚拟机都有一个执行引擎,它负责执行那些包含在被装载类的方法中的指令。
当Java虚拟机运行一个程序时,它需要内存来存储许多东西,例如,字节码,从已装载的class文件中得到的其他信息,程序创建的对象,传递给方法的参数,返回值,局部变量,以及运算的中间结果等。Java虚拟机会把这些东西组织到几个"运行时数据区"中,以便于管理。
某些运行时数据区是由程序中所有线程共享的,有些则是只能由一个线程拥有。
每个Java虚拟机实例都有一个方法区以及一个堆,它们是由该虚拟机实例中所有线程共享的。
当虚拟机装载一个class文件时,它会从这个class文件包含的二进制数据中解析类型信息,并把这些类型信息放到方法区中。
当程序运行时,虚拟机会把所有该程序在运行时创建的对象都放到堆中。
当每一个新线程被创建时,它都将得到它自己的PC寄存器(程序计数器)以及一个Java栈。
如果线程正在执行的是一个Java方法(非本地方法),那么PC寄存器的值将总是指示下一条将被执行的指令,
Java栈存储该线程中Java方法调用的状态--包括它的局部变量、被调用时传进来的参数、它的返回值,以及运算的中间结果等等。
本地方法调用的状态,依赖于具体实现的方式存储在本地方法栈中,也可能是在寄存器或者其他某些与特定实现相关的内存区中。
Java栈是由许多栈帧组成的,一个栈帧包含一个Java方法调用的状态。
当线程调用一个Java方法时,虚拟机压入一个新的栈帧到该线程的Java栈中;
当该方法返回时,这个栈帧被从Java栈中弹出并抛弃。
Java虚拟机没有寄存器,其指令集使用Java栈来存储中间数据。
这样可以保持Java虚拟机的指令集尽量紧凑,也有助于某些虚拟机实现的动态编译器和即时编译器的代码优化。
Java虚拟机为每一个线程创建的内存区,这些内存区域是私有的。
任何线程都不能访问另一个线程的PC寄存器或者Java栈。
数据类型
Java语言中的所有基本类型同样都是Java虚拟机中的基本类型。
但是指令集对boolean只有很有限的支持。
当编译器把Java源码编译为字节码时,它会用int或byte表示boolean。
returnAddress,被用来实现Java程序中的finally字句。
引用,是对动态创建对象的引用。
类类型的值是对类实例的引用;
数组类型的值是对数组对象的引用,在Java虚拟机中,数组是个真正的对象;
接口类型的值,则是对实现了该接口的某个类实例的引用。
还有种特殊的引用值是null,它表示该引用变量没有运用任何对象。
字长的考量
在Java虚拟机中,最基本的数据单元是字(word),它的大小是由每个虚拟机实现的设计者来决定的。
字长必须足够大,至少是一个子单元就足以持有byte、short、int、char、float、returnAddress或reference类型的值,
而两个字单元就足以持有long或者double类型的值。
因此,虚拟机实现的设计者至少得选择32位作为字长,或者选择更为高效的字长大小。
通常根据底层主机平台的指针长度来选择字长。
在Java虚拟机规范中,关于运行时数据区的大部分内容,都是基于“字”这个抽象概念的。
比如,关于栈帧的两个部分--局部变量和操作数栈--都是按照“字”来定义的。
这些内存区域能够容纳任何虚拟机数据类型的值,当把这些值放到局部变量或者操作数栈中时,它将占用一个或两个字单元。
在运行时,Java程序无法侦测到底层虚拟机的字长大小;
同样,虚拟机的字长大小也不会影响程序的行为--它仅仅是虚拟机实现的内部属性。
类装载器子系统
在Java虚拟机中,负责查找并装载类型的那部分被称为类装载器子系统。
Java虚拟机有两种类装载器:启动类装载器+用户自定义类装载器。
启动类装载器是Java虚拟机实现的一部分,用户自定义类装载器则是Java程序的一部分。
类装载器子系统涉及Java虚拟机的其他几个组成部分,以及几个来自java.lang库的类。
比如,用户自定义的类装载器是普通的Java对象,它的类必须派生自java.lang.ClassLoader类。
ClassLoader中定义的方法为程序提供了访问类装载器机制的接口,
此外,对于每一个被装载的类型,Java虚拟机都会为他创建一个java.lang.Class类的实例来代表该类型。
和所有其他对象一样,用户自定义的类装载器以及class类的实例都放在内存中的堆区,而装载的类型信息则都位于方法区。
装载、连接以及初始化
类装载器子系统除了要定位和导入二进制class文件外,还必须负责验证被导入类的正确性,为类变量分配并初始化内存,以及帮助解析符号引用。这些动作必须严格按以下顺序进行:
1)装载----查找并装载类型的二进制数据
2)连接----执行验证,准备,以及解析(可选)
验证 确保被导入类型的正确性
准备 为类变量分配内存,将其初始化为默认值
解析 为类型中的符号引用转换为直接引用
3)初始化----把类变量初始化为正确初始值
启动类装载器 只要是符合Java class文件格式的二进制文件,Java虚拟机实现都必须能够从中辨别并装载其中的类和接口。
某些虚拟机实现也可以识别其他的非规范的二进制格式文件,但它必须能够辨别class文件。
每个Java虚拟机实现都必须有一个启动类装载器,它知道怎么装载受信任的类,比如Java API的class文件。
Java虚拟机规范并未规定启动类装载器如何去寻找class文件,这又是一件保留给具体的实现设计者去决定的事情。
只要给定某个类型的全限定名,启动类装载器就必须能够以某种方式得到定义该类型的数据。
用户自定义类装载器 是Java程序的一部分,但类ClassLoader中的四个方法是通往Java虚拟机的通道:
defineClass
findSystemClass
resolveClass
任何Java虚拟机实现都必须把这些方法连到内部的类装载器子系统
每个Java虚拟机实现都必须保证ClassLoader类的defineClass()方法能够把新类型导入到方法区中。
任何Java虚拟机实现都必须保证findSystemClass()能够以这种方式调用启动类装载器或者系统类装载器
resolveClass() 将对class实例表示的类型执行连接动作,
defineClass()只负责装载,当方法返回一个Class实例时,也就表示指定的class文件已经被找到并装载到方法区了,但却不一定被连接和初始化。Java虚拟机实现必须保证ClassLoader类的resolveClass方法能够让类装载器子系统执行连接操作。
命名空间
每个类装载器都有自己的命名空间,其中维护着由它装载的类型。
所以一个Java程序可以多次装载具有同一个全限定名的多个类型。
当多个类加载器都装载了同名的类型时,为了唯一地标识该类型,还要再类型名称前面加上装载该类型的类装载器标识。
Java虚拟机中的命名空间,其实就是解析过程的结果。
对于每一个被装载的类型,Java虚拟机都会记录装载它的类装载器。
当虚拟机解析一个类到另一个类的符号引用时,它需要被引用类的类装载器。
方法区
在Java虚拟机中,关于被装载类型的信息存储在一个逻辑上被称为方法区的内存中。
当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件(一个线性二进制数据流),然后将它传输到虚拟机中。紧接着虚拟机提取其中的类型信息,并将这些信息存储到方法区中。该类型中的类(静态)变量同样也是存储在方法区中。
Java虚拟机在内部如何存储类型信息,这是由具体实现的设计者来决定的。
比如,在class文件中,多字节值总是以高位在前(即代表较大数的字节在前)的顺序存储。
但是,当这些数据被引入到方法区后,虚拟机可以以任何方式存储它。
假设某个实现是运行在低位优先的处理器上,那么它很可能会把字节值以低位优先的顺序存储到方法区。
当虚拟机运行Java程序时,它会查找使用存储在方法区中的类型信息。
设计者应当为类型信息的内部表示设计适当的数据结构,以尽可能在保持虚拟机小巧紧凑的同时加快程序的运行效率。
方法区的大小不必是固定的,虚拟机可以根据应用的需要动态调整;
方法区也不必是连续的,方法区可以在一个堆中自由分配。
方法区也可以被垃圾收集。
类型信息
虚拟机必须为每个装载的类型维护一个常量池,是该类型所用常量的一个有序集合,
包括直接常量(string、integer、floating point)和对其他类型、字段和方法的符号引用。
池中的数据项就像数组一样是通过索引访问的。
常量池存储了相应类型所用到的所有类型、字段和方法的符号引用。
字段信息
对于类型中的声明的每一个字段,方法区中必须保存下面的信息,这些字段在类或者接口中的声明顺序也必须保存。
字段名
字段类型
字段的修饰符(public、private、protected、static、final、volatile、transient的某个子集)
方法信息
对于类型中声明的每一个方法,方法区中必须保存下面的信息,这些方法在类或者接口中的声明顺序也必须保存。
方法名
方法的返回类型(或void)
方法参数的数量和类型(按声明顺序)
方法的修饰符(public、private、protected、static、final、synchronized、native、abstract的某个子集)
除上面的清单中列出的条目之外,如果某个方法不是抽象的和本地的,它还必须保存下列信息:
方法的字节码(bytecodes)
操作数栈和该方法的栈帧中的局部变量区的大小
异常表
类(静态)变量,由所有类实例共享的,但是即使没有任何类实例,它也可以被访问。
这些变量只与类有关--而非类的实例,因此类(静态)变量作为类型信息的一部分而存储在方法区中。
除了在类中声明的编译时常量外,虚拟机在使用某个类之前,必须在方法区中为这些类变量分配空间。
编译时常量(就是那些用final声明以及用编译时已知的值初始化的类变量),
都会复制它的所有常量到自己的常量池中,或嵌入到它的字节码流中。
作为常量池或字节码流的一部分,编译时常量保存在方法区中。
一般的类变量作为声明它们的类型的一部分数据而保存,编译时常量作为使用它们的类型的一部分而保存。