JVM 虚拟机详解

1、 什么是 JVM?

  JVM 是 Java Virtual Machine(Java 虚拟机)的缩写,JVM 是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java 虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。 JVM 屏蔽了与具体操作系统平台相关的信息,使 Java 程序只需生成在 Java 虚拟机上运行的目标代码(字节码), 就可以在多种平台上不加修改地运行。JVM 在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。

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

2、JRE/JDK/JVM 是什么关系?

JRE(JavaRuntimeEnvironment,Java 运行环境),也就是 Java 平台。所有的 Java 程序都要在 JRE 下才能运行。普通用户只需要运行已开发好的 java 程序,安装 JRE 即可。

JDK(Java Development Kit) 是程序开发者用来来编译、调试 java 程序用的开发工具包。JDK 的工具也是 Java 程序,也需要 JRE 才能运行。为了保持 JDK 的独立性和完整性,在 JDK 的安装过程中,JRE 也是 安装的一部分。所以,在 JDK 的安装目录下有一个名为 jre 的目录,用于存放 JRE 文件。

JVM(JavaVirtualMachine,Java 虚拟机) 是 JRE 的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM 有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java 语言最重要的特点就是跨平台运行。使用 JVM 就是为了支持与操作系统无关,实现跨平台。

3、JVM 原理

  Java 编译器只要面向 JVM,生成 JVM 能理解的代码或字节码文件。Java 源文件经编译成字节码程序,通过 JVM 将每一条指令翻译成不同平台机器码,通过特定平台运行。

4、JVM 的体系结构

(1)java 栈内存,它等价于 C 语言中的栈, 栈的内存地址是不连续的, 每个线程都拥有自己的栈。 栈里面存储着的是 StackFrame,在《JVM Specification》中文版中被译作 java 虚拟机框架,也叫做栈帧。StackFrame 包含三类信息:局部变量,执行环境,操作数栈。局部变量用来存储一个类的方法中所用到的局部变量。执行环境用于保存解析器对于 java 字节码进行解释过程中需要的信息,包括:上次调用的方法、局部变量指针和 操作数栈的栈顶和栈底指针。操作数栈用于存储运算所需要的操作数和结果。StackFrame 在方法被调用时创建,在某个线程中,某个时间点上,只有一个 框架是活跃的,该框架被称为 Current Frame,而框架中的方法被称为 Current Method,其中定义的类为 Current Class。局部变量和操作数栈上的操作总是引用当前框架。当 Stack Frame 中方法被执行完之后,或者调用别的 StackFrame 中的方法时,则当前栈变为另外一个 StackFrame。Stack 的大小是由两种类 型,固定和动态的,动态类型的栈可以按照线程的需要分配。 下面两张图是关于栈之间关系以及栈和非堆内存的关系基本描述:

(2) Java 堆是用来存放对象信息的,和 Stack 不同,Stack 代表着一种运行时的状态。换句话说,栈是运行时单位,解决程序该如何执行的问题,而堆是存储的单位, 解决数据存储的问题。Heap 是伴随着 JVM 的启动而创建,负责存储所有对象实例和数组的。堆的存储空间和栈一样是不需要连续的。

(3)想要了解更多 JVM 虚拟机以及 JVM 性能调优的,可以关注我一下,我后续也会整理更多 Java 架构这一块的知识点分享出来,另外顺便给大家推荐一个 Java 程序员中高级程度的交流学习群:650385180,里面会分享一些资深架构师录制的视频录像:有 Spring,MyBatis,Netty 源码分析,高并发、高性能、分布式、微服务架构的原理,JVM 性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,相信对于已经工作和遇到技术瓶颈的码友,在这个群里一定有你需要的内容。

(4)程序计数寄存器,程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

  由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Natvie 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

(5)方法区域(Method Area),在 Sun JDK 中这块区域对应的为 PermanetGeneration,又称为持久代。方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为 final 类型的常量、类中的 Field 信息、类中的方法信息,当开发人员在程序中通过 Class 对象中的 getName、isInterface 等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被 GC,当方法区域需要使用的内存超过其允许的大小时,会抛出 OutOfMemory 的错误信息。

(6)运行时常量池(Runtime Constant Pool),存放的为类中的固定的常量信息、方法和 Field 的引用信息等,其空间从方法区域中分配。

(7)本地方法堆栈(Native Method Stacks),JVM 采用本地方法堆栈来支持 native 方法的执行,此区域用于存储每个 native 方法调用的状态。