Java虚拟机工作原理具体解释
一、类载入器
首先来看一下java程序的运行过程。
从这个框图非常easy大体上了解java程序工作原理。首先,你写好java代码,保存到硬盘其中。然后你在命令行中输入
javac YourClassName.java
此时,你的java代码就被编译成字节码(.class).假设你是在Eclipse IDE或者其它开发工具中,你保存代码的时候,开发工具已经帮你完毕了上述的编译工作,因此你能够在相应的文件夹下看到class文件。此时的class文件依旧是保存在硬盘中,因此,当你在命令行中执行
java YourClassName
就完毕了上面红色方框中的工作。JRE的来载入器从硬盘中读取class文件,载入到系统分配给JVM的内存区域--运行数据区(Runtime Data Areas). 然后运行引擎解释或者编译类文件,转化成特定CPU的机器码,CPU运行机器码,至此完毕整个过程。
接下来就重点研究一下类载入器到底为何物?又是怎样工作的?
首先看一下来载入器的一些特点,有点抽象,只是总有帮助的。
》》层级结构
类载入器被组织成一种层级结构关系,也就是父子关系。当中,Bootstrap是全部类载入器的父亲。例如以下图所看到的:
--Bootstrap class loader:
当执行java虚拟机时,这个类载入器被创建,它载入一些主要的java API,包含Object这个类。须要注意的是,这个类载入器不是用java语言写的,而是用C/C++写的。
--Extension class loader:
这个载入器载入出了基本API之外的一些拓展类,包含一些与安全性能相关的类。(眼下了解得不是非常深,仅仅能笼统说,待日后再具体说明)
--System Class Loader:
它载入应用程序中的类,也就是在你的classpath中配置的类。
--User-Defined Class Loader:
这是开发者通过拓展ClassLoader类定义的自己定义载入器,载入程序猿定义的一些类。
》》委派模式(Delegation Mode)
细致看上面的层次结构,当JVM载入一个类的时候,下层的载入器会将将任务托付给上一层类载入器,上一层载入检查它的命名空间中是否已经载入这个类,假设已经载入,直接使用这个类。假设没有载入,继续往上托付直到顶部。检查完了之后,依照相反的顺序进行载入,假设Bootstrap载入器找不到这个类,则往下托付,直到找到类文件。对于某个特定的类载入器来说,一个Java类仅仅能被载入一次,也就是说在Java虚拟机中,类的完整标识是(classLoader,package,className)。一个雷能够被不同的类载入器载入。
举个详细的样例来说明,如今增加我有一个自定义的类MyClass须要载入,假设不指定的话,一般交App(System)载入。接到任务后,System检查自己的库里是否已经有这个类,发现没有之后托付给Extension,Extension进行相同的检查,发现还是没有继续往上托付,最顶层的Boots发现自己库里也没有,于是依据它的路径(Java 核心类库,如java.lang)尝试去载入,没找到这个MaClass类,于是仅仅好(人家看好你,交给你完毕,你无能为力,仅仅好交给别人啦)往下托付给Extension,Extension到自己的路径(JAVA_HOME/jre/lib/ext)是找,还是没找到,继续往下,此时System载入器到classpath路径寻找,找到了,于是载入到Java虚拟机。
如今如果我们将这个类放到JAVA_HOME/jre/lib/ext这个路径中去(相当于交给Extension载入器载入),依照相同的规则,最后由Extension载入器载入MyClass类,看到了吧,统一各类被两次载入到JVM,可是每次都是由不同的ClassLoader完毕。
》》可见性限制
下层的载入器可以看到上层载入器中的类,反之则不行,也就是是说托付仅仅能从下到上。
》》不同意卸载类
类载入器能够载入一个类,可是它不能卸载一个类。可是类载入器能够被删除或者被创建。
当类载入完毕之后,JVM继续依照下图完毕其它工作:
框图中各个步骤简介例如以下:
Loading:文章前面介绍的类载入,将文件系统中的Class文件载入到JVM内存(执行数据区域)
Verifying:检查加载的类文件是否符合Java规范和虚拟机规范。
Preparing:为这个类分配所须要的内存,确定这个类的属性、方法等所需的数据结构。(Prepare a data structure that assigns the memory required by classes and indicates the fields, methods, and interfaces defined in the class.)
Resolving:将该类常量池中的符号引用都改变为直接引用。(不是非常理解)
Initialing:初始化类的局部变量,为静态域赋值,同一时候运行静态初始化块。
那么,Class Loader在载入类的时候,到底做了些什么工作呢?
要了解这当中的细节,必须得先具体介绍一下执行数据区域。
二、执行数据区域
Runtime Data Areas:当执行一个JVM演示样例时,系统将分配给它一块内存区域(这块内存区域的大小能够设置的),这一内存区域由JVM自己来管理。从这一块内存中分出一块用来存储一些执行数据,比如创建的对象,传递给方法的參数,局部变量,返回值等等。分出来的这一块就称为执行数据区域。执行数据区域能够划分为6大块:Java栈、程序计数寄存器(PC寄存器)、本地方法栈(Native Method Stack)、Java堆、方法区域、执行常量池(Runtime Constant Pool)。执行常量池本应该属于方法区,可是因为其重要性,JVM规范将其独立出来说明。当中,前面3各区域(PC寄存器、Java栈、本地方法栈)是每一个线程独自拥有的,后三者则是整个JVM实例中的全部线程共同拥有的。这六大块例如以下图所看到的:
》PC计数器:
每个线程都拥有一个PC计数器,当线程启动(start)时,PC计数器被创建,这个计数器存放当前正在被运行的字节码指令(JVM指令)的地址。
》Java栈:
相同的,Java栈也是每一个线程单独拥有,线程启动时创建。这个栈中存放着一系列的栈帧(Stack Frame),JVM仅仅能进行压入(Push)和弹出(Pop)栈帧这两种操作。每当调用一个方法时,JVM就往栈里压入一个栈帧,方法结束返回时弹出栈帧。假设方法运行时出现异常,能够调用printStackTrace等方法来查看栈的情况。栈的示意图例如以下:
OK。如今我们再来具体看看每个栈帧中都放着什么东西。从示意图非常easy看出,每个栈帧包括三个部分:本地变量数组,操作数栈,方法所属类的常量池引用。
》局部(本地)变量数组:
局部(本地)变量数组中,从0開始按顺序存放方法所属对象的引用、传递给方法的參数、局部变量。举个样例:
public void doSomething(int a, double b, Object o) { ... }
这种方法的栈帧中的局部变量存储的内容各自是:
0: this 1: a 2,3:b 4:0
看细致了,当中double类型的b须要两个连续的索引。取值的时候,取出的是2这个索引中的值。假设是静态方法,则数组第0个不存放this引用,而是直接存储传递的參数。
》操作数栈:
操作数栈中存放方法运行时的一些中间变量,JVM在运行方法时压入或者弹出这些变量。事实上,操作数栈是方法真正工作的地方,运行方法时,局部变量数组与操作数栈依据方法定义进行数据交换。比如,运行下面代码时,操作数栈的情况例如以下:
int a = 90; int b = 10; int c = a + b;
注意在这个图中,操作数栈的地步是在上边,所以先压入的100位于上方。能够看出,操作数栈事实上是一个数据暂时存储区,存放一些中间变量,方法结束了,操作数栈也就没有啦。
》栈帧中数据引用:
除了局部变量数组和操作数栈之外,栈帧还须要一个常量池的引用。当JVM运行到须要常量池的数据时,就是通过这个引用来訪问常量池的。栈帧中的数据还要负责处理方法的返回和异常。假设通过return返回,则将该方法的栈帧从Java栈中弹出。假设方法有返回值,则将返回值压入到调用该方法的方法的操作数栈中。另外,数据区中还保存中该方法可能的异常表的引用。以下的样例用来说明:
class Example3C{ public static void addAndPrint(){ double result = addTwoTypes(1,88.88); System.out.println(result); } public static double addTwoTypes(int i, double d){ return i+d; } }
运行上述代码时,Java栈例如以下图所看到的:
花些时间好好研究上图。一样须要注意的是,栈的底部在上方,先押人员addAndPrint方法的栈帧,再压入addTwoTypes方法的栈帧。上图最右边的文字说明有错误,应该是addTwoTypes的运行结果存放在addAndPrint的操作数栈中。
》》本地方法栈
当程序通过JNI(Java Native Interface)调用本地方法(如C或者C++代码)时,就依据本地方法的语言类型建立对应的栈。
》》方法区域
方法区域是一个JVM实例中的全部线程共享的,当启动一个JVM实例时,方法区域被创建。它用于存执行放常量池、有关域和方法的信息、静态变量、类和方法的字节码。不同的JVM实现方式在实现方法区域的时候会有所差别。Oracle的HotSpot称之为永久区域(Permanent Area)或者永久代(Permanent Generation)。
》》执行常量池
这个区域存放类和接口的常量,除此之外,它还存放方法和域的全部引用。当一个方法或者域被引用的时候,JVM就通过执行常量池中的这些引用来查找方法和域在内存中的的实际地址。
》》堆(Heap)
堆中存放的是程序创建的对象或者实例。这个区域对JVM的性能影响非常大。垃圾回收机制处理的正是这一块内存区域。
所以,类载入器载入事实上就是依据编译后的Class文件,将java字节码载入JVM内存,并完毕对运行数据处于的初始化工作,供运行引擎运行。
三、 运行引擎(Execution Engine)
类载入器将字节码载入内存之后,运行引擎以Java 字节码指令为但愿,读取Java字节码。问题是,如今的java字节码机器是读不懂的,因此还必须想办法将字节码转化成平台相关的机器码。这个过程能够由解释器来运行,也能够有即时编译器(JIT Compiler)来完毕。