Dive into Java -- (1) JVM introduction

June 23, 2016 / Jason Wang

JVM 简介

JVM 简介

JVM 是 Java Virtual Machine(Java虚拟机)的缩写,JVM 是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

Java 语言的一个非常重要的特点就是与平台的无关性。而使用 Java 虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后, Java 语言在不同平台上运行时不需要重新编译。 Java 语言使用 Java 虚拟机屏蔽了与具体平台相关的信息,使得 Java 语言编译程序只需生成在 Java 虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。 Java 虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是 Java 程序能够“一次编译,到处运行”的原因。

JVM 规格

JVM 的设计目标是提供一个基于抽象规格描述的计算机模型,为解释程序开发人员提供很好的灵活性,同时也确保 Java 代码可在符合该规范的任何系统上运行。JVM 对其实现的某些方面给出了具体的定义,特别是对 Java 可执行代码,即字节码(Bytecode)的格式给出了明确的规格。这一规格包括操作码和操作数的语法和数值、标识符的数值表示方式、以及 Java 类文件中的 Java 对象、常量缓冲池在 JVM 的存储映象。这些定义为 JVM 解释器开发人员提供了所需的信息和开发环境。Java 的设计者希望给开发人员以随心所欲使用 Java 的自由。

JVM 定义了控制 Java 代码解释执行和具体实现的五种规格,它们是:

JVM 指令系统
JVM 寄存器
JVM 栈结构
JVM 碎片回收堆
JVM 存储区

因此,只要是符合 JVM 规格的实现,都可以执行 Java 程序。大多情况下,我们使用的是 Sun(2009 年 4 月被 Oracle 收购)JDK 提供的 JVM。但除此之外,还有其它的 JVM 实现,如 Open JDK,BEA 的 JRockit。

JVM 结构

JVM 结构图

JVM 的主要结构如下:

可以看出,JVM 主要由三部分组成:

  1. Class Loader(类装载器:用来装载.class文件)
  2. Execution Engine (执行引擎:执行字节码或者执行本地方法)
  3. Runtime Data Area (运行时数据区:方法区、堆、Java 栈、PC寄存器、本地方法栈)

还可以看出,在运行时数据区中,方法区是所有 Java 线程共享的,而Java栈、本地方法栈、PC寄存器则是线程私有的。

Runtime Data Area部分将另外单独说明。

JVM ClassLoader

Class Loader 分类

a) Bootstrap Class Loader(启动类加载器)

JVM 的根 ClassLoader,JVM 启动时初始化此 ClassLoader,Java的核心类都是由该ClassLoader加载。

在 Sun JDK 的实现中,由此 ClassLoader 完成 $JAVA_HOME 中 jre/lib/rt.jar 中所有 class 文件的加载,这个jar中包含了java规范定义的所有接口以及实现。这个类加载器是由C++实现的,并且在Java语言中无法获得它的引用。

b) Extension Class Loader(扩展类加载器)

负责加载一些扩展功能的jar包。

c) System Class Loader(系统类加载器)

负责加载启动参数中指定的 Classpath 中的 jar 包及目录,通常我们自己写的Java类也是由该ClassLoader 加载。在Sun JDK中,系统类加载器的名字叫 AppClassLoader。

d) User Defined Class Loader(用户自定义类加载器)

User Defined Class Loader 是开发者继承 ClassLoader 抽象类自行实现的ClassLoader,可用于加载非 Classpath 中的 jar 以及目录。由用户自定义类的加载规则,可以手动控制加载过程中的步骤。

ClassLoader 抽象类的关键方法

1) loadClass

负责加载指定名字的类,它接受一个全类名,然后返回一个Class类型的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {  
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}

loadClass的过程为:为先从已经加载的类中寻找,如没有则继续从 parent ClassLoader 中寻找,如仍然没找到,则从 System ClassLoader 中寻找,最后再调用 findClass 方法来寻找,如要改变类的加载顺序,则可覆盖此方法

2) findLoadedClass

负责从当前 ClassLoader 实例对象的缓存中寻找已加载的类,调用的为 native 的方法。

3) findClass

直接抛出 ClassNotFoundException,因此需要通过覆盖 loadClass 或此方法来以自定义的方式加载相应的类。

1
2
3
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

4) findSystemClass

负责从 System ClassLoader 中寻找类,如未找到,则继续从 Bootstrap ClassLoader 中寻找,如仍然为找到,则返回 null 。

5) defineClass

负责将二进制的字节码转换为 Class 对象

6) resolveClass

负责完成 Class 对象的链接,如已链接过,则会直接返回。

类加载过程

完整的JVM 类加载过程的步骤如下:

1) 装载

装载过程负责找到编译好的 .class 字节码并加载至 JVM 中。

JVM 通过 ClassLoader 根据类名和类所在的包名,来完成类的加载。已加载的类使用如下标识:

类名 + 包名 + ClassLoader 实例ID

JVM 的类加载子系统支持运行时动态加载,动态加载的优点有很多,例如可以节省内存空间、灵活地从网络上加载类,动态加载的另一好处是可以通过命名空间的分隔来实现类的隔离,增强了整个系统的安全性。

2) 链接

链接过程负责:校验二进制字节码的格式,初始化装载类中的静态变量以及解析类中调用的接口、类。

完成校验后,JVM 初始化类中的静态变量,并将其值赋为默认值。

最后对类中的所有属性、方法进行验证,以确保其需要调用的属性、方法存在,以及具备应的权限(例如 public、private 域权限等),可能会抛出 NoSuchMethodError、NoSuchFieldError 等错误信息。

3) 初始化

初始化过程即为执行类中的静态初始化代码、构造器代码以及静态属性的初始化,JVM规范严格定义了何时需要对类进行初始化:

a、通过 new 关键字、反射、clone、反序列化机制实例化对象时。

b、调用类的静态方法时。

c、使用类的静态字段或对其赋值时。

d、通过反射调用类的方法时。

e、初始化该类的子类时(初始化子类前其父类必须已经被初始化)。

f、JVM启动时被标记为启动类的类(简单理解为具有main方法的类)。

JVM Execution Engine

指令执行

在执行方法时 JVM 提供了四种指令来执行:

  1. invokestatic:调用类的static方法

  2. invokevirtual:调用对象实例的方法

  3. invokeinterface:将属性定义为接口来进行调用

  4. invokespecial:JVM 对于初始化对象(Java 构造器的方法为:)以及调用对象实例中的私有方法时。

主要执行技术

由于 JVM 执行的是平台无关的字节指令,因此需要先将字节指令转换为特定的处理器硬件平台对应的指令代码才能在平台上运行。

1) 解释模式

由 interpreter 将每个 Java 指令都转译成对等的微处理器指令,并根据转译后的指令先后次序依序执行,由于一个 Java 指令可能被转译成十几或数十几个对等的微处理器指令,这种模式执行的速度会比较慢。

2) JIT 模式

Just In Time(即时编译)。

当 Java 执行 runtime 环境时,每遇到一个新的类,JIT 编译器在此时就会针对这个类进行编译(compile)作业。经过编译后的程序,被优化成更为精简的原生指令码(native code),因此执行速度更快。以少许的编译时间来节省稍后相当长的执行时间。虽然 JIT 可以提高效率,但是并未达到最佳性能,因为某些极少执行到的 Java 指令在编译时所额外花费的时间可能比转译器在执行时的时间还长。

3) 自适应优化

使用动态编译器仅针对较常被执行的程序码进行编译,其余部分仍使用解释模式来执行。动态编译器通过某种机制,对每个类进行分析,然后决定使用哪种方式。动态编译器针对程序的特性或者是让程序执行几个循环,再根据结果决定是否编译这段程序码。动态编译器会根据“历史数据”做决策,所以程序执行的时间愈长,判断正确的机率就愈高。以整个结果来看,动态编译器的执行速度超越以前的 JIT 技术,平均速度可提高至50%。

(Sun 的 HotspotJVM 采用这种技术)

JVM 生命周期

一个 JVM 实例对应一个独立运行的 Java 程序, 它属于进程级别。

而 JVM 执行引擎(Execution Engine)实例则是线程级别的。

JVM 的生命周期如下:

1) 启动

启动一个 Java 程序时,一个 JVM 实例就产生了,任何一个拥有 public static void main(String[] args) 函数的 class 都可以作为 JVM 实例运行的起点。

b) 运行

main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM 内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由 JVM 自己使用,Java 程序也可以标明自己创建的线程是守护线程。

c) 终止

当程序中的所有非守护线程都终止时,JVM 才退出;若安全管理器允许,程序也可以使用 Runtime 类或者 System.exit() 来退出。

Reference

  1. Jvm工作原理学习笔记 - OPEN 开发经验库. http://www.open-open.com/lib/view/open1408453806147.html

  2. 舒の随想日记 » 浅析Java虚拟机结构与机制. http://blog.hesey.net/2011/04/introduction-to-java-virtual-machine.html