JVM 系列 (一) - JVM 总体概述

本贴最后更新于 2209 天前,其中的信息可能已经水流花落

JVM 系列(一) - JVM 总体概述

原文地址: Icarus

前言

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

JVM 屏蔽了与具体操作系统平台相关的信息,使 Java 程序只需生成在 Java 虚拟机上一次编译,多次运行,具有跨平台性JVM 在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。

Java 虚拟机包括一套字节码指令集、一组寄存器、一个、一个垃圾回收堆和一个存储方法区

本文将简述以下内容:

  • JVM 是什么?

  • JVM 能干什么?

  • JVM 生命周期?

  • JVM 组成架构?

# 正文

JVM 是什么

JDK、JRE 和 JVM 对比

JVMJREJDK 都是 java 语言的支柱,他们分工协作。但不同的是 JdkJRE 是真实存在的,而 JVM 是一个抽象的概念,并不真实存在。

JDK

JDK(Java Development Kit) 是 Java 语言的软件开发工具包( SDK)。 JDK 物理存在,是 programming toolsJREJVM 的一个集合。

JRE

JRE(Java Runtime Environment) Java 运行时环境, JRE 是物理存在的,主要由 JavaAPIJVM 组成,提供了用于执行 java 应用程序最低要求的环境。

JVM

JVM 是一种用于计算设备的规范,它是一个虚构的计算机的软件实现,简单的说, JVM 是运行 bytecode 字节码程序的一个容器。

JVM 的特点

  • 基于堆栈的虚拟机:最流行的计算机体系结构,如英特尔 X86 架构和 ARM 架构上运行基于 寄存器。比如,安卓的 Davilk 虚拟机就是基于 寄存器 结构,而 JVM 是基于栈结构的。

  • 符号引用 :除了基本类型以外的数据 (类和接口) 都是通过符号来引用,而不是通过显式地使用内存地址来引用。

  • 垃圾收集 :一个类的实例是由用户程序创建和垃圾回收自动销毁

  • 网络字节顺序Javaclass 文件用网络字节码顺序来进行存储,保证了小端的 Intelx86 架构和大端的 RISC 系列的架构之间的无关性。

JVM 字节码

JVM 使用 Java 字节码的方式,作为 Java 用户语言机器语言 之间的中间语言。实现一个通用的机器无关 的执行平台。

JVM 能干什么

基于安全方面考虑, JVM 要求在 class 文件中使用强制性的语法和约束,但任意一门语言都可以转换为被 JVM 接受的有效的 class 文件。作为一个通用的、机器无关的执行平台,任何其他语言的实现者都可将 JVM 当作他的语言产品交付媒介。

JVM 中执行过程如下:

  • 加载代码

  • 验证代码

  • 执行代码

  • 提供运行环境

JVM 生命周期

  • 启动:任何一个拥有 main 方法的 class 都可以作为 JVM 实例运行的起点。

  • 运行main 函数为起点,程序中的其他线程均有它启动,包括 daemon 守护线程和 non-daemon 普通线程。 daemonJVM 自己使用的线程比如 GC 线程, main 方法的初始线程是 non-daemon

  • 消亡:所有线程终止时, JVM 实例结束生命。

JVM 组成架构

JAVA 代码执行过程如下:

1. 类加载器(Class Loader)

类加载器 负责加载程序中的类型(类和接口),并赋予唯一的名字予以标识。

JDK 默认提供的三种 ClassLoader 如下:

类加载器的关系

  1. BootstrapClassloader 是在 Java 虚拟机启动后初始化的。

  2. BootstrapClassloader 负责加载 ExtClassLoader,并且将 ExtClassLoader 的父加载器设置为 BootstrapClassloader

  3. BootstrapClassloader 加载完 ExtClassLoader 后,就会加载 AppClassLoader,并且将 AppClassLoader 的父加载器指定为 ExtClassLoader

类加载器的作用

Class Loader 实现 负责加载
Bootstrap Loader C++ %JAVA_HOME%/jre/lib, %JAVA_HOME%/jre/classes 以及-Xbootclasspath 参数指定的路径以及中的类
Extension ClassLoader Java %JAVA_HOME%/jre/lib/ext,路径下的所有 classes 目录以及 java.ext.dirs 系统变量指定的路径中类库
Application ClassLoader Java Classpath 所指定的位置的类或者是 jar 文档,它也是 Java 程序默认的类加载器

双亲委托机制

JavaClassLoader 的加载采用了双亲委托机制,采用双亲委托机制加载类的时候采用如下的几个步骤:

  1. 当前 ClassLoader 首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。

  2. 当前 ClassLoader 的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到 BootstrapClassLoader

  3. 当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。

小结 :双亲委托机制的核心思想分为两个步骤。其一,自底向上检查类是否已经加载;其二,自顶向下尝试加载类。

ClassLoader 隔离问题

每个类装载器都有一个自己的命名空间用来保存已装载的类。当一个类装载器装载一个类时,它会通过保存在命名空间里的类全局限定名( FullyQualifiedClassName)进行搜索来检测这个类是否已经被加载了。

JVMDalvik 对类唯一的识别是 ClassLoaderid + PackageName + ClassName,所以一个运行程序中是有可能存在两个包名和类名完全一致的类的。并且如果这两个”类”不是由一个 ClassLoader 加载,是无法将一个类的示例强转为另外一个类的,这就是 ClassLoader 隔离。

双亲委托ClassLoader 类一致问题的一种解决方案,也是 Android 差价化开发和热修复的基础。

类装载器特点

Java 提供了动态加载特性。在运行时的第一次引用到一个 class 的时候会对它进行装载(Loading) 、** ****连接(Linking) 和 初始化(Initialization) ***,而不是在编译时进行。不同的 JVM 的实现不同,本文所描述的内容均只限于 HotspotJVM

JVM 的类装载器负责动态装载, Java 的类装载器有如下几个特点:

  • 层级结构:Java 里的类装载器被组织成了有父子关系的层级结构。Bootstrap 类装载器是所有装载器的父亲。

  • 代理模式: 基于层级结构,类的代理可以在装载器之间进行代理。当装载器装载一个类时,首先会检查它在父装载器中是否进行了装载。如果上层装载器已经装载了这个类,这个类会被直接使用。反之,类装载器会请求装载这个类

  • 可见性限制:一个子装载器可以查找父装载器中的类,但是一个父装载器不能查找子装载器里的类。

  • 不允许卸载:类装载器可以装载一个类但是不可以卸载它,不过可以删除当前的类装载器,然后创建一个新的类装载器装载。

类装载器过程

1)加载(Loading)

首先,根据类的全限定名找到代表这个类的 Class 文件,然后读取到一个字节数组中。接着,这些字节会被解析检验它们是否代表一个 Class 对象并包含正确的 majorminor 版本信息。直接父类 的类和接口也会被加载进来。这些操作一旦完成,类或者接口对象 就从二进制表示中创建出来了。

2)连接(Linking)

链接是检验类或接口并准备类型和父类接口的过程。链接过程包含三步:校验(Verifying)准备(Preparing)部分解析(Optionally resolving)

  • 验证

这是类装载中最复杂的过程,并且花费的时间也是最长的。任务是确保导入类型的准确性,验证阶段做的检查,运行时不需要再做。虽然减慢加了载速度,但是避免了多次检查。

  • 准备

准备过程通常分配一个结构用来存储类信息,这个结构中包含了类中定义的成员变量方法接口信息等。

  • 解析

解析是可选阶段,把这个类的常量池中的所有的符号引用改变成直接引用。如果不执行,符号解析要等到字节码指令使用这个引用时才会进行。

3)初始化(Initialization)

把类中的变量初始化成合适的值。执行静态初始化程序,把静态变量初始化成指定的值。

JVM 规范定义了上面的几个任务,不过它允许具体执行的时候能够有些灵活的变动。

2. 执行引擎(Execution Engine)

通过类装载器装载的,被分配到 JVM运行时数据区的字节码会被执行引擎执行。

执行引擎指令为单位读取 Java 字节码。它就像一个 CPU 一样,一条一条地执行机器指令。每个字节码指令都由一个 1 字节的操作码和附加的操作数组成。执行引擎 取得一个操作码,然后根据操作数来执行任务,完成后就继续执行下一条操作码

不过 Java 字节码是用一种人类可以读懂的语言编写的,而不是用机器可以直接执行的语言。因此,执行引擎 必须把字节码转换成可以直接被 JVM 执行的语言。

字节码 可以通过以下两种方式转换成机器语言

  • 解释器

解释器 一条一条地读取字节码解释 并且 执行 字节码指令。因为它一条一条地解释和执行指令,所以它可以很快地解释字节码,但是执行起来会比较慢。这是解释执行的语言的一个缺点。字节码这种“语言”基本来说是解释执行的。

  • 即时(Just-In-Time)编译器

即时编译器 被引入用来弥补解释器的缺点。执行引擎 首先按照 解释执行 的方式来执行,然后在合适的时候,即时编译器整段字节码 编译成 本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行它。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。

Java 字节码是解释执行的,但是没有直接在 JVM 宿主执行原生代码快。为了提高性能, OracleHotspot 虚拟机会找到执行最频繁的字节码片段并把它们编译成原生机器码。编译出的原生机器码被存储在非堆内存的代码缓存中。

通过这种方法 (JIT)Hotspot 虚拟机将权衡下面两种时间消耗:将字节码编译成本地代码需要的额外时间和解释执行字节码消耗更多的时间。

这里插入一下 Android 5.0 以后用的 ART 虚拟机使用的是 AOT 机制。

Dalvik 是依靠一个 Just-In-Time(JIT) 编译器去解释字节码。开发者编译后的应用代码需要通过一个解释器在用户的设备上运行,这一机制并不高效,但让应用能更容易在不同硬件和架构上运行。 ART 则完全改变了这套做法,在应用安装时就预编译字节码到机器语言,这一机制叫 Ahead-Of-Time(AOT) 编译。在移除解释代码这一过程后,应用程序执行将更有效率,启动更快。

参考

周志明,深入理解 Java 虚拟机:JVM 高级特性与最佳实践,机械工业出版社

  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖 • 3 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...