本文来自:曹胜欢博客专栏。转载请注明出处:http://blog.csdn.net/csh624366188
在曾经的博客里面,我们介绍了在java领域中大部分的知识点,从最基础的java最基本的语法到SSH框架。这里面应该包括了在java领域里面的大部分内容了吧。可是,那些知识点是让我们从一个应用的层面上了解了java,java程序真正底层的执行机制和一些底层虚拟机的工作我们还不了解,尽管这些内容在我们真正的开发中差点儿用不到这些底层的东西,但对于我们对java的理解会有比較大的帮助。尤其也对以后java开发中的性能优化有非常大帮助,能够使我们降低一些不是必需的内存浪费等优点。所以,从今天開始,我将和大家一起来学习一下java虚拟机的内容。从底层开一下java的执行机制。
Java虚拟机
Java虚拟机(Java Virtual Machine) 简称JVM Java虚拟机是一个想象中的机器,在实际的计算机上通过软件模拟来实现。Java虚拟机有自己想象中的硬件,如处理器、堆栈、寄存器等,还具有对应的指令系统。以下我们就来看一下这几部分比較重要的java虚拟机的结构
JVM寄存器
全部的CPU均包括用于保存系统状态和处理器所需信息的寄存器组。假设虚拟机定义义较多的寄存器,便能够从中得到很多其它的信息而不必对栈或内存进行訪问,这有利于提高运行速度。然而,假设虚拟机中的寄存器比实际CPU的寄存器多,在实现虚拟机时就会占用处理器大量的时间来用常规存储器模拟寄存器,这反而会减少虚拟机的效率。针对这样的情况,JVM仅仅设置了4个最为经常使用的寄存器。它们是:pc程序计数器,optop操作数栈顶指针 ,frame当前运行环境指针, vars指向当前运行环境中第一个局部变量的指针, 全部寄存器均为32位。pc用于记录程序的运行。optop,frame和vars用于记录指向Java栈区的指针。
JVM栈结构
作为基于栈结构的计算机,Java栈是JVM存储信息的主要方法。当JVM得到一个java字节码应用程序后,便为该代码中一个类的每个方法创建一个栈框架,以保存该方法的状态信息。每个栈框架包含下面三类信息:局部变量运行环境操作数栈 局部变量用于存储一个类的方法中所用到的局部变量。vars寄存器指向该变量表中的第一个局部变量。运行环境用于保存解释器对Java字节码进行解释过程中所需的信息。它们是:上次调用的方法、局部变量指针和操作数栈的栈顶和栈底指针。运行环境是一个运行一个方法的控制中心。比如:假设解释器要运行iadd(整数加法),首先要从frame寄存器中找到当前运行环境,而后便从运行环境中找到操作数栈,从栈顶弹出两个整数进行加法运算,最后将结果压入栈顶。 操作数栈用于存储运算所需操作数及运算的结果。
JVM碎片回收堆
Java类的实例所需的存储空间是在堆上分配的。解释器详细承担为类实例分配空间的工作。解释器在为一个实例分配完存储空间后,便開始记录对该实例所占用的内存区域的使用。一旦对象使用完成,便将其回收到堆中。在Java语言中,除了new语句外没有其它方法为一对象申请和释放内存。对内存进行释放和回收的工作是由Java执行系统承担的。这同意Java执行系统的设计者自己决定碎片回收的方法。在SUN公司开发的Java解释器和Hot Java环境中,碎片回收用后台线程的方式来执行。这不但为执行系统提供了良好的性能,并且使程序设计人员摆脱了自己控制内存使用的风险。
JVM存储区
JVM有两类存储区:常量缓冲池和方法区。常量缓冲池用于存储类名称、方法和字段名称以及串常量。方法区则用于存储Java方法的字节码。对于这两种存储区域详细实现方式在JVM规格中没有明白规定。这使得Java应用程序的存储布局必须在执行过程中确定,依赖于详细平台的实现方式。JVM是为Java字节码定义的一种独立于详细平台的规格描写叙述,是Java平台独立性的基础。眼下的JVM还存在一些限制和不足,有待于进一步的完好,但不管怎样,JVM的思想是成功的。对照分析:假设把Java原程序想象成我们的C++原程序,Java原程序编译后生成的字节码就相当于C++原程序编译后的80x86的机器码(二进制程序文件),JVM虚拟机相当于80x86计算机系统,Java解释器相当于80x86CPU。在80x86CPU上执行的是机器码,在Java解释器上执行的是Java字节码。 Java解释器相当于执行Java字节码的“CPU”,但该“CPU”不是通过硬件实现的,而是用软件实现的。Java解释器实际上就是特定的平台下的一个应用程序。仅仅要实现了特定平台下的解释器程序,Java字节码就能通过解释器程序在该平台下执行,这是Java跨平台的根本。当前,并非在全部的平台下都有对应Java解释器程序,这也是Java并不能在全部的平台下都能执行的原因,它仅仅能在已实现了Java解释器程序的平台下执行。
Java虚拟机的体系结构图
Java虚拟机从启动到结束的生命周期,当java虚拟机启动后,在例如以下几种情况下,Java虚拟机将结束生命周期:
1.运行了System.exit()方法
2.程序正常运行结束
3.程序在运行过程中遇到了异常或错误而异常终止
4.因为操作系统出现错误而导致Java虚拟机进程终止
Java虚拟机的栈有三个区域:局部变量区、执行环境区、操作数区。
局部变量区
每一个Java方法使用一个固定大小的局部变量集。它们依照与vars寄存器的字偏移量来寻址。局部变量都是32位的。长整数和双精度浮点数占领了两个局部变量的空间,却依照第一个局部变量的索引来寻址。(比如,一个具有索引n的局部变量,假设是一个双精度浮点数,那么它实际占领了索引n和n+1所代表的存储空间)虚拟机规范并不要求在局部变量中的64位的值是64位对齐的。虚拟机提供了把局部变量中的值装载到操作数栈的指令,也提供了把操作数栈中的值写入局部变量的指令。
执行环境区
在执行环境中包括的信息用于动态链接,正常的方法返回以及异常捕捉。
操作数栈区
机器指令仅仅从操作数栈中取操作数,对它们进行操作,并把结果返回到栈中。选择栈结构的原因是:在仅仅有少量寄存器或非通用寄存器的机器(如Intel486)上,也可以高效地模拟虚拟机的行为。操作数栈是32位的。它用于给方法传递參数,并从方法接收结果,也用于支持操作的參数,并保存操作的结果。比如,iadd指令将两个整数相加。相加的两个整数应该是操作数栈顶的两个字。这两个字是由先前的指令压进堆栈的。这两个整数将从堆栈弹出、相加,并把结果压回到操作数栈中。
每一个原始数据类型都有专门的指令对它们进行必须的操作。每一个操作数在栈中须要一个存储位置,除了long和double型,它们须要两个位置。操作数仅仅能被适用于其类型的操作符所操作。比如,压入两个int类型的数,假设把它们当作是一个long类型的数则是非法的。在Sun的虚拟机实现中,这个限制由字节码验证器强制实行。可是,有少数操作(操作符dupe和swap),用于对执行时数据区进行操作时是不考虑类型的。
本地方法栈,当一个线程调用本地方法时,它就不再受到虚拟机关于结构和安全限制方面的约束,它既能够訪问虚拟机的执行期数据区,也能够使用本地处理器以及不论什么类型的栈。比如,本地栈是一个C语言的栈,那么当C程序调用C函数时,函数的參数以某种顺序被压入栈,结果则返回给调用函数。在实现Java虚拟机时,本地方法接口使用的是C语言的模型栈,那么它的本地方法栈的调度与使用则全然与C语言的栈同样。
下图能够表示出来java程序执行的一个全过程
3 Java虚拟机的执行过程
上面对虚拟机的各个部分进行了比較具体的说明,以下通过一个具体的样例来分析它的执行过程。
虚拟机通过调用某个指定类的方法main启动,传递给main一个字符串数组參数,使指定的类被装载,同一时候链接该类所使用的其他的类型,而且初始化它们。比如对于程序:
class HelloApp
{
public static void main(String[] args)
{
System.out.println("Hello World!");
for (int i = 0; i < args.length; i++ )
{
System.out.println(args[i]);
}
}
}
编译后在命令行模式下键入: java HelloApp run virtual machine
将通过调用HelloApp的方法main来启动java虚拟机,传递给main一个包括三个字符串"run"、"virtual"、"machine"的数组。如今我们略述虚拟机在运行HelloApp时可能採取的步骤。
開始试图运行类HelloApp的main方法,发现该类并没有被装载,也就是说虚拟机当前不包括该类的二进制代表,于是虚拟机使用ClassLoader试图寻找这种二进制代表。假设这个进程失败,则抛出一个异常。类被装载后同一时候在main方法被调用之前,必须对类HelloApp与其他类型进行链接然后初始化。链接包括三个阶段:检验,准备和解析。检验检查被装载的主类的符号和语义,准备则创建类或接口的静态域以及把这些域初始化为标准的默认值,解析负责检查主类对其他类或接口的符号引用,在这一步它是可选的。类的初始化是对类中声明的静态初始化函数和静态域的初始化构造方法的运行。一个类在初始化之前它的父类必须被初始化。整个步骤例如以下:
推荐阅读(内含jvm内存区域说明):
Java程序猿从笨鸟到菜鸟之(九十三)深入java虚拟机(二)——类载入器具体解释(上)
參考资料:http://www.kuqin.com/java/20080525/8907.html