当前位置: 首页 > news >正文

Java虚拟机(Java Virtual Machine,JVM)

一、Java 虚拟机

Java 虚拟机(Java Virtual Machine, JVM)是运行 Java 字节码的虚拟机。它是Java平台的核心组件之一,使得Java程序具有 一次编写,到处运行(Write Once, Run Anywhere) 的特性。

JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言 一次编写,到处运行 的关键所在。

二、类加载机制与字节码

1. 字节码

计算机无法直接执行Java代码,必须通过Java虚拟机(JVM)来运行。首先,Java源代码需要经过编译器编译成Java字节码(.class文件)。然后,JVM加载并执行这些字节码。字节码是JVM的中间语言,它使得Java程序能够在不同的硬件和操作系统平台上运行,只要该平台上有相应的JVM实现。

Java代码间接翻译成字节码,储存字节码的文件再交由运行于不同平台上的JVM虚拟机去读取执行,从而实现一次编写,到处运行的目的。

许多基于JVM的编程语言,如Groovy、Scala、Koltin等编程语言,也被JVM支持。
在这里插入图片描述

2. 类加载机制

2.1 类加载过程

类加载过程是Java虚拟机(JVM)将类的字节码加载到内存中,并将其转换为可供JVM运行的类对象的过程。以下是类加载过程的详细步骤:

(1)类加载过程三个阶段

类加载过程主要分为三个阶段:加载、连接和初始化,其中连接阶段可以分为 验证、准备和解析三个子阶段。

  1. 加载(Loading):通过类的全限定名来获取这个类的二进制字节流;将字节流转化为方法区的运行时数据结构;在内存中生成一个代表这个类的 java.lang.Class 对象,作为这个类在方法区的访问入口。

  2. 连接(Linking) :连接阶段又分为三个子阶段:验证、准备和解析。

    • 验证(Verification)验证的作用是保证加载的字节码是合法的。先后验证字节码文件格式(字节码版本号、文件结构等)、字节码指令(检查指令的类型转换是否合法、操作数栈的深度是否合理等)是否符合JVM规范;验证类的元数据信息(类名、字段名、方法名)是否符合Java语言规范;验证类的符号引用是否可以被解析为直接引用(检查类的父类、接口、字段和方法是否存在等)。
    • 准备(Preparation):准备的作用是 正式为类的静态变量分配内存,并设置默认初始值和代码中声明的初始值。
    • 解析(Resolution):解析的作用是 将常量池内的符号引用替换为直接引用的过程。将类或接口的符号引用转换为直接引用。
  3. 初始化(Initialization):执行类构造器<clinit>()方法,初始化类的静态变量和静态代码块,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。

在这里插入图片描述

类加载过程是Java程序运行的基础,确保了类的字节码能够被正确加载、验证、准备和初始化,为程序的执行提供了必要的环境和资源。

(2)类加载过程的触发时机

类加载过程通常在以下情况下触发:

  • 创建类的实例时(例如使用new关键字)。
  • 访问类的静态变量或静态方法时。
  • 反射调用时(例如使用Class.forName()方法)。
  • 初始化子类时,会先触发父类的初始化。
  • Java虚拟机启动时,会加载核心类库和应用程序的主类.

2.2 类加载器

类加载器是负责加载类字节码到JVM中的组件,主要有以下几种:

  • 启动类加载器(Bootstrap ClassLoader):由C++编写,负责加载JVM核心类库(如rt.jar)和扩展类库(如ext目录下的jar文件)。
  • 扩展类加载器(Extension ClassLoader):由Java编写,负责加载JVM扩展目录中的类库。
  • 应用类加载器(Application ClassLoader):也称为系统类加载器,由Java编写,负责加载应用程序类路径(如CLASSPATH环境变量指定的路径)中的类库。
  • 用户自定义类加载器:可以通过继承ClassLoader类来实现自定义的类加载器,以满足特定的加载需求。

2.3 双亲委派模型(Parent Delegation Model)

双亲委派模型(Parent Delegation Model)是Java虚拟机(JVM)中类加载器加载类时的一种机制。它规定了类加载器之间如何协作,确保类的唯一性和安全性。

(1)双亲委派模型工作原理
  1. 当一个类加载器尝试加载某个类时,它首先会将请求委托给它的父类加载器去加载。父类加载器会继续委托给它的父类加载器,这个过程会递归向上进行,直到到达顶层的 应用类加载器(Application ClassLoader) 扩展类加载器(Extension ClassLoader)、启动类加载器(Bootstrap ClassLoader)。
  2. 如果父类加载器无法完成加载请求(即该类不在父类加载器的搜索范围内),子类加载器才会尝试自己去加载该类。
  3. 一旦某个类加载器成功加载了某个类,那么这个类就由该类加载器负责,其他类加载器不再参与这个类的加载过程。

在这里插入图片描述

(2)双亲委派模型的实现

双亲委派模型的实现主要依赖于 java.lang.ClassLoader 的 loadClass(String name, boolean resolve) 方法。以下是该方法的简化实现逻辑:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// 首先检查是否已经加载过该类Class<?> c = findLoadedClass(name);if (c == null) {try {// 委托给父类加载器加载if (parent != null) {c = parent.loadClass(name, false);} else {// 如果没有父类加载器,则由启动类加载器加载c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// 父类加载器无法加载时,子类加载器尝试加载c = findClass(name);}}if (resolve) {// 解析类resolveClass(c);}return c;}
}

三、JVM内存模型与垃圾回收

1. JVM 内存模型

Java虚拟机(JVM)内存模型定义了Java程序在运行时如何管理和操作内存资源。它详细描述了Java程序在执行过程中,数据如何在不同的内存区域之间流动和存储。以下是JVM内存模型的主要组成部分:

  1. 程序计数器(Program Counter Register):存储当前线程执行的字节码的行号指示器。它是一个小的内存区域,用于指示当前线程正在执行的字节码指令的位置。
    程序计数器是线程私有的,每个线程都有自己的程序计数器,是线程切换后能够恢复执行的关键。当线程切换时,程序计数器会保存当前线程的执行位置,以便线程恢复执行时能够从正确的位置继续执行.

  2. 虚拟机栈(Virtual Machine Stack):存储局部变量、方法调用的上下文信息和部分结果。Java栈由栈帧(Stack Frame)组成,每个栈帧对应一个方法调用。栈帧结构包括 存储方法的参数和局部变量的局部变量表、用于存储字节码指令的操作数和中间结果的操作数栈、存储对当前方法所属类的运行时常量池的引用,以及对被调用方法的符号引用的动态链接信息、存储方法返回后的执行位置的方法返回地址。
    Java栈是线程私有的,每个线程都有自己的Java栈。栈帧的创建和销毁与方法的调用和返回同步进行。

  3. 本地方法栈(Native Method Stack):为JVM使用到的Native方法(本地方法)提供服务。本地方法栈与Java栈类似,但用于执行非Java代码(如C/C++代码)。
    本地方法栈是线程私有的,其实现和具体平台有关,其结构和功能与Java栈类似,但主要用于支持本地方法的执行。

  4. 堆(Heap):存储对象实例和数组。堆是JVM中最大的一块内存区域,是垃圾回收器管理的主要区域。
    堆是所有线程共享的内存区域。堆中对象的内存分配和回收是动态进行的,由垃圾回收器负责。
    JVM中堆空间可以分成 新生代( Young Generation)老年代( Old Generation )永久代(Permanent Generation) 三个大区。新生代用于存储新创建的对象,老年代用于存储经过多次垃圾回收仍然存活的对象。新生代可以划分为三个区,Eden区,From Survivor区、To Survivor区两个幸存区。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。

    • 新生代中一般保存新出现的对象,所以每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
    • 老年代中一般保存存活了很久的对象,他们存活率高、没有额外空间对它进行分配担保,就必须采用标记-清理或者标记-整理算法。
    • 永久代就是JVM的方法区。在这里都是放着一些被虚拟机加载的类信息,静态变量,常量等数据。这个区中的东西比老年代和新生代更不容易回收。
  5. 方法区(Method Area):存储类的结构信息,如类名、字段信息、方法信息、常量池等。
    方法区是所有线程共享的内存区域。方法区的大小是有限的,如果超出限制,会抛出OutOfMemoryError异常。
    HotSpot 虚拟机对虚拟机规范中方法区的实现方式包括永久代以及元空间,永久代是 JDK 1.8 之前的方法区实现,元空间是JDK 1.8 及以后方法区的实现。
    在这里插入图片描述

JVM内存模型通过这些内存区域的协同工作,为Java程序的运行提供了必要的内存支持和管理机制,确保程序能够高效、安全地执行.

2. 垃圾回收

2.1 判断对象被垃圾回收的方法

(1)引用计数法(Reference Counting)

给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。因此 Java 虚拟机不使用引用计数算法。

(2)局部可达算法(Reachability Analysis)

维护一个 GC Roots Set 集合,存放着所有的 GC Roots ,通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。

Java 虚拟机使用局部可达算法来判断对象是否可被回收。
在 Java 中 GC Roots 一般包含:虚拟机栈中引用的对象、本地方法栈中引用的对象、方法区中类静态属性引用的对象、方法区中的常量引用的对象。
在这里插入图片描述
如上图,通过 GC Roots 作为起始点进行搜索,Object1、Object3、Object5、Object6 、Object7 对象是能够到达到的对象,局部可达算法将这些对象判断为存活对象;而Object2、Object4 对象是不能到达到的对象,局部可达算法认为这些对象可被回收。

2.2 垃圾回收算法

垃圾回收(Gabage Collection,GC )功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。

  • 标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
    在这里插入图片描述

  • 标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
    在这里插入图片描述

  • 复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
    在这里插入图片描述

  • 分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。

2.3 垃圾回收机制

  • Minor GC:是新生代GC,指的是发生在新生代的垃圾收集动作。由于Java对象大都是朝生夕死的,所以Minor GC非常频繁,一般回收速度也比较快。(一般采用复制算法回收垃圾)
  • Major GC:是老年代GC,指的是发生在老年代的GC,通常执行Major GC会连着Minor GC一起执行。Major GC的速度要比Minor GC慢的多。(可采用标记清楚法和标记整理法)
  • Full GC:是清理整个堆空间,包括年轻代和老年代

触发 Full GC 的情况:

  1. 调用System.gc()
    只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
  2. 未指定老年代和新生代大小,堆伸缩时会产生 Full GC,所以一定要配置-Xmx、-Xms
  3. 老年代空间不足

2.4 垃圾收集器

HotSpot 虚拟机中包含 9 个垃圾收集器,包括 Serial 收集器、ParNew 收集器、Parallel Scavenge 收集器、Serial Old 收集器、Parallel Old 收集器、MS 收集器、G1 收集器、ZGC 收集器、Shenandoah GC 收集器。

垃圾收集器的几个概念:

  • 单线程与多线程:
    • 单线程指的是垃圾收集器使用一个线程进行收集;
    • 多线程指的是垃圾收集器使用多个线程进行收集。
  • 串行(Serial)与并行(Parallel):
    • 串行指的是垃圾收集器与用户程序交替执行,因此在执行垃圾收集的时候需要停顿用户程序;
    • 并行指的是垃圾收集器和用户程序同时执行,因此在执行垃圾收集的时候不必停顿用户程序。
(1)Serial 收集器

从JDK 1.3之前开始使用。

它是单线程的收集器,只会使用一个线程进行垃圾收集工作;以串行的方式执行,垃圾收集器与用户程序交替执行。

Serial 收集器采用标记-复制算法。

Serial 收集器是 HotSpot 中 Client 模式下默认的新生代垃圾收集器

它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。

(2)ParNew 收集器

从JDK 1.4开始引入,是Serial收集器的多线程版本。

它是多线程的收集器,同时使用一个线程进行垃圾收集工作;以并行的方式执行,垃圾收集器与用户程序交替执行。

ParNew 收集器采用标记-复制算法。

ParNew 收集器是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。

默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。

(3)Parallel Scavenge 收集器

从JDK 1.5开始引入,是Serial收集器的多线程版本。

其它收集器关注点是用户线程的停顿时间(提高用户体验),而Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量(高效率的利用 CPU),它被称为吞吐量优先收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。

Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略(GC Ergonomics),把内存管理优化交给虚拟机去完成也是一个不错的选择。

(4)Serial Old 收集器

从JDK 1.3之前开始使用,是Serial收集器的老年代版本。

(5)Parallel Old 收集器

从JDK 1.6开始引入,是Parallel Scavenge收集器的老年代版本。

(6)MS 收集器

从JDK 1.5开始引入

(7)G1 收集器

从JDK 1.7u4版本开始引入

(8)ZGC 收集器

ZGC 收集器从JDK 11开始引入

(9)Shenandoah GC 收集器

Shenandoah GC 收集器从JDK 12开始引入


http://www.mrgr.cn/news/82707.html

相关文章:

  • STM32传感器系列:GPS定位模块
  • Visual studio code编写简单记事本exe笔记
  • 小白学Pytorch
  • Redis中字符串和列表的区别
  • 生成一个mosaic增强的图片
  • 奥迪TT MK1(初代奥迪TT、第一代奥迪TT)仪表盘故障/不精准/水温/剩余油量不准,如何修复、测试、复位?
  • 学习Video.js
  • K8s高可用集群之Kubernetes集群管理平台、命令补全工具、资源监控工具部署及常用命令
  • 第四、五章补充:线代本质合集(B站:小崔说数)
  • [SAP ABAP] SMARTFORMS表单开发
  • Nginx (40分钟学会,快速入门)
  • 【操作系统不挂科】操作系统期末考试卷<2>(单选题&简答题&计算与分析题&程序分析题&应用题)
  • 01:C语言的本质
  • 深入探索 Kubernetes:从基础概念到实战运维
  • LLM - 使用 LLaMA-Factory 部署大模型 HTTP 多模态服务 教程 (4)
  • 多模态论文笔记——CogVLM和CogVLM2
  • 毕业项目推荐:基于yolov8/yolov5的行人检测识别系统(python+卷积神经网络)
  • 【Unity3D】UGUI Canvas画布渲染流程
  • TP8 前后端跨域访问请求API接口解决办法
  • 基于海思soc的智能产品开发(camera sensor的两种接口)
  • 【Vim Masterclass 笔记05】第 4 章:Vim 的帮助系统与同步练习(L14+L15+L16)
  • 【C++】B2104 矩阵加法
  • 【MyBatis-Plus 进阶功能】开发中常用场景剖析
  • Markdown中流程图的用法
  • 【C++】P5732 【深基5.习7】杨辉三角
  • 【C++】B2103 图像相似度