`

Java程序员从笨鸟到菜鸟之(九十二)深入java虚拟机(一)——java虚拟机底层结构详解

阅读更多

本文来自:曹胜欢博客专栏。转载请注明出处:http://blog.csdn.net/csh624366188

在以前的博客里面,我们介绍了在java 领域中大部分的知识点,从最基础的 java 最基本语法到 SSH 框架。这里面应该包含了在 java 领域里面的大部分内容了吧。但是,那些知识点是让我们从一个应用的层面上了解了 java java 程序真正底层的运行机制和一些底层虚拟机的工作我们还不了解,虽然这些内容在我们真正的开发中几乎用不到这些底层的东西,但对于我们对 java 的理解会有比较大的帮助。尤其也对以后 java 开发中的性能优化有很大帮助,可以使我们减少一些没必要的内存浪费等好处。所以,从今天开始,我将和大家一起来学习一下 java 虚拟机的内容。从底层开一下 java 的运行机制。

Java虚拟机

Java虚拟机JavaVirtualMachine) 简称 JVMJava 虚拟机是一个想象中的机器,在实际的计算机上 通过软件模拟来实现。Java 虚拟机有自己想象中的硬件,如处理器 、堆栈、寄存器等,还具有相应的指令系统。下面我们就来看一下这几部分比较重要的java虚拟机的结构

JVM寄存器

所有的CPU 均包含用于保存系统状态和处理器所需信息 的寄存器组。如果虚拟机定义义较多的寄存器,便可以从中得到更多的信息而不必对栈或内存进行访问,这有利于提高运行速度。然而,如果虚拟机中的寄存器比实际CPU 的寄存器多,在实现虚拟机时就会占用处理器大量的时间来用 常规存储器模拟寄存器,这反而会降低虚拟机的效率。针对这种情况,JVM 只设置了 4 个最为常用的寄存器。它们是: p c程序计数器, 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 解释器和 HotJava 环境中,碎片回收用 后台线程的方式来执行。这不但为运行系统提供了良好的性能,而且使程序设计人员摆脱了自己控制内存使用的风险。

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程序运行的一个全过程

 

3Java虚拟机的运行过程

上面对虚拟机的各个部分进行了比较详细的说明,下面通过一个具体的例子来分析它的运行过程。

虚拟机通过调用某个指定类的方法main 启动,传递给 main 一个字符串数组参数,使指定的类被装载,同时链接该类所使用的其它的类型,并且初始化它们。例如对于程序:

classHelloApp

{

publicstaticvoidmain(String[]args)

{

System.out.println("HelloWorld!");

for(inti=0;i<args.length;i++)

{

System.out.println(args[i]);

}

}

}

编译后在命令行模式下键入:javaHelloApprunvirtualmachine

将通过调用HelloApp 的方法 main 来启动 java 虚拟机,传递给 main 一个包含三个字符串 "run" "virtual" "machine" 的数组。现在我们略述虚拟机在执行 HelloApp 时可能采取的步骤。

开始试图执行类HelloApp main 方法,发现该类并没有被装载,也就是说虚拟机当前不包含该类的二进制代表,于是虚拟机使用 ClassLoader 试图寻找这样的二进制代表。如果这个进程失败,则抛出一个异常。类被装载后同时在 main 方法被调用之前,必须对类 HelloApp 与其它类型进行链接然后初始化。链接包含三个阶段:检验,准备和解析。检验检查被装载的主类的符号和语义,准备则创建类或接口的静态域以及把这些域初始化为标准的默认值,解析负责检查主类对其它类或接口的符号引用,在这一步它是可选的。类的初始化是对类中声明的静态初始化函数和静态域的初始化构造方法的执行。一个类在初始化之前它的父类必须被初始化。整个过程如下:

 


推荐阅读(内含jvm内存区域说明):

Java程序员从笨鸟到菜鸟之(九十三)深入java虚拟机(二)——类加载器详解(上)

 


参考资料:http://www.kuqin.com/java/20080525/8907.html

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics