Java虚拟机之所以被称之为是“虚拟”的,就是因为它仅仅是由一个规范来定义的抽象计算机。
3.1 Java虚拟机是什么
要理解Java虚拟机,首先要意识到,当你说“Java虚拟机”时,可能指的是如下三种不同的东西:
- 抽象规范
- 一个具体的实现
- 一个运行中的虚拟机实例
3.2 Java虚拟机的生命周期
一个运行时的Java虚拟机实例的天职是:负责运行一个Java程序。当启动一个Java程序时,一个虚拟机实例也就诞生了。当该程序关闭退出,这个虚拟机实例也就随之消亡。如果在同一台计算机上同时运行三个Java程序,将得到三个Java虚拟机实例。每个Java程序都运行于它自己的Java虚拟机实例中。
在Java虚拟机内部有两种线程:守护线程与非守护线程。守护线程通常是由虚拟机自己使用的,比如执行垃圾收集任务的线程。但是,Java程序也可以把它创建的任何线程标记为守护线程。而Java程序中的初始线程——就是开始于main( )的那个,是非守护线程。
只要还有任何非守护线程在运行,那么这个Java程序也在继续运行(虚拟机仍然存活)。当该程序中所有的非守护线程都终止时,虚拟机实例将自动退出。假若安全管理器允许,程序本身也能够调用Runtime类或者System类的exit( )方法来退出。
3.3 Java虚拟机的体系结构
图3-1 Java虚拟机的内部体系结构.png当Java虚拟机运行一个程序时,它需要内存来存储许多东西,例如,字节码,从已装载的class文件中得到的其他信息,程序创建的对象,传递给方法的参数,返回值,局部变量,以及运算的中间结果等。Java虚拟机把这些东西都组织到几个“运行时数据区”中,以便于管理。
图3-2 由所有线程共享的运行时数据区.png每个Java虚拟机实例都有一个方法区以及一个堆,它们是由该虚拟机实例中所有线程共享的。
当每个新线程被创建时,它都将得到它自己的PC寄存器(程序计数器)以及一个Java栈。Java栈是由许多栈帧(stack frame)或者说帧(frame)组成的,一个栈帧包含一个Java方法调用时的状态。
图3-3 线程专有的运行时数据区.pngJava虚拟机没有寄存器,其指令集使用Java栈来存储中间数据。
数据类型
Java虚拟机的数据类型可以分为两种:基本类型和引用类型。
图3-4 Java虚拟机中的数据类型.pngJava语言中的所有基本类型同样也都是Java虚拟机中的基本类型。但是boolean有点特别,虽然Java虚拟机也把boolean看做基本类型,但是指令集对boolean只有很有限的支持。当编译器把Java源码编译为字节码时,它会用int或byte来表示boolean。
Java虚拟机中还有一个只在内部使用的基本类型:returnAddress,Java程序员不能使用这个类型。这个基本类型被用来实现Java程序中的finally字句。
字长的考量
在运行时,Java程序无法侦测底层虚拟机的字长大小;同样,虚拟机的字长大小也不会影响程序的行为——它仅仅是虚拟机实现的内部属性。
类装载器子系统
Java虚拟机有两种类装载器:启动类装载器和用户自定义类装载器。对于每一个被装载的类型,Java虚拟机都会为它创建一个java.lang.Class
类的实例来代表该类型。和所有其他对象一样,用户自定义的类装载器以及Class类的实例都放在内存中的堆区,而装载的类型信息则都位于方法区。
装载,连接以及初始化。类装载器子系统除了要定位和导入二进制class文件外,还必须负责验证被导入类的正确性,为类变量分配并初始化内存,以及帮助解析符号引用。这些动作必须严格按以下顺序进行:
- 装载——查找并装载类型的二进制数据。
- 连接——执行验证,准备,以及解析(可选)。
- 初始化——把类变量初始化为正确初始值。
用户自定义类装载器。类ClassLoader中的四个方法是通往Java虚拟机的通道:
protected final Class defineClass(String name, byte data[], int offset, int length);
protected final Class defineClass(String name, byte data[], int offset, int length, ProtectionDomain protectionDomain);
protected final Class findSystemClass(String name);
protected final void resolveClass(Class c);
任何Java虚拟机实现都必须把这些方法连到内部的类装载器子系统中。每个Java虚拟机实现都必须保证
- ClassLoader类的defineClass( )方法能够把新类型导入到方法区中。
- findSystemClass( )方法使用系统类装载器(版本1.2以上)装载指定类型。
- resolveClass( )方法能够对类装载器子系统执行连接动作。
命名空间。每个类装载器都有自己的命名空间,其中维护着由它装载的类型。Java虚拟机中的命名空间,其实是解析过程的结果。
方法区
当虚拟机运行Java程序时,它会查找使用存储在方法区中的类型信息。设计者应当为类型信息的内部表示设计适当的数据结构,以尽可能在保持虚拟机小巧紧凑的同时加快程序的运行效率。
由于所有线程都共享方法区,因此它们对方法区数据的访问必须被设计为是线程安全的。
方法区的大小不必是固定的,虚拟机可以根据应用的需要动态调整。同样,方法区也不必是连续的,方法区可以在一个堆(甚至是虚拟机自己的堆)中自由分配。另外,虚拟机也可以允许用户或者程序员指定方法区的初始大小以及最小和最大尺寸等。
方法区也可以被垃圾收集。当某个类变为不再被引用的类时,Java虚拟机可以卸载这个类(垃圾收集),从而使方法区占据的内存保持最小。
类型信息。对于每个被装载的类型,虚拟机都会在方法区中存储以下类型信息:
- 这个类型的全限定名
- 这个类型的直接超类的全限定名(除非这个类型是
java.lang.Object
,它没有超类) - 这个类型是类类型还是接口类型
- 这个类型的访问修饰符(public、abstract或final的某个子集)
- 任何直接超接口的全限定名的有序列表
在Java源代码中,全限定名由类所属包的名称加上一个“.”,再加上类名组成。例如,类Object的所属包为java.lang
,那么它的全限定名应该是java.lang.Object
,但在class文件里,所有的“.”都被斜杠“/”代替,这样就成为java/lang/Object
。至于全限定名在方法区中的表示,则因不同的设计者决定,可以用任何形式和数据结构来代表。
除了上面列出的基本类型信息外,虚拟机还得为每个被装载的类型存储以下信息:
- 该类型的常量池
- 字段信息
- 方法信息
- 除了常量以外的所有类(静态)变量
- 一个到类ClassLoader的引用
- 一个到Class类的引用
给定一个指向Class对象的引用,就可以通过Class类中定义的方法来找出这个类型的相关信息。Class类使得运行程序可以访问方法区中保存的信息:
public String getName();
public Class getSuperClass();
public boolean isInterface();
public Class[] getInterfaces();
public ClassLoader getClassLoader();
堆
一个Java虚拟机实例中只存在一个堆空间,因此所有线程都将共享这个堆。又由于一个Java程序独占一个Java虚拟机实例,因而每个Java程序都有它自己的堆空间——它们不会彼此干扰。
Java虚拟机有一条在堆中分配新对象的指令,却没有释放内存的指令。虚拟机自己负责决定如何以及何时释放不再被运行的程序引用的对象所占据的内存。
和方法区一样,堆空间也不必是连续的内存区。在程序运行时,它可以动态扩展或收缩。某些实现可能也允许用户或程序员指定堆的初始大小、最大最小值等。
Java对象中包含的基本数据由它所属的类及其所有超类声明的实例变量组成。只要有一个对象引用,虚拟机就必须能够快速地定位对象实例的数据。另外,它也必须能通过该对象引用访问相应的类数据(存储于方法区的类型信息)。因此在对象中通常会有一个指向方法区的指针。
图3-5 划分为对象池和句柄池的对象.png 图3-6 保持对象数据在一起.png有如下几个理由要求虚拟机必须能够通过对象引用得到类(类型)数据:
- 当程序在运行时需要转换某个对象引用为另一种类型时,虚拟机必须要检查这种转换是否被允许。当程序在执行instanceof操作时,虚拟机也进行了同样的检查。
- 当程序调用某个实例方法时,虚拟机必须进行动态绑定。
在Java中,数组是真正的对象。和其它对象一样,数组总是存储在堆中。同样,和普通对象一样,实现的设计者将决定数组在堆中的表示形式。
图3-8 用堆表示数组的一种可能.png程序计数器
对于一个运行中的Java程序而言,其中的每一个线程都有它自己的PC(程序计数器)寄存器,它是在该线程启动时创建的。当线程执行某个Java方法时,PC寄存器的内容总是下一条将被执行指令的地址。如果该线程正在执行一个本地方法,那么此时PC寄存器的值是“undefined”。
Java栈
每当启动一个新线程时,Java虚拟机都会为它分配一个Java栈。Java栈以帧为单位保存线程的运行状态。虚拟机只会直接对Java栈执行两种操作:以帧为单位的压栈或出栈。
Java栈上的所有数据都是此线程私有的。任何线程都不能访问另一个线程的栈数据。当一个线程调用一个方法时,方法的局部变量保存在调用线程Java栈的帧中。只有一个线程能总是访问那些局部变量,即调用方法的线程。
栈帧
栈帧由三部分组成:局部变量区、操作数栈和帧数据区。
局部变量区
class Example3a {
public static int runClassMethod(int i, long l, float f,
double d, Object o, byte b) {
return 0;
}
public int runInstanceMethod(char c, double d, short s,
boolean b) {
return 0;
}
}
图3-9 在Java栈中本地变量区中方法的参数.png
类方法只与类相关,而与具体的对象无关。不能直接通过类方法访问类实例的变量,因为在方法调用的时候没有联系到一个具体实例。
在源代码中的byte、short、char和boolean在局部变量区都被转换成了int,在操作数栈中也一样。前面提到过,虚拟机并不直接支持boolean类型,因此Java编译器总是用int来表示boolean。但Java虚拟机对byte、short和char是直接支持的,这些类型的值可以作为实例变量或者数组元素存储在局部变量区,也可以作为类变量存储在方法区中。但在局部变量区和操作数栈中都会被转换成int类型的值。它们在栈帧中的时候都是当做int来进行处理的,只有当它被存回堆或方法区时,才会转换回原来的类型。
在Java中,所有的对象都按引用传递,并且都存储在堆中,永远都不会在局部变量区或操作数栈中发现对象的拷贝,只会有对象引用。
操作数栈
和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组。但是和前者不同的是,它不是通过索引来访问,而是通过标准的栈操作——压栈和出栈——来访问的。比如,如果某个指令把一个值压入操作数栈中,稍后另一个指令就可以弹出这个值来使用。
帧数据区
除了局部变量区和操作数栈外,Java栈帧还需要一些数据来支持常量池解析、正常方法返回以及异常派发机制。这些信息都保存在Java栈帧的帧数据区中。
本地方法栈
当线程调用本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。可以把这看做是虚拟机利用本地方法来动态扩展自己。
图3-13 一个线程调用Java方法和本地方法时的栈.png执行引擎
任何Java虚拟机实现的核心都是它的执行引擎。在Java虚拟机规范中,执行引擎的行为使用指令集来定义。
和开头提到的对“Java虚拟机”这个术语有三种不同的理解一样,“执行引擎”这个术语也可以有三种理解:一个抽象的规范,一个具体的实现,或是一个正在运行的实例。抽象规范使用指令集规定了执行引擎的行为。具体实现可能使用多种不同的技术——包括软件方面、硬件方面或数种技术的集合。作为运行时实例的执行引擎就是一个线程。
Java虚拟机指令集关注的中心是操作数栈。
网友评论