我们知道Java是
跨平台(平台无关性)
的,实际上是由于Java虚拟机的存在,Java才能实现一次编译,处处运行
。今天仙鱼来说一下Java虚拟机的架构
在那之前,先了解一下JVM的基本概念:
Question 1:什么是虚拟机?
- 定义:模拟某种计算机体系结构,执行特定指令集的软件。
- 类型:
系统虚拟机
(VMware,Virtual Box),程序虚拟机
(JVM,.NET CLR,P-Code)
Question 2:什么是Java虚拟机?
- Java虚拟机可以严格的换分为两类:
Java语言虚拟机
,真正的Java虚拟机
Java语言虚拟机
:可以执行Java语言的高级语言虚拟机,Java语言虚拟机并不一定就可以成为JVM,譬如:Apache Harmony真正的Java虚拟机
:- 通过Java TCK(Technology Compatibility Kit)的兼容性测试的Java语言虚拟机才是真正的java虚拟机。像我们平时自己实现的简单功能的虚拟机只能算是Java语言虚拟机。
- 误区:有人 认为Java虚拟机只能执行Java程序,其实不然,在混合语言快速发展的时代,Java虚拟机支持其他语言的执行。
- 业界三大商用的JVM:
Oracle HotSpot
,Oracle JRokit VM
,IBM J9 VM
- 其他虚拟机:Google Dalvik VM,Microsoft JVM
- Java虚拟机可以严格的换分为两类:
Java虚拟机架构:
- 重点来了:Java虚拟机的架构轮廓如下图所示,我主要按程序的运行环境来划分:
编译时环境
,运行时环境
,分别对应下图中的绿色虚线包围的区域和褐色虚线包围的区域,从编译时环境到运行时环境是一个Java程序的执行流程:从下图我们可以看出,JVM与java语言没有必然的联系,只与编译后生成的二进制文件:.class文件有关
。 - 提前说明一点,下图中绿色填充区域为
所有线程共享的数据区域
,橙色填充的区域为线程私有的数据区域
。
编译时环境:
- 将Java源文件经Java编译器编译为二进制的.class文件,这个不能算是JVM的一部分,JVM实际上是从.class文件进行类加载开始的。
运行时环境:
在JVM规范中,JVM由四部分组成:
ClassLoader
,RuntimeDateArea
,ExecutionEngine
,NativeInterface
,接下来分别介绍一下者四部分所包含的内容。*.class
:有人可能疑问,为什么不能以.java文件为入口呢?实际上.class这种二进制文件不依赖于特定的硬件和操作系统
,所以.class文件更适用。每一个.class文件中都对应着一个类或者接口的定义信息,但是类或接口并不一定定义在文件中,比如类和接口可以通过类加载器来直接生成。.class(ClassFile)的文件结构如下所示:
java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18ClassFile {
u4 magic; //魔数,固定值为0xCAFEBABE,用来判断当前文件是能被Java虚拟机处理的Class文件
u2 minor_version; //副版本号
u2 major_version; //主版本号
u2 constant_pool_count; //常量池计数器
cp_info constant_pool[constant_pool_count-1]; //常量池
u2 access_flags; //类和接口层次的访问标志
u2 this_class; //类索引
u2 super_class; //父类索引
u2 interfaces_count; //接口计数器
u2 interfaces[interfaces_count]; //接口表
u2 fields_count; //字段计数器
field_info fields[fields_count]; //字段表
u2 methods_count; //方法计数器
method_info methods[methods_count]; //方法表
u2 attributes_count; //属性计数器
attribute_info attributes[attributes_count]; //属性表
}ClassLoader SubSystem(类加载器子系统)
:- 一个.class文件在类加载器中须严格按照以下三个步骤顺序执行:
1 装载(Loading)
:查找.class文件并将.class文件加载到JVM中,包含三种系统类加载器,如下:引导类加载器(Bootstrap Class Loader)
:这个类加载器适用C/C++实现的类加载器,用来加载Java虚拟机运行时做需要的系统类(通常位于{JRE_HOME}/lib下)。JVM的启动就是通过引导类加载器创建一个厨师类来完成的。这个类不能被Java代码访问到,但是我们可以查询到某个类是否被引导类加载器加载过。(我们要自定义类加载器时是通过继承java.lang.ClassLoader类的方式来实现的,但是这里注意一下,引导类加载器并不继承java.lang.ClassLoader)。扩展类加载器(Extension Class Loader)
:这个加载器用于加载Java的拓展类,用来提供除了系统类之外的额外功能,通常放于{JRE_HOME}/lib/ext/目录下。应用程序类加载器(Application Class Loader)
:这个类加载器适用于加载用户代码的加载器,是用户代码的入口。应用类加载器会将拓展类加载器当成是自己的父类加载器,当尝试加载类的时候,首先尝试让拓展类加载器加载,如果加载成功,则直接返回加载结果Classinstance,若加载失败,则会询问引导类加载器是否已经加载了该类,若没有,应用类加载器才会尝试自己加载,这个就是 双亲委托
加载机制。
2 链接(Linking)
:又分为三个步骤,如下:验证(veriy)
:验证被导入的.class文件类型的正确性准备(Prepare)
:为类的静态字段分配字段,并用默认值初始化这些字段解析(Resolve)
:根据运行时常量池的符号引用来动态决定具体值的过程
3 初始化(Initialization)
: 将类变量初始化为正确的初始值
- 一个.class文件在类加载器中须严格按照以下三个步骤顺序执行:
Runtime Data Area(运行时数据区)
:方法区(Method Area)
:是被所有线程共享的运行时内存区域,如:每个线程都可以访问同一个类的静态变量,在方法区种,存储了已经被Java虚拟机加载的类的信息
,静态变量
,编译器编译后的代码
等,一句话,所有类级别的数据都将存储在这里
。如:当程序通过getXXX()等方法获取信息时,这些数据均来源于方法区。每个JVM只有一个共享的方法区域。- 注:
JDK1.7
之前的运行时常量池是在方法区里面的,由于使用反射机制的原因,JVM很困难推测那个类的信息不再使用,因此这块区域的回收很难,幸好JDK1.7之后已经将常量池转移到堆里面了。 - 当方法区无法满足内存分配需求时,会抛出:OutOfMemoryError。
- 注:
Java堆(Heap Area)
:Java堆是JVM所管理的最大的一块内存
,他是是被所有线程共享的一块内存区域,在虚拟机启动时创建。所有的对象及其对应的实例变量和数组都将存储在这里
,每个JVM只有一个对区域,由于方法和堆区域为多个线程共享内存,所以堆区存储的数据并不是线程安全的。运行时常量池(Runtime Constant Pool)
:从JDK1.7开始,常量池从方法区分配内存变成从堆空间中分配内存,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有就是常量表,运行时常量池可以理解为将常量在类加载后进入永久代存放。当创建类或者接口时,如果构造运行时常量池所需的内存超过了堆空间是所能提供的最大值(蜗居的很难~),JVM就会抛出:OutOfMemoryError。Java堆是垃圾回收管理的主战场
,所有的Java堆可以细分为:新生代和老年代
,再细致的分就是把新生代分为:Eden空间,FromSurvivor空间,To Survivor空间
。有时间我再写篇博客介绍下。- Java堆的容量可以时固定的,也可以动态的扩展。Java堆的所使用的内存在物理上不需要连续,逻辑上连续即可。
- 如果在堆中没有足够的内存来完成实例分配,并且堆也无法进行扩展时,就是抛出:OutOfMemoryError。
Java栈(Stack Area)
:每一条Java虚拟机线程都有一个线程私有的Java虚拟机栈
,就像上面图中所示,它的生命中周期与线程相同,与线程时同时创建的,当线程运行完毕后,相应内存也就被自动回收。- Java栈包含很多个栈帧,每个栈帧存储局部变量表,操作数栈(执行引擎计算时需要),动态链接,方法出口等信息,当线程调用一个Java方法时,虚拟机将一个新的栈帧入栈,当该方法执行完成,这个栈帧就从Java栈中弹出。
- 栈内存中可能会出现两种异常:
- 1.当线程请求分配的栈容量超过JVM所允许的最大容量时,会抛出:StackOverflowError。就像一个递归函数反复递归时就会抛出这个异常。
- 2.若JVM允许栈动态扩展内存,当扩展时无法申请到足够的内存时会抛出:OutOfMemoryError。
PC计数器(PC Register)
:又叫PC寄存器,是一块较小的内存空间。每一个Java线程都有一个PC寄存器
,用来记录在线程中切换回来后恢复到正确的执行位置,因此,程序寄存器是线程私有的。- 若该线程正在执行一个Java方法,则计数器记录的是正在执行的JVM字节码地址,若执行native方法,则计数器为空。
- 程序计数器是Java虚拟机规范中唯一没有规定任何OutOfMemoryError情况的数据区域。
本地方法栈(Native Method Stack)
:JVM实现可能要用到C Stacks来支持Native语言,这个C Stacks就是本地方法栈。- 本地方法栈和JVM栈的作用很相似,他们的区别在于
JVM栈是为执行Java代码方法服务,而本地方法栈是为Native方法服务
。 - 和JVM一样,这个区域也会抛出:OutOfMemoryError和StackOverflowError。
- 本地方法栈和JVM栈的作用很相似,他们的区别在于
Execution Engine(执行引擎)
:- 分配给运行时数据区域的字节码将由执行引擎来执行,执行引擎读取字节码并逐个执行它。主要有以下的组件。
解释器(Interpreter)
:解释器解释字节码很快,但执行很慢。而且当一个发方法被多次调用时,每次都需要新的解释。JIT编译器(Just-In-Time Compiler)
:JIT编译器消除了解释器的缺点,执行引擎将使用解释器(Interpreter)的帮助来转换字节码代码,但是当它发现重复的代码时,它使用JIT编译器,它编译整个字节码并将其改为本机代码,这种本机代码将直接用于重复的方法调用,从而提高系统的性能。其中包含下面的几部分:Intermediate Code Generator
:生成中间代码。Code Optimizer
:负责优化上面生成的中间代码。Target Code Generator
:负责生成机器代码或者本机代码。Profiler
:一个特殊的组件,负责查找hostports,即该方法是否被多次调用。
Garbage Collection(垃圾回收)
:简称GC,收集和删除为引用的对象,可以通过调用System.gc()来处罚垃圾收集,但不会立刻执行(执行时机由GC决定)。JVM的垃圾回收收集已创建的对象。
- 分配给运行时数据区域的字节码将由执行引擎来执行,执行引擎读取字节码并逐个执行它。主要有以下的组件。
Java Native Interface(本地接口)
:- Java Native Interface与Native Method Libraries交互,为Execution Engine提供所需要的本地库接口。
Native Method Libraries(本地库集合)
:- Native Method Libraries是执行引擎uoxu的本地库的集合。