Java虚拟机体系结构分析
下图是JAVA虚拟机的结构图:
每个Java虚拟机都有一个类装载子系统,它根据给定的全限定名来装入类型(类或接口)。同样,每个Java虚拟机都有一个执行引擎,它负责执行那些包含在被装载类的方法中的指令。 当JAVA虚拟机运行一个程序时,它需要内存来存储许多信息,Java虚拟机把这些信息都组织到几个“运行时数据区”中,以便于管理。运行时数据区共包括五个部分(方法区、Java堆区、Java栈区、程序计数器、本地方法栈)。
1.类加载子系统
在JAVA虚拟机中,负责查找并加载类型的那部分被称为类加载子系统。
类加载器子系统在Java虚拟机中实现了类加载的全过程动作,这些动作必须严格按以下顺序进行:
(1)加载——主要完成3件事:
1.通过一个类的全限定名来获取定义此类的二进制字节流。
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
(2)连接——指向验证、准备、以及解析(可选)。
● 验证 确保Class文件的字节流中包含的的信息符合当前虚拟机的要求,并且不会危害到虚拟机自身的安全。
● 准备 为类变量分配内存,并将其初始化为默认值。
● 解析 把类型中的符号引用转换为直接引用。
(3)初始化——把类变量初始化为正确初始值。
类加载器:用来加载 Java 类到 Java 虚拟机中,实现“通过一个类的全限定名来获取描述此类的二进制字节流”动作的代码模块。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器都拥有一个独立的类名称空间。也就是说,比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。
JAVA虚拟机有两种类加载器:启动类加载器和用户自定义类加载器。前者由C++实现,是JAVA虚拟机实现的一部分;后者则是由Java程序实现,独立于虚拟机外部。
2.运行时数据区
2.1方法区(线程共享)
用于存放已被虚拟机加载的类信息、常亮、静态变量、及时编译后的代码等数据。Java虚拟机规范把方法区描述为堆的一个逻辑部分,但为了将其与Java堆区分开来,称其别名为:Non-Heap(非堆)。
对于每个装载的类型,虚拟机都会在方法区中存储以下类型信息:
● 这个类型的全限定名
● 这个类型的直接超类的全限定名(除非这个类型是java.lang.Object,它没有超类)
● 这个类型是类类型还是接口类型
● 这个类型的访问修饰符(public、abstract或final的某个子集)
● 任何直接超接口的全限定名的有序列表
除了上面列出的基本类型信息外,虚拟机还得为每个被装载的类型存储以下信息:
● 该类型的常量池
● 字段信息
● 方法信息
● 除了常量以外的所有类(静态)变量
● 一个到类ClassLoader的引用
● 一个到Class类的引用
2.1.1常量池
存放字符串常量和基本类型常量(public static final)和符号引用。虚拟机必须为每个被加载的类型维护一个常量池。常量池就是该类型所用常量的一个有序集合,包括直接常量和对其他类型、字段和方法的符号引用,池中的数据项就像数组一样是通过索引访问的。因为常量池存储了相应类型所用到的所有类型、字段和方法的符号引用,所以它在Java程序的动态连接中起着核心的作用。
2.1.2字段信息
对于类型中声明的每一个字段。方法区中必须保存下面的信息。除此之外,这些字段在类或者接口中的声明顺序也必须保存。
○ 字段名
○ 字段的类型
○ 字段的修饰符(public、private、protected、static、final、volatile、transient的某个子集)
2.1.3方法信息
对于类型中声明的每一个方法,方法区中必须保存下面的信息。和字段一样,这些方法在类或者接口中的声明顺序也必须保存。
○ 方法名
○ 方法的返回类型(或void)
○ 方法参数的数量和类型(按声明顺序)
○ 方法的修饰符(public、private、protected、static、final、synchronized、native、abstract的某个子集)
除了上面清单中列出的条目之外,如果某个方法不是抽象的和本地的,它还必须保存下列信息:
○ 方法的字节码(bytecodes)
○ 操作数栈和该方法的栈帧中的局部变量区的大小
○ 异常表
2.1.4类(静态)变量
类变量是由所有类实例共享的,但是即使没有任何类实例,它也可以被访问。这些变量只与类有关——而非类的实例,因此它们总是作为类型信息的一部分而存储在方法区。除了在类中声明的编译时常量外,虚拟机在使用某个类之前,必须在方法区中为这些类变量分配空间。
而编译时常量(就是那些用final声明以及用编译时已知的值初始化的类变量)则和一般的类变量处理方式不同,每个使用编译时常量的类型都会复制它的所有常量到自己的常量池中,或嵌入到它的字节码流中。作为常量池或字节码流的一部分,编译时常量保存在方法区中——就和一般的类变量一样。但是当一般的类变量作为声明它们的类型的一部分数据面保存的时候,编译时常量作为使用它们的类型的一部分而保存。
2.1.5指向ClassLoader类的引用
每个类型被装载的时候,虚拟机必须跟踪它是由启动类装载器还是由用户自定义类装载器装载的。如果是用户自定义类装载器装载的,那么虚拟机必须在类型信息中存储对该装载器的引用。这是作为方法表中的类型数据的一部分保存的。
虚拟机会在动态连接期间使用这个信息。当某个类型引用另一个类型的时候,虚拟机会请求装载发起引用类型的类装载器来装载被引用的类型。这个动态连接的过程,对于虚拟机分离命名空间的方式也是至关重要的。为了能够正确地执行动态连接以及维护多个命名空间,虚拟机需要在方法表中得知每个类都是由哪个类装载器装载的。
2.1.6指向Class类的引用
对于每一个被装载的类型(不管是类还是接口),虚拟机都会相应地为它创建一个java.lang.Class类的实例,而且虚拟机还必须以某种方式把这个实例和存储在方法区中的类型数据关联起来。
方法区使用实例详见参考一
2.2Java堆(线程共享)
Java程序在运行时创建的所有类实例对象(包括String对象)或数组都放在同一个堆中。而一个JAVA虚拟机实例中只存在一个堆空间,因此所有线程都将共享这个堆。又由于一个Java程序独占一个JAVA虚拟机实例,因而每个Java程序都有它自己的堆空间——它们不会彼此干扰。但是同一个Java程序的多个线程却共享着同一个堆空间,在这种情况下,就得考虑多线程访问对象(堆数据)的同步问题了。
JAVA 堆是垃圾收集器管理的主要区域,JAVA虚拟机有一条在堆中分配新对象的指令,却没有释放内存的指令,正如你无法用Java代码区明确释放一个对象一样。虚拟机自己负责决定如何以及何时释放不再被运行的程序引用的对象所占据的内存。通常,虚拟机把这个任务交给垃圾收集器。
↑
关于Java堆和Java栈存储内容简单的区分记忆:凡是new出来的对象,都存在堆区;凡是引用都存在栈区★
↓
2.3Java栈 (线程私有)
存放基本类型的值,对象的引用(包括String引用、数组引用)和 returnAddress,在编译期间完成分配。
每当启动一个新线程时,Java虚拟机都会为它分配一个Java栈。Java栈以帧为单位保存线程的运行状态。虚拟机只会直接对Java栈执行两 种操作:以帧为单位的压栈和出栈。
某个线程正在执行的方法被称为该线程的当前方法,当前方法使用的栈帧称为当前帧,当前方法所属的类称为当前类,当前类的常量池称为当前常量池。在线程执行一个方法时,它会跟踪当前类和当前常量池。此外,当虚拟机遇到栈内操作指令时,它对当前帧内数据执行操作。
每当线程调用一个Java方法时,虚拟机都会在该线程的Java栈中压入一个新帧。而这个新帧自然就成为了当前帧。在执行这个方法时,它使用这个帧来存储参数、局部变量、中间运算结果等数据。
Java方法可以以两种方式完成。一种通过return返回的,称为正常返回;一种是通过抛出异常而异常终止的。不管以哪种方式返回,虚拟机都会将当前帧弹出Java栈然后释放掉,这样上一个方法的帧就成为当前帧了。
Java帧上的所有数据都是此线程私有的。任何线程都不能访问另一个线程的栈数据,因此我们不需要考虑多线程情况下栈数据的访问同步问题。当一个线程调用一个方法时,方法的的局部变量保存在调用线程Java栈的帧中。只有一个线程能总是访问那些局部变量,即调用方法的线程。
两个局部变量相加时栈帧的内存快照如下:
2.4程序计数器 (线程私有)
对于一个运行中的Java程序而言,其中的每一个线程都有它自己的PC(程序计数器)寄存器,用来指示下一条指令的地址。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。它是在该线程启动时创建的,PC寄存器的大小是一个字长(因此它既能够持有一个本地指针,也能够持有一个returnAddress)。当线程执行某个Java方法时,PC寄存器的内容总是下一条将被执行指令的“地址”,这里的“地址”可以是一个本地指针,也可以是在方法字节码中相对于该方法起始指令的偏移量。如果该线程正在执行一个本地方法,那么此时PC寄存器的值是"undefined"。
2.5本地方法栈 (线程私有)
这是 Java 调用操作系统本地库的地方,它与Java栈的区别是Java虚拟机为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机执行使用到的Native方法服务。当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。在虚拟机规范中对本地方法栈中使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。
任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈。然而当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。
如果某个虚拟机实现的本地方法接口是使用C连接模型的话,那么它的本地方法栈就是C栈。当C程序调用一个C函数时,其栈操作都是确定的。传递给该函数的参数以某个确定的顺序压入栈,它的返回值也以确定的方式传回调用者。同样,这就是虚拟机实现中本地方法栈的行为。
很可能本地方法接口需要回调Java虚拟机中的Java方法,在这种情况下,该线程会保存本地方法栈的状态并进入到另一个Java栈。
下图描绘了这样一个情景,就是当一个线程调用一个本地方法时,本地方法又回调虚拟机中的另一个Java方法。这幅图展示了JAVA虚拟机内部线程运行的全景图。一个线程可能在整个生命周期中都执行Java方法,操作它的Java栈;或者它可能毫无障碍地在Java栈和本地方法栈之间跳转。
该线程首先调用了两个Java方法,而第二个Java方法又调用了一个本地方法,这样导致虚拟机使用了一个本地方法栈。假设这是一个C语言栈,其间有两个C函数,第一个C函数被第二个Java方法当做本地方法调用,而这个C函数又调用了第二个C函数。之后第二个C函数又通过本地方法接口回调了一个Java方法(第三个Java方法),最终这个Java方法又调用了一个Java方法(它成为图中的当前方法)。
3.执行引擎
Java虚拟机的核心,控制装入 Java 字节码并解析;对于运行中的Java程序而言,每一个线程都是一个独立的虚拟机执行引擎的实例,从线程生命周期的开始到结束,他要么在执行字节码,要么在执行本地方法。
4.本地接口
连接了本地方法栈和操作系统库。