Java虚拟机是什么
要理解java虚拟机,你首先必须意识到,当你说“Java虚拟机”时,可能指的是如下三个不同的东西:
1. 抽象规范
2.一个具体的实现
3.一个运行中的虚拟机实例
Java虚拟机抽象规范仅仅是一个概念,在Tim Lindholm和Frank Yellin编著的《The Java Virtual Machine Specification》一书中详细地描述了它。而该规范的具体实现,可能来自多个提供商,并存在于多个平台。他或者完全用软件实现,或者以硬件和软件结合的方式来实现。当运行一个Java程序的同时,也就是运行了一个Java虚拟机实例。每个Java程序都运行于某个具体的Java虚拟机实现的实例上。
Java虚拟机的生命周期
一个运行时的Java虚拟机实例的天职就是:负责运行一个Java程序。当启动一个Java程序时,一个虚拟机实例也就诞生了。当该程序关闭退出是,这个虚拟机实例也就随之消亡。如果在同一个计算机上同时运行三个程序,将得到三个Java虚拟机实例。每个Java程序都运行于它自己的Java虚拟机实例中。
Java虚拟机实例通过调用某个初始类的main()方法来运行一个Java程序,此方法将作为该程序初始线程的启动,任何其他的线程都是由这个初始线程启动的。
在Java虚拟机内部有两种线程:守护线程与非守护线程。守护线程通常是由虚拟机自己使用的,比如执行垃圾收集任务的线程。但是,Java程序也可以把他创建的任何线程标记为守护线程。而Java程序中的初始线程---------就是开始于main()的那个,是非守护进程。只要还有非守护线程在运行,那么这个Java程序也在继续运行(虚拟机仍然存活)。当该程序中所有的非守护线程都终止时,虚拟机实例将自动退出。假若安全管理器允许,程序本身也能够通过调用Runtime类或者System类的exit()方法来退出。
Java虚拟机体系结构
从上图可以看得出来,JVM中包含:
1. Class Loader 类加载器
所有的class文件必须被加载后才能在jvm中运行,关于jvm的类加载器,在其他章节中重点描述。
2.Runtime Data Areas 运行数据区
运行时数据区分为:method area(本地方法区)、heap(堆)、java stacks(Java 栈)、 pc registers(pc寄存器)、 native method stacks(本地方法栈)
下面来依次解释下上述内容:
Method area(方法区)
在Java虚拟机中,关于被装载类型的信息存储在一个逻辑上被称为方法区的内存中。当虚拟机状态某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件------一个线性二进制数据流。然后将它传输到虚拟机中。紧接着虚拟机提取其中的类型信息,并将这些信息存储到方法区。该类型中的类(静态)变量同样也是存储在方法区中。
Java虚拟机在内部如何存储类型信息,这是由具体实现的设计者来决定的。比如,在class文件中,多字节总是以高位在前(即代表较大数的字节在前)的顺序储存。但是这些数据被引入到方法区后,虚拟机可以以任何方式存储它。假设某个实现是运行在低位优先的处理器上,那么它很可能会把多字节以低位优先的顺序存储到方法区中。
当虚拟机运行Java程序时,它会查找使用存储在方法区中的类型信息。设计者应当为类型信息的内部设计适当的数据结构,以尽可能在保持虚拟机小巧紧凑的同事加快程序的运行效率。如果正在设计一个需要在少量内存的限制中操作的实现,设计则可能会决定以牺牲某些运行速度来换取紧凑性。另外一方面,如果设计一个将在虚拟内存系统中运行的实现,设计者可能会决定在方法去中保存一些冗余的信息,以此来加快执行 速度。(如果底层主机没有提供虚拟内存,但是提供了一个硬盘,设计者可能会在实现中穿件一个虚拟内存系统。)Java虚拟机的设计者可以根据目标平台的资源限制和需求,在空间和时间上作出权衡,选择实现什么样的数据结构和数据组织。
由于所有线程都共享方法区,因此它们对方法区数据的访问必须被设计为是线程安全的。比如,假设同时有连个线程都企图访问一个名为Lava的类,而这个类还没有被装入虚拟机,那么这是只应该有一个线程去装载它,而另外一个线程则只能等待。
方法区的大小不必是固定的,虚拟机可以根据应用的需要动态调整。同样,方法区也不必是连续的,方法区可以在一个堆(甚至是虚拟机自己的堆)中自由分配。另外,虚拟机也可以允许用户或者程序员指定方法区的初始化大小以及最小和最大尺寸等。
方法区也可以被垃圾收集,因为虚拟机允许通过用户定义的类装载器来动态扩展java程序,因此一些类也会成为程序"不再引用"的类。当某个类变为不再被引用的类时,Java虚拟机可以卸载这个类(垃圾收集),从而使方法区占据的内存保持最小。
方法区针对具体的语言特性有几种信息是存储在方法区内的:
【类型信息】 --对每个装载的类型,虚拟机都会在方法区中存储以下类型信息
这个类型的完全限定名(java.lang.String格式)
这个类型的直接超类的全限定名(除非uzhege类型时java.lang.Object,它没有超类)
这个类型是类类型还是接口类型
这个类型的访问修饰符(public、abstract或final的某个子集)
任何直接超接口的全限定名的有序列表
在Java class文件和虚拟机中,类型名总是以全限定名出现。在java源代码中,全限定名由类所属包的名称加上一个“.”,再加上类名组成。例如,类Object 的所属包为java.lang,那她的全限定名应该是 java.lang.Object,但是在class文件里,所有的“.”都被斜杠“/”替换,这样就成为java/lang/Object。至于全限定名在方法区中的表示,则因不同的设计者有不同的选择而不同,可以用任何形式和数据结构来表示。
除了上面列出的基本类型信息外,虚拟机还得为每个被装载的类型存储以下信息:
【该类型的常量池】
【字段信息】
【方法信息】
【除了常量意外的所有类(静态)变量】
【一个到ClassLoader的引用】
【一个到Class类的引用】
【常量池】
虚拟机必须为每一个被装载的类型维护一个常量池。常量池就是该类型所用常量的一个有序集合,包括直接常量(string,integer和floating point常量)和对其他类型、字段和方法的符号引用。池中的数据项就像数组一样是通过索引访问的。因为常量池存储了相应类型所用到的所有类型、字段和方法的符号引用,所以它在java程序的动态连接中起着核心的作用。
【字段信息】
对于类型中声明的没一个字段,方法区中必须保存下面的信息。除此之外,这些字段在类或者接口中的声明顺序也必须保存。下面是字段信息的清单:
字段名
字段的类型
字段的修饰符(public、private、protected、static、final、volatile、transient的某个子集)
【方法信息】
对于类型中声明的每一个方法,方法区中必须保存下面的信息。和字段一样,这些方法法在类或者接口中的声明顺序也必须保存。下面试方法信息的清单:
方法名
方法的返回类型(或void)
方法参数的数量和类型(按按声明顺序)
方法的修饰符(public、private、protected、static、final、synchronized、native、abstract的某个子集)
除了上面的清单中列出的条目之外,如果某个方法不是抽象的和本地的,它还必须保存下面的信息:
方法的字节码(bytecodes)
操作数栈和该方法的栈帧中的局部变量区的大小
异常表
【类(静态)变量】
类变量是由所有类实例共享的,即使没有任何类实例,它也可以被访问。这些变量只与类有关---而非类的实例,因此他们总是作为类型信息的一部分二存储在方法区。除了在类中声明的编译时常量外,虚拟机在使用某个类之前,必须在方法区中为这些类变量分配空间。
而编译时常量(就是那些用final声明以及用编译时已知的值初始化的类变量)则和一般的类变量的处理方式不同,每个使用编译时常量的类型都会复制它的所有常量到自己的常量池中,或嵌入到它的字节码流中。作为 常量池或字节码流的一部分,编译时常量保存在方法区中,就和一般的类变量一样。但是当一般的类变量做为声明他们的类型的一部分数据面保存的时候,编译时常量作为使用他们的类型的一部分而保存。
【指向ClassLoader类的应用】
每个类型被加载的时候,虚拟机必须跟踪它是由启动类装载器还是由用户自定义类装载器装载的。如果是用户自定义类装载器装载的,那么虚拟机必须在类型信息中存储对该装载器的引用。这是作为方法表中的类型数据的一部分保存的。虚拟机会在动态连接期间使用这个信息。当某个类型引用另外一个类型的时候,虚拟机会请求装载发起引用类型的类装载器类装载来装载被引用的类型。这个动态连接的过程,对于虚拟机分离命名空间的范式也是至关重要的。为了能够正确地执行动态连接以及维护多个命名空间,虚拟机需要在方法表中得知每个类都是由哪个类装载器装载的,
【指向Class类的引用】
对于每个被装载的类型(不管是类还是接口),虚拟机都会相应地位它创建一个java.lang.Class类的实例,而且虚拟机还必须以某种方式吧这个实例和存储在方法区中的数据关联起来。
【方法表】
为了尽可能提高访问效率,设计者必须仔细设计存储在方法区中的类型信息的数据结构,因此,除了以上讨论的原始类型信息,实现中还可能包括其他数据结构加快访问原始数据的速度,比如方法表。虚拟机对每个装载的非抽象类,都生成一个方法表,把它作为类信息的一部分保存在方法区。方法表是一个数组,他的元素是所有他的实例可能被调用的实例方法的直接引用,包括哪些从超类继承过来的实例方法。(对于抽象类和接口,方法表没有什么帮助,因为程序绝不会生成他们的实例)运行时可以通过方法表快速搜索在对象中调用的实例方法。
方法区使用示例
为了展示虚拟机如何使用方法区中的信息,我们举个例子,看下面这个类
Java代码
class Lava{
private int speed = 5;
void flow(){
}
}
class Volcano{
public void main(String[] args) {
Lava lava =new Lava();
lava.flow();
}
}
下面的段落描述了某个实现是如何执行Volcano程序中main()方法的字节码中第一条指令的。不同的虚拟机实现可能会用完全不同的方法来操作,下面描述的只是其中一种可能,但是并不是仅有的一种,下面看一下Java虚拟机是如何执行Volcano程序中main()方法的第一条指令的。
要运行Volcano程序,首先得以某种“依赖于实现的”方式告诉虚拟机“Volcano”这个名字。之后虚拟机将找到并读入相应的class文件“Volcano.class”,然后他会从导入的class文件里的而精致数据中提取类型信息并放到方法区中。通过执行保存在方法区中额字节码,虚拟机开始执行main()方法,在执行时,他会一直持有指向当前类(Volcano类)的常量池(方法区中的一个数据结构)的指针。
注意,虚拟机开始执行Volcano类中main()方法的字节码的时候,尽管Lava类还没被装载,但是和大多数(也许是所有) 虚拟机实现一样,他不会等到把程序中用到的所有类都装载后才开始运行程序。恰好相反,他只需在需要是才装载相应的类。
main()的第一条指令告知虚拟机为列在常量池第一项的类分配足够的内存。所以虚拟机使用指向Volcano常量池的指针找到第一项,发现他是一个堆Lava类的符号引用,然后他就检查方法区,看Lava类是否已经被装载了。
这个符号引用仅仅是一个给出了类Lava的全限定名“Lava”的字符串。为了能让虚拟机尽可能快地从一个名称找到类,设计者应当选择最佳的数据结构和算法。这里可以采用各种方法,如散列表、搜索树等等。同样的算法可以以用于实现Class类的forName()方法,这个方法根据给定的全限定名返回Class引用。
当虚拟机发现还没有装载过名为“Lava”的类时,他就开始查找并装载文件“Lava.class”,并把从读入的二进制数据中提取的类型信息放在方法区中。
紧接着,虚拟机以一个直接指向方法区Lava类数据的指针类替换常量池第一项(就是那个字符串“Lava”)----以后就可以用这个指针来快速访问Lava类了。这个替换过程称为常量池解析,即把常量池中的符号引用替换为直接引用。这是通过在方法区中搜索被引用的元素实现的,在这期间可能又需要装载其他类。在这里,我们替换掉符号引用的“直接引用”是一个本地指针。
终于,虚拟机转变为一个新的Lava对象分配内存。此时,它又需要方法区中的信息。还记得刚刚放到Volcano类常量池第一项的指针吗?现在虚拟机用它来访问Lava类型信息(此前刚放到方法区中的),找到其中记录的这样一个信息:一个Lava对象需要分配多少堆空间。
Java虚拟机总能够通过存储于方法区的类型信息来实现一个对象需要的内存,但是,某一个特定对象事实上需要多少内存,是跟特定实现相关的。对象在虚拟机内部的表示由实现的设计者来决定的。
当java虚拟机确定了一个Lava对象的大小后,它就在堆上分配这么大的空间,并把这个对象实例的变量speed初始化为默认初始值0.假如Lava类的超类Object也有实例变量,这也会在此时被初始化为相应的默认值。
当把新生成的Lava对象的引用压到栈中,main()方法的第一条指令也完成了。接下来的指令通过这个引用调用Java代码(改代码把speed变量初始化为争取的初始值5)。另外一条指令将用这个引用调用Lava对 象引用的flow()方法。