引言
本系列文章整理了 JVM 相关的知识,本文作为开篇简单地介绍一下 JVM 的组成和结构,更多关于 JVM 的文章均收录于<JVM系列文章>。
简介
所谓虚拟机,就是一台虚拟的计算机,它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统级虚拟机和程序虚拟机。前者有 Visual Box 等,是对真实物理计算机的仿真,提供一个完整的可运行操作系统的平台。而后者则专门为执行单个计算机程序设计,在 Java 虚拟机中执行的指令被称为 Java 字节码指令。
Java 程序可以通过 Java 虚拟机运行于各大主流体系结构的平台上,它以虚拟机为中介,实现跨平台的特性。
Java 虚拟机发展至今,出现过不少实现方案。最初,Sun 使用的是一款 Classic JVM,它是一个纯解释型 JVM 实现。后来一家名为 “Longview Technologies” 的小公司开发出了性能优异的 HostSpot JVM,这家公司后被 Sun 收购,于是 HotSpot 就成了 JDK1.3 及之后的默认虚拟机。
除了,Sun 公司之外,各大公司以及组织都层级及研发过 Java 虚拟机,比如 BEA 的 JRockit,目前,JRockit 和 HotSpot 都被收入 Oracel 旗下,并被合并为一个整体,合并版的 JVM 也叫 HotSpot。由于这款 HotSpot 虚拟机占有绝对的市场地位,所以后续我们的介绍中都是针对它展开的。
提到 JVM 我们就不得不提一下 Java 语言规范和 JVM 规范的关系。虽然 Java 语言和 Java 虚拟机有着紧密的联系,但是两者的规范是各自独立的。JVM 是一台可以执行 Java 字节码的虚拟计算机,它拥有独立的运行机制,其运行的 Java 字节码也未必由 Java 语言编译而成,像 Groovy、Scala 等语言都能生成 Java 字节码并在 JVM 上运行。基于 JVM 不仅可以实现跨平台特性,也能实现跨语言特性,只要这些语言能够编译成 Java 字节码。
基本结构
在展开介绍 JVM 的内部构造之前,我们先来了解一下整个 JVM 的基本结构是怎么样的。
- 类加载子系统负责从文件系统或者网络中加载 Class 信息, 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外, 方法区中可能还会存放运行时常量池, 包括字符串内容和数字常量(这部分常量信息是 Class 文件中常量池部分的内存映射)。
- Java 堆在虚拟机启动的时候建立, 它是 Java 程序最主要的内存工作区域。几乎所有的 Java 对象实例都存放于 Java 堆中。堆空间是所有线程共享的, 这是一块与 Java 应用密切相关的内存区间。
- Java 的 NIO 库允许 Java 程序使用直接内存。直接内存是在 Java 堆外的、直接向系统申请的内存区间。通常, 访问直接内存的速度会优于 Java 堆。因此出于性能考虑, 读写频繁的场合可能会考虑使用直接内存。由于直接内存在 Java 堆外, 因此它的大小不会直接受限于 JVM 的最大堆大小, 但是系统内存是有限的, Java 堆和直接内存的总和依然受限于操作系统能给出的最大内存。
- 垃圾回收系统是 Java 虚拟机的重要组成部分, 垃圾回收器可以对方法区、Java 堆和直接内存进行回收。其中,Java 堆是垃圾收集器的工作重点。和 C/C++ 不同, Java 中所有的对象空间释放都是隐式的。也就是说, Java 中没有类似 free 或者 delete 这样的函数释放指定的内存区域。对于不再使用的垃圾对象, 垃圾回收系统会在后台默默工作, 默默查找、标识并释放垃圾对象, 完成包括 Java 堆、方法区和直接内存中的全自动化管理。
每一个 Java 虚拟机线程都有一个私有的 Java 栈。一个线程的 Java 栈在线程创建的时候被创建。Java 栈中保存着帧信息(一次方法调用都会生成对应的栈帧), 其中保存着局部变量、方法参数、操作数栈等, 同时它和 Java 方法的调用、返回密切相关。
因为 JVM 要实现跨平台特性,而不同平台上寄存器的数量各不相同,所以在 JVM 中数学计算是通过内存栈的形式来组织操作数,也就是上面所说的操作数栈。操作数栈也是栈帧中重要的内容之一, 它主要用于保存计算过程的中间结果, 同时作为计算过程中变量临时的存储空间。操作数栈是一个先进后出的数据结构, 只支持入栈和出桟两种操作。许多 Java 字节码指令都需要通过操作数栈进行参数传递。比如 iadd 指令, 它就会在操作数栈中弹出两个整数并进行加法计算, 计算结果会被入栈, 如下图所示, 显示了 iadd 前后操作数栈的变化。
除了局部变量表和操作数栈外, Java 栈帧还需要一些数据来支持常量池解析、正常方法返回和异常处理等。大部分 Java 字节码指令都需要进行常量池访问, 在帧数据区中保存着访问常量池的指针, 方便程序访问常量池。此外, 当函数返回或者出现异常时, 虚拟机必须恢复调用者函数的栈帧, 并让调用者函数继续执行下去。对于异常处理, 虚拟机必须有一个异常处理表, 方便在发生异常的时候找到处理异常的代码, 因此异常处理表也是帧数据区中重要的一部分。
Exception table: from to target type 4 16 19 any
上述异常处理表表示,字节码 4 ~ 16 字节可能抛出异常,如果遇到异常,则跳转到字节码偏移 19 处执行。如果方法抛出异常时,虚拟机就会自动查找类似的异常表来处理,如果无法在异常处理表中找到合适的处理方法,则会结束当前函数调用,返回调用者所在的函数,并在调用函数中抛出相同的异常,并查找调用函数的异常表进行处理。
- 本地方法栈和 Java 栈非常类似, 最大的不同在于 Java 桟用于 Java 方法的调用, 而本地方法栈则用于本地方法调用。作为对 Java 虛拟机的重要扩展, Java 虛拟机允许 Java 直接调用本地方法(通常使用 C 編写)。
- PC (Program Counter)寄存器也是每个线程私有的空间, Java 虚拟机会为每一个 Java 线程创建 PC 寄存器。在任意时刻,一个 Java 线程总是在执行一个方法, 这个正在被执行的方法称为当前方法。如果当前方法不是本地方法, PC 寄存器就会指向当前正在被执行的指令。如果当前方法是本地方法, 那么 PC 寄存器的值就是 undefined。
- 执行引擎是 Java 虚拟机的最核心组件之一, 它负责执行虚拟机的字节码。现代虚拟机为了提高执行效率, 会使用即时编译技术将方法编译成机器码后再执行。执行引擎的进一步细节我们会在后面介绍。
文章说明
更多有价值的文章均收录于贝贝猫的文章目录
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
创作声明: 本文基于下列所有参考内容进行创作,其中可能涉及复制、修改或者转换,图片均来自网络,如有侵权请联系我,我会第一时间进行删除。
参考内容
[1]《实战 Java 虚拟机》
[2]《深入理解 Java 虚拟机》
[3] GC复制算法和标记-压缩算法的疑问
[4] Java中什么样的对象才能作为gc root,gc roots有哪些呢?
[5] concurrent-mark-and-sweep
[6] 关于 -XX:+CMSScavengeBeforeRemark,是否违背cms的设计初衷?
[7] Java Hotspot G1 GC的一些关键技术
[8] Java 垃圾回收权威指北
[9] [[HotSpot VM] 请教G1算法的原理](https://hllvm-group.iteye.com/group/topic/44381)
[10] [[HotSpot VM] 关于incremental update与SATB的一点理解](https://hllvm-group.iteye.com/group/topic/44529)
[11] Java线程的6种状态及切换
[12] Java 8 动态类型语言Lambda表达式实现原理分析
[13] Java 8 Lambda 揭秘
[14] 字节码增强技术探索
[15] 不可不说的Java“锁”事
[16] 死磕Synchronized底层实现--概论
[17] 死磕Synchronized底层实现--偏向锁
[18] 死磕Synchronized底层实现--轻量级锁
[19] 死磕Synchronized底层实现--重量级锁