Java虚拟机体内部系结构包括class文件、类装载子系统、运行时数据区、执行引擎、本地方法调用结构,其中运行时数据区包括方法区、堆、Java栈、程序计数器、本地方法栈等。具体结构如下图所示(摘自Inside Java Virtual Machine):
1. class文件
在Java中,所有源文件都编译成二进制的字节码,然后由虚拟机装载运行。一般这样的字节码是以class文件的形式存在。在运行时,由ClassLoader类(System ClassLoader or User-defined ClassLoader)找到对应的class文件,读取其中的字节码,然后交由虚拟机解析运行。
在class文件中,包含了定义一个类或接口的所有信息,包括类名、访问权限、父类名、继承的所有接口、所有字段、所有方法、方法中的代码、属性等信息,并且每个class文件的开头还包含了魔术值和版本信息,魔术值用以标识当前的字节码是合法的字节码,版本表示生成当前字节码的编译器版本,从而虚拟机获知其版本而做特定处理,如果对于虚拟机不支持的字节码版本号拒绝加载。
在class文件中,很多信息都是以字符串的形式存放,比如对外部类成员或方法的引用,这些字符串信息在链接的时候由虚拟机解析。每个Java类,不管是包成员类还是内部类都会生成一个单独的class文件,因而class文件是相对独立的。详细信息参考class文件格式。
2. 类装载子系统
类装载子系统负责查找class文件,读取字节码,做部分简单的检验,如魔数是否正确,版本是否受支持,各种数据格式是否正确等。部分解析后的字节码数据存放到方法区中,最后创建字节码代表的类或接口的Class实例。
在Java中,类装载系统是通过ClassLoader来完成的。虚拟机规范中,定义了启动类装载器和用于定义类装载器。在sun提供的虚拟机中,包括了启动类装载器、扩展类装载器、系统类装载器、用户定义类装载器。他们以父子链的方式组织在一起。除了启动类装载器,其他的装载器都是ClassLoader的子类。ClassLoader定义了一些方法可以帮助用户定义自己的类装载器,如defineClass等。详情参考Java中的ClassLoader。
如何卸载类数据?(第七章)
3. 运行时数据区
运行时数据区保存了所有在运行时的信息。包括方法区、Java栈、堆、程序寄存器、本地方法栈等。其中方法区和堆只在虚拟机中保存一份实例,因而需要处理多线程的同步问题;Java栈、程序寄存器是每个线程中有单独的实例,因而对不同的线程,他们的数据是私有的。
3.1 方法区
方法区中保存了读取的字节码信息(包括常量池,静态方法和静态成员信息)、字节码代表的Class类实例、一个指向加载它的ClassLoader实例。
Java程序可以有两种方式来获取某个类的Class实例:
1. Class.forName()方法
2. Object.getClass()方法
通过Class实例获取和该类或接口相关的任何信息。参考Class类的定义。
(注:对有启动ClassLoader加载的类,Class方法中的getClassLoader方法返回null)
为加快执行速度,可以在方法区中引入方法表机制,记录能被外界调用的该类的实例方法,包括父类中继承下来的方法。(第八章详细介绍?)
方法区中根据类名搜索类信息,算法:散列、搜索树等。
3.2 Java栈
虚拟机为每个线程生成一个Java栈,因而对不同的线程,栈内的数据都是私有的。Java栈由栈帧组成,Java栈的操作只有两种,压入栈帧和弹出栈帧。线程中每个方法的调用都会在Java栈压入一个栈帧;每次方法返回(正常方法或抛异常返回),该方法对应的栈帧都会从栈中弹出。
3.2.1 栈帧
栈帧由操作数栈、局部变量区和栈帧数据组成。由于Java中的指令是基于栈而设计的,因而很多指令的默认操作数就是操作数栈中的数据。操作数栈用于保存指令的操作数和指令操作后的结果。
局部变量区用于保存当前方法的局部变量。
栈帧数据区则保存当前栈帧的信息,如指向当前类常量池的指针,用于操作数为常量池索引的指令;还有一些和特定虚拟机实现相关的信息和调试信息。
3.3 程序寄存器
每个线程在执行时都会保存当前指令的下一条指令的地址,以控制程序的之行流程。
3.4 堆
堆保存了程序在运行时的所有对象。在Java中,所有的对象都是保存在堆中的,而外部通过对象的引用来访问对象。由于Java存在垃圾回收器,因而Java对象可能被移动,以减少内存碎片。其中一种实现可以很好的解决移动对象而需要改变所有该对象的引用变量的技术,即将堆分为句柄池和对象池。对象池中的对象保存了对象的真正内容,而句柄池中的项包含两个指针,一个指向对象,一个指向类数据。一个对象引用就是指向句柄的对象指针。这样当需要移动对象时,对象的地址更改以后,就不需要将每个引用的值都进行更改,只要改变句柄池中指向对象的指针值即可。然而这种设计是以牺牲速度为代价的,因为这样每次访问对象就要多经历一次指针定位。
在某些垃圾回收器实现中,对象需要额外的信息,如果引用计数的垃圾收集器,需要为每个对象记录引用计数信息;而对另外有些机制,则可能需要暂时保存某些数据。这些额外的数据可以保存在类中,也可以在记录在其他地方。类似的还有同步机制中的数据和记录是否已经调用过finalize方法的信息。
在Java中有指令用于在内存中分配对象,却没有显式的指令来释放内存中的对象。
3.5 本地方法栈
当Java方法调用本地方法的时候,当前线程的程序寄存器是不确定的值。程序的执行也转向本地方法。本地方法可以正常返回,也可以抛出异常。抛出的异常会在调用该本地方法的指令中重新抛出。
4. 执行引擎
每个用户线程(即不包括垃圾回收线程等)都有一个执行引擎实例,用以执行字节码指令。
5. 本地方法接口
Java程序可以通过本地方法接口来调用本地方法。