文章原文请参阅《Java虚拟机精讲》(高翔龙...)
一、Java平台的结构图
二、JVM与JRE、JDK关系?
JVM:Java Virtual Machine(Java虚拟机),负责执行符合规范的Class文件
JRE:Java Runtime Environment(java运行环境),包含JVM和类库
JDK:Java Development Kit(java开发工具包),包含JRE和开发工具包,例如javac、javah
相关文章:http://blog.csdn.net/ljheee/article/details/50810570
三、JVM所处的位置
(1)通常工作中所接触的基本是Java库和应用以及Java核心类库,知道如何使用就可以了,但是归根结底代码都是要编译成class文件由Java虚拟机装载执行,所产生的结果或者现象都可以通过Java虚拟机的运行机制来解释。一些相同的代码会由于虚拟机的实现不同而产生不同结果。
(2)在Java平台的结构中,可以看出,Java虚拟机(JVM)处在核心的位置,是程序与底层操作系统和硬件无关的关键。它的下方是移植接口,移植接口由两部分组成:适配器和Java操作系统,其中依赖于平台的部分称为适配器;JVM通过移植接口在具体的平台和操作系统上实现;在JVM的上方是Java的基本类库和扩展类库以及它们的API, 利用Java API编写的应用程序(application)和小程序(Java applet)可以在任何Java平台上运行而无需考虑底层平台,就是因为有Java虚拟机(JVM)实现了程序与操作系统的分离,从而实现了Java的平台无关性。
(3)对JVM规范的的抽象说明是一些概念的集合,它们已经在书《The Java Virtual Machine Specification》(《Java虚拟机规范》)中被详细地描述了;对JVM的具体实现要么是软件,要么是软件和硬件的组合,它已经被许多生产厂商所实现,并存在于多种平台之上;运行Java程序的任务由JVM的运行期实例单个承担。
(4)JVM可以由不同的厂商来实现。由于厂商的不同必然导致JVM在实现上的一些不同,像国内就有著名的TaobaoVM;然而JVM还是可以实现跨平台的特性,这就要归功于设计JVM时的体系结构了。
(5)JVM在它的生存周期中有一个明确的任务,那就是装载字节码文件,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行,即Java程序被执行。因此当Java程序启动的时候,就产生JVM的一个实例;当程序运行结束的时候,该实例也跟着消失了。
四、Class字节码
编译后被Java虚拟机所执行的代码使用了一种平台中立(不依赖于特定硬件及操作系统的)的二进制格式来表示,并且经常(但并非绝对)以文件的形式存储,因此这种格式被称为Class文件格式。Class文件格式中精确地定义了类与接口的表示形式,包括在平台相关的目标文件格式中一些细节上的惯例,
正如概念所说,Java为了能够实现平台无关性,制定了一套自己的二进制格式,并经常以文件的方式存储,称为Class文件。这样在不同平台上,只要都安装了Java虚拟机,具备Java运行环境[JRE],那么都可以运行相同的Class文件。
上图描述了Java程序运行的一个全过程,也可以看出Java平台由Java虚拟机和Java应用程序接口搭建,Java语言则是进入这个平台的通道,用Java语言编写并编译的程序可以运行在这个平台上。
由Java源文件编译生成字节码文件,这个过程非常复杂,学过《编译原理》的朋友都知道必须经过词法分析、语法分析、语义分析、中间代码生成、代码优化等;同样的,Java源文件到字节码的生成也想要经历这些步骤。Javac编译器的最后任务就是调用con.sun.tools.javac.jvm.Gen类将这课语法树编译为Java字节码文件。
其实,所谓的编译字节码,无非就是将符合Java语法规范的Java代码转化为符合JVM规范的字节码文件。JVM的架构模型是基于栈的,大部分都需要通过栈来完成。
字节码结构比较特殊,其内部不包含任何的分隔符,无法人工区分段落(字节码文件本身就是给机器读的),所以无论是字节顺序、数量都是有严格规定的,所有16位、32位、64位长度的数据都将构造成2个、4个、8个-----8位字节单位来表示,多字节数据项总是按照Big-endian顺序(高位字节在地址的最低位,地位字节在地址的最高位)来进行存储。
参考《Java虚拟机规范 Java SE7版》的描述,每一个字节码其实都对应着全局唯一的一个类或者接口的定义信息。字节码文件才用的是一种类似于C语言结构体的伪结构来描述字节码文件格式。字节码文件中对应的“基本类型”u1,u2,u4,u8分别表示无符号1、2、4、8个字节。
Class文件----总体格式
值得一提的是,一个有效的class字节码文件的前4个字节为0xCAFEBABE,都是固定的,被称为“魔术”,即magic。它就是JVM用于校验所读取的目标文件是否是一个有效且合法的字节码文件。由此可见,JVM并不是通过判断文件后缀名的方式来校验,以防止人为手动修改。
五、Java虚拟机的体系结构
一个JVM实例的行为不光是它自己的事,还涉及到它的子系统、存储区域、数据类型和指令这些部分,它们描述了JVM的一个抽象的内部体系结构,其目的不光规定实现JVM时它内部的体系结构,更重要的是提供了一种方式,用于严格定义实现时的外部行为。每个JVM都有两种机制,一个是装载具有合适名称的类(类或是接口),叫做类装载子系统;另外的一个负责执行包含在已装载的类或接口中的指令,叫做运行引擎。每个JVM又包括方法区、堆、Java栈、程序计数器和本地方法栈这五个部分,这几个部分和类装载机制与运行引擎机制一起组成的体系结构图为:
Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
可以看出Java虚拟机的运行时数据区包括了:方法区、Java堆、Java虚拟机栈、PC寄存器、本地方法栈,还有常量池。它们被分为两大类-----线程共享、私有数据区。
1.线程共享数据区
包括:Java堆、方法区、常量池。它们会随着虚拟机启动而创建,随着虚拟机退出而销毁。
(1)Java堆
推荐文章:http://blog.csdn.net/ljheee/article/details/52196455
Java堆在虚拟机启动的时候被创建,Java堆主要用来为类实例对象和数组分配内存。Java虚拟机规范并没有规定对象在堆中的形式。
在Java中,堆被划分成两个不同的区域:新生代( Young )、老年代( Old );这也就是JVM采用的“分代收集算法”,简单说,就是针对不同特征的java对象采用不同的策略实施存放和回收,自然所用分配机制和回收算法就不一样。新生代( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。(《Java虚拟机精讲》(高翔龙...))
分代收集算法:采用不同算法处理[存放和回收]Java瞬时对象和长久对象。大部分Java对象都是瞬时对象,朝生夕灭,存活很短暂,通常存放在Young新生代,采用复制算法对新生代进行垃圾回收。老年代对象的生命周期一般都比较长,极端情况下会和JVM生命周期保持一致;通常采用标记-压缩算法对老年代进行垃圾回收。
这样划分的目的是为了使JVM能够更好的管理堆内存中的对象,包括内存的分配以及回收。
Java堆可能发生如下异常情况:如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那Java虚拟机将会抛出一个OutOfMemoryError异常。
(2)方法区
方法区在虚拟机启动的时候被创建,它存储了每一个类的结构信息,例如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容、还包括在类、实例、接口初始化时用到的特殊方法。
方法区可能发生如下异常情况: 如果方法区的内存空间不能满足内存分配请求,那Java虚拟机将抛出一个OutOfMemoryError异常.
(3)常量池
运行时常量池(Runtime Constant Pool)是每一个类或接口的常量池的运行时表示形式,它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。运行时常量池在方法区中。
在创建类和接口的运行时常量池时,可能会发生如下异常情况:当创建类或接口的时候,如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最大值,那Java虚拟机将会抛出一个OutOfMemoryError异常。
2.线程私有数据区
包括:PC寄存器、JVM栈、本地方法区。它们是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
(1)PC寄存器
每个Java虚拟机线程都有自己的PC寄存器。在某个线程被新建时,会获得一个PC寄存器。线程当前执行的方法称为当前方法,PC寄存器用来存放当前方法中当前执行的字节码指令的地址;之所以为每一个线程都分配一个PC寄存器,试想:多线程运行时,某个时间片内只执行一个线程,CPU在不停的切换多个线程,那如何记录具体每一个线程上一次执行到哪个位置了呢,这时候PC寄存器用来存放当前方法中当前执行的字节码指令的地址,就完美解决了,这就是为什么PC寄存器是线程私有数据区的原因。
如果当前方法是本地方法(Native),那么寄存器存放undefined。寄存器的大小至少应该能够存放一个returnAddress类型的数据或者与平台相关的本地指针的值。
PC寄存器是惟一一个没有明确规定需要抛出OutOfMemoryError异常的运行时数据区。
(2)JVM栈
每个Java虚拟机线程都有自己的Java虚拟机栈。Java虚拟机栈用来存放栈帧,而栈帧主要包括了:局部变量表、操作数栈、动态链接。Java虚拟机栈允许被实现为固定大小或者可动态扩展的内存大小。
Java虚拟机使用局部变量表来完成方法调用时的参数传递。局部变量表的长度在编译期已经决定了并存储于类和接口的二进制表示中,一个局部变量可以保存一个类型为boolean、byte、char、short、float、reference和returnAddress的数据,两个局部变量可以保存一个类型为long和double的数据。
Java虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用的时候,操作数栈也用来准备调用方法的参数以及接收方法返回结果。
每个栈帧中都包含一个指向运行时常量区的引用支持当前方法的动态链接。在Class文件中,方法调用和访问成员变量都是通过符号引用来表示的,动态链接的作用就是将符号引用转化为实际方法的直接引用或者访问变量的运行是内存位置的正确偏移量。
总的来说,Java虚拟机栈是用来存放局部变量和过程结果的地方。
Java虚拟机栈可能发生如下异常情况: 如果Java虚拟机栈被实现为固定大小内存,线程请求分配的栈容量超过Java虚拟机栈允许的最大容量时,Java虚拟机将会抛出一个*Error异常。
如果Java虚拟机栈被实现为动态扩展内存大小,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。
(3)本地方法区
本地方法栈用于支持native方法的运行。(native方法,比如用C/C++实现的代码)