1.什么是JVM?
JVM英文全称为Java Virtual Machine,Java虚拟机。
官方文档对其的定义为:
Java 虚拟机是 Java 平台的基石。它是该技术的组成部分,负责其硬件和操作系统独立性、编译代码的小尺寸以及保护用户免受恶意程序侵害的能力。
Java 虚拟机是一种抽象计算机。像真正的计算机一样,它具有指令集并在运行时操纵各种内存区域。使用虚拟机实现编程语言是相当普遍的;最著名的虚拟机可能是 UCSD Pascal 的 P-Code 机器。
Java 虚拟机的第一个原型实现由 Sun Microsystems, Inc. 完成,它在类似于当代个人数字助理 (PDA) 的手持设备托管的软件中模拟 Java 虚拟机指令集。Oracle 当前的实现在移动、桌面和服务器设备上模拟 Java 虚拟机,但 Java 虚拟机不采用任何特定的实现技术、主机硬件或主机操作系统。它本身不是解释的,但也可以通过将其指令集编译为硅 CPU 的指令集来实现。它也可以用微码或直接用硅实现。
Java 虚拟机对 Java 编程语言一无所知,只知道一种特定的二进制格式,即
class
文件格式。文件class
包含 Java 虚拟机指令(或字节码)和符号表,以及其他辅助信息。为了安全起见,Java 虚拟机对
class
文件中的代码施加了强大的语法和结构约束。但是,任何具有可以用有效class
文件表示的功能的语言都可以由 Java 虚拟机托管。被一个普遍可用的、独立于机器的平台所吸引,其他语言的实现者可以将 Java 虚拟机作为他们语言的交付工具。此处指定的 Java 虚拟机与 Java SE 17 平台兼容,并支持Java 语言规范 Java SE 17 版中指定的 Java 编程语言。
2. JVM的结构
2.1 运行时数据区
Java 虚拟机定义了在程序执行期间使用的各种运行时数据区域。其中一些数据区是在 Java 虚拟机启动时创建的,只有在 Java 虚拟机退出时才会被销毁。其他数据区域是每个线程。每个线程的数据区域在创建线程时创建,并在线程退出时销毁。
2.1.1 pc计数器
每个 Java 虚拟机线程都有自己的
pc
(程序计数器)寄存器。在任何时候,每个 Java 虚拟机线程都在执行单个方法的代码,即该线程的当前方法。如果该方法不是native
,则pc
寄存器包含当前正在执行的 Java 虚拟机指令的地址。如果线程当前正在执行的方法是native
,则 Java 虚拟机的pc
寄存器的值是未定义的。Java 虚拟机的pc
register 的宽度足以容纳returnAddress
特定平台上的本地指针或本地指针。
2.1.2 本地方法栈
Java 虚拟机的实现可以使用传统的堆栈,通俗地称为“C 堆栈”,以支持
native
方法(用 Java 编程语言以外的语言编写的方法)。本地方法栈也可以被 Java 虚拟机指令集的解释器实现使用,例如 C 语言。不能加载native
方法并且本身不依赖传统栈的 Java 虚拟机实现不需要提供本地方法栈. 如果提供,本机方法堆栈通常在创建每个线程时为每个线程分配。该规范允许本地方法堆栈具有固定大小或根据计算要求动态扩展和收缩。如果本地方法堆栈具有固定大小,则每个本地方法堆栈的大小可以在创建该堆栈时独立选择。
Java 虚拟机实现可以为程序员或用户提供对本地方法堆栈的初始大小的控制,以及在不同大小的本地方法堆栈的情况下,控制最大和最小方法堆栈大小。
2.1.3 虚拟机堆栈
每个 Java 虚拟机线程都有一个私有的Java 虚拟机堆栈,与线程同时创建。Java 虚拟机堆栈存储帧。Java 虚拟机堆栈类似于 C 等传统语言的堆栈:它保存局部变量和部分结果,并在方法调用和返回中发挥作用。因为除了推送和弹出帧外,Java 虚拟机堆栈永远不会被直接操作,因此帧可能是堆分配的。Java 虚拟机堆栈的内存不需要是连续的。
在Java® 虚拟机规范的第一版中,Java虚拟机堆栈被称为Java 堆栈。
该规范允许 Java 虚拟机堆栈具有固定大小或根据计算要求动态扩展和收缩。如果 Java 虚拟机堆栈具有固定大小,则每个 Java 虚拟机堆栈的大小可以在创建堆栈时独立选择。
Java 虚拟机实现可以让程序员或用户控制 Java 虚拟机堆栈的初始大小,以及在动态扩展或收缩 Java 虚拟机堆栈的情况下,控制最大和最小大小。
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。
操作数栈(Operand Stack)也称作操作栈,是一个后入先出栈(LIFO)。随着方法和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。
动态链接:Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态链接(Dynamic Linking)。
方法返回:无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。
2.1.4 方法区
Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区。方法区类似于传统语言的编译代码的存储区,或者类似于操作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括类和接口初始化以及实例初始化中使用的特殊方法。
方法区是在虚拟机启动时创建的。尽管方法区在逻辑上是堆的一部分,但简单的实现可能会选择不进行垃圾收集或压缩它。本规范不要求方法区域的位置或用于管理已编译代码的策略。方法区域可以是固定大小,也可以根据计算需要扩大,如果不需要更大的方法区域,可以缩小。方法区的内存不需要是连续的。
Java 虚拟机实现可以为程序员或用户提供对方法区域初始大小的控制,以及在方法区域大小可变的情况下,对最大和最小方法区域大小的控制。
需要注意的是:
在jdk7及以前,习惯上把方法区,称为永久代。jdk8开始,使用元空间取代了永久代。永久代和元空间都是方法区的实现。
方法区存储的是已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
2.1.5 堆
堆的目的就是存放对象。几乎所有的对象实例都在此分配.
- JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)。
- 年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。
- 堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。
- 非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。