Java虚拟机--JVM简述 | 青训营笔记

114 阅读6分钟

这是我参与「第四届青训营 」笔记创作活动的第 12 天

本篇文章我们简单介绍下Java虚拟机--JVM基础知识,我们主要从JVM的定义、内存结构等方面来叙述,对JVM知识并不是很了解的同学可以仔细阅读。

JDK,JRE和JVM之间的区别

JVM

JVM(Java虚拟机)是一个抽象机器。它被称为虚拟机,因为它实际上不存在。它是一个规范,提供可以执行Java字节码的运行时环境。它还可以运行那些用其他语言编写并编译为Java字节码的程序。

JVM主要执行以下任务:

  • 加载代码
  • 验证代码
  • 执行代码
  • 提供运行时环境

针对此,我们可以得出以下结论:

  • JVM是一个能够运行字节码的虚拟机;
  • JVM提供的接口不依赖于底层操作系统和机器硬件;
  • JVM的以上两点特性,使得Java程序具有一次编译,到处执行的特性 JRE

JRE是Java Runtime Environment的首字母缩写,也被称为Java RTE;它是JVM的实现,是实际存在的。

JRE是一组用于开发Java应用程序的软件工具,用于提供运行时的环境;它包含一组库以及JVM在运行时使用的其他文件。

JDK

JDK是Java Development Kit的首字母缩写,它是一个软件开发环境,用于开发Java应用程序和applet;它是实际存在的。它包含JRE +开发工具。

JDK包含一个私有Java虚拟机(JVM)和一些其他资源,如解释器/加载器(java),编译器(javac),归档器(jar),文档生成器(Javadoc)等,以完成开发Java应用程序。

三者之间关系图 image.png

JVM内存结构

运行时数据区域

JVM的内存结构,往往指的就是JVM定义的 运行时数据区域(程序运行时,class文件会被类加载器装载至JVM中,并且JVM会负责程序 运行时的内存管理,而JVM的内存结构,指的就是JVM定义的 运行时数据区域)。

运行时数据区域 简单来说分为了5大块:方法区、堆、程序计数器、虚拟机栈、本地方法栈。

image.png 上面的图中,有两种颜色不同的区域,红色的是线程共享区域,绿色的是线程私有区域。
线程共享: 堆、方法区
线程隔离:虚拟机栈、本地方法栈、程序计数器

堆(Heap)

Java堆区具有下面几个特点:

  • 存储的是我们new的对象,不存放基本类型和对象引用;
  • 由于创建了大量的对象,所以垃圾回收器主要工作在这块区域;
  • 线程共享区域,几乎所有类的实例和数组分配的内存都来自于堆;
  • 能够发生OutOfMemoryError。

Java堆区 被划分为 新生代 和 老年代, 新生代 又被进一步划分为 Eden 和 Survivor 区,而 SurvivorFrom Survivor 和 To Survivor 组成。将 堆内存 分为这几块区域,主要跟 内存回收 有关(垃圾回收机制)。

image.png

方法区(Method Area)

方法区主要是用来存储已被虚拟机加载的 类相关信息:包括类信息、常量池。 方法区是线程共享区域,因此这是线程不安全的区域。

类信息 包括了 类的版本、字段、方法、接口和父类 等信息;
常量池 可以分 静态常量池运行时常量池

  • 静态常量池 主要存储的是 字面量 以及 符号引用 等信息,静态常量池也包括我们说的 字符串常量池;
  • 运行时常量池 存储的是 类加载时生成的直接引用 等信息(Java程序运行后,Class文件中的信息被字节码执行引擎加载到了方法区,从而形成了运行时常量池)。

拓展知识:

我们知道 运行时数据区 这个 分区 是JVM的 规范,具体的落地实现,不同的虚拟机厂商可能是不一样的。

在HotSpot虚拟机中,就会常常提到 永久代 这个词。HotSpot虚拟机在 JDK8前永久代 实现了 方法区,而在JDK8中,使用 元空间 来替代 永久代 作为 方法区 的实现。很多其他厂商的虚拟机中其实是没有 永久代 这个概念的。

JDK8中,使用的 元空间 存储不在虚拟机中,而是使用本地内存,JVM 不会再出现方法区的内存溢出,以往永久代 经常因为内存不够用导致抛出OOM异常。

在JDK8版本中,类信息 是存储在 元空间 的;而 常量池 从JDK7开始,从物理存储的角度上看就是在 中。 image.png

程序计数器

进程是资源分配的最小单位,线程是CPU调度的最小单位,一个进程可以包含多个线程,Java线程通过抢占的方法获得CPU的执行权

Java是多线程的语言,假设线程数大于CPU数,就会出现 线程切换 的现象,线程切换意味着「中断」和「恢复」,那就需要有一块区域来保存「当前线程的执行信息」。

所以,程序计数器用于记录各个线程执行的字节码的地址(分支、循环、跳转、异常、线程恢复等)。它具有以下特点:线程私有,每一个线程都有一个程序计数器,因此它是线程安全的

虚拟机栈

每个线程在创建的时候都会创建一个虚拟机栈,每次方法调用都会创建一个 栈帧,方法执行完毕后该栈帧就会被销毁,栈帧是以先进后出的方式进出虚拟机栈的

每个 栈帧 会包含几块内容:局部变量表、操作数栈、动态连接和返回地址

所以 虚拟机栈 的作用就是:保存方法的局部变量、部分变量的计算并参与方法的调用和返回。

这个区域可能有两种异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError异常(通常是递归导致的);
JVM动态扩展时无法申请到足够内存则抛出 OutOfMemoryError异常。

本地方法栈

本地方法栈跟虚拟机栈的功能类似,虚拟机栈用于管理 Java 函数的调用,而本地方法栈则用于管理本地方法的调用。这里的 本地方法 指的是 非Java方法 ,一般 本地方法 是使用 C语言 实现的。

总结

JVM内存结构 又称为 运行时数据区域 ,主要由五部分组成:虚拟机栈、本地方法栈、程序计数器、方法区 和 堆。其中方法区和堆是线程共享的;虚拟机栈、本地方法栈以及程序计数器是线程隔离的。